gwt-lint¶
gwt-lint checks a change list against a snapshot of the tree before gwt-apply runs it. gwt-apply checks how an op is written; gwt-lint checks your research rules: which people a list may touch, which people it must leave alone, what may be written about people who may still be alive, and whether another list is working on the same objects.
Every rule ships its own broken copy of a list and the code it must fire on it, so gwt-lint --prove shows that each rule can still go red. You never write a red control by hand.
It reads files only: the list, a snapshot, and optionally a config file and folders of other lists. It never contacts a server.
gwt-lint changes.jsonl --snapshot tree.json.gz --root I0001 --protect I0009 --weak-tag weak --rules scope,protect,living,weak
gwt-lint changes.jsonl --snapshot tree.json.gz --config lint.toml --json
gwt-lint --prove changes.jsonl --snapshot tree.json.gz --config lint.toml
gwt-lint --list-rules
With no --rules, every rule runs, and a rule whose setting is missing (overlap without --open, say) makes the run exit 2. Keep the settings in a lint.toml, or name the rules you mean.
The snapshot is a gwt-check dump or a gwt-apply --save-snapshot file, plain or gzipped. Take it right before you lint: a list judged against an old tree is judged wrongly.
The list may be JSON Lines, a JSON array or {"ops": [...]}, the shapes gwt-apply reads. Before any rule runs, gwt-lint runs gwt-apply's own check of how each op is written, so a list gwt-apply would refuse exits 2 instead of linting clean.
Exit codes¶
| Exit | Meaning |
|---|---|
| 0 | every rule looked and found nothing |
| 1 | at least one finding |
| 2 | a rule had nothing to check (a missing setting, a root the snapshot does not have), the list, snapshot or config cannot be read, or gwt-apply would refuse the list |
2 wins over 1. A run where one rule did not look is not a clean run, whatever the other rules found, so a missing --root can never pass as "no scope findings".
Output¶
scope: 1 finding
scope-out op 'e-hugo-res' line 1: I7 (Hugo Vega) is not a blood relative of I1 or the spouse of one
protect: nothing to check: no protected people: give --protect or [protect] ids
living: ok
exit 2: a rule had nothing to check (1 finding from 3 rules)
-v adds each rule's notes, such as the blood path of every person in scope (I11 (Mario Ruiz): I1 > child I6 > spouse I11). --json prints one object: exit, and per rule its status (ok, findings or nothing), the reason for nothing, its info notes and its findings, each with rule, code, key, line, message and fix. When the run cannot start (an unreadable input, a bad setting), --json prints {"list", "exit": 2, "error"}.
Settings¶
Options on the command line win over lint.toml. Folders in lint.toml are read relative to the file. A setting that takes a list of names also takes one name as a plain string (roots = "I0001"); a setting of the wrong type exits 2. All names and IDs below are invented.
rules = ["scope", "protect", "living", "overlap", "weak", "count"] # default: all of them
[scope]
roots = ["I0001"] # --root
[protect]
ids = ["I0009", "I0010"] # --protect
locales = ["en", "es"] # life-event words from these locale tables; default: every locale
[living]
years = 110 # possibly living: no death and born less than this many years ago
[overlap]
open = "lists/open" # --open: lists not applied yet
applied = "lists/applied" # --applied: lists already applied
[weak]
tag = ["weak", "hipotesis"] # --weak-tag; the first is the one a fix suggests
machine_sources = ["S0042"] # sources whose citations come from a machine index
[count]
words = ["tree", "árbol"] # default: the word for "tree" in every gwt_core locale
--rules scope,living runs only those rules. --records DIR names a folder of saved records for rules that compare a list with them: the evidence rules (quote, principal, role, placement) read it, and with no --rules they run too. A list that cites a record or quotes one then needs saved records (exit 2 without them); one that does neither needs none.
Rules¶
Each rule judges an op on the tree as the list leaves it: the people, families and events the list adds (as key:<key>) and the links it adds count, so a child the list links into the root's family is in scope. What the list removes does not count, so an op is judged on the tree before the removal.
New text is every free-text field an op writes: note and transcription text, event descriptions, citation pages, media descriptions, source titles, authors and publication details, attribute values, and the reasons ops give (namesake_reason, reason, ...). Names, IDs, types and paths are not.
scope¶
Every person and family an op touches must be a blood relative of a root, or the spouse of one. Blood is the roots' ancestors and every descendant of those ancestors, through parent links of type Birth, Adopted, Unknown or none. A Stepchild, Foster or Sponsored link is never a blood line, and neither is a marriage: a spouse's brother is out. A family is in scope when one of its parents is a blood relative. An event touches the people and families that list it; a child_ref its child and family; notes, citations, media, places and sources touch nobody. namesake_of and new_couple_of name the people and families a new object is not, so they touch nobody either (here and in overlap).
| Code | When |
|---|---|
scope-out |
the op touches a person or family outside that set |
scope-unknown |
the op names a person, family or event that neither the snapshot nor the list has |
Nothing to check: no root, or a root the snapshot does not have.
protect¶
People who asked to stay out of the tree (the tree's owners, say) get no new data. No update or merge of them, no new or moved event on them or the families they head, no date, place or other change to their events (an update_event that sets only what the event already has is no change: gwt-apply skips it, and dates compare by what they mean, as gwt-apply compares them), no new parent or child link, no note, citation, media, attribute or association attached to them, and no new text that names them (by Gramps ID, or first name followed by a surname word) within eight words of a life-event word. The life-event words are the events section of the locale tables: born, baptised, married, died, buried and their forms in each language.
| Code | When |
|---|---|
protect-event |
an event added to or moved onto a protected person or their family |
protect-date |
the date of a protected person's event changed |
protect-place |
the place of a protected person's event changed |
protect-link |
a parent or child link to a protected person |
protect-attach |
a note, citation, media, attribute or association on a protected person |
protect-update |
an update_person or update_family on a protected person or their family, or a merge where one side is a protected person, their family or their event |
protect-text |
new text names a protected person near a life-event word |
protect-unknown |
a protected ID is not in the snapshot, so nothing protects that person |
Tags are not data about the person, so add_tag and remove_tag pass. Text that names a protected person without a full name or ID ("her daughter was born...") is not caught.
Nothing to check: no protected IDs.
living¶
Who may be living comes from possibly_living() in the shared rules: no death recorded and born less than years ago, an end of a DNA association, or a living bound from relatives' dates. A person the list adds counts with the events the list gives them. An event is judged when the list adds it, updates it, or moves it (move_event) onto someone who may be living; a moved event brings the date, place and type it has in the snapshot.
| Code | When |
|---|---|
living-date |
an event of someone who may be living gets a date with a day; a year, or a year and month, passes |
living-place |
an event of someone who may be living gets a place |
living-quote |
a quotation of four words or more («…», “…”, "…"; a shorter quoted span reads as a title or a name), a Transcript note or a transcript attached to someone who may be living, or an update_note putting one in a note they carry |
living-marriage |
a marriage event of someone who may be living |
living-id |
new text holds an identity number (DNI, NIE, NIR, codice fiscale, SSN with a valid check). The message shows its first three characters only. Leave it out; if it must stay, mark the note private |
Nothing to check: a snapshot with no people.
overlap¶
Two lists written at the same time against one tree should not change the same objects, and a list should not repeat an update that is already in the tree.
| Code | When |
|---|---|
overlap-open |
the op touches a person, family, event, note, citation or media object that a list in the open folder also touches. Places, sources and repositories are shared freely and do not count |
overlap-applied |
an update_*, set_attribute, add_tag or remove_tag op is the same as one in the applied folder, everything but key, ruling and tag equal |
The open folder may hold the list being linted; it is skipped. Every *.jsonl directly in the folder is read.
Nothing to check: neither --open nor --applied, or a folder that does not exist.
weak¶
An op that makes a weak claim must carry the weak tag:
- an
add_personwithallow_namesake; - an
add_citationwithconfidence0 or 1 (1.0andtruecount, asgwt-applystores them as 1), or of a source inmachine_sources; - an
update_citationthat lowersconfidenceto 0 or 1.
A tag counts as weak when its name, with case and accents folded, holds one of the weak tags, the same test gwt-report uses, so r7-weak or Hipótesis count for weak and hipotesis. The op's own tag counts on ops that make an object, an add_tag of the weak tag on the same object in the list counts, and so does the tag already on the object in the snapshot. A citation whose every target is a child_ref marked [probable] (by link_child with probable, an add_note starting with [probable] in the list, or a note already on the link) needs no tag: a child_ref cannot carry tags, and the mark is how a doubtful link is shown. A link_child with probable carries its own mark.
| Code | When |
|---|---|
weak-untagged |
a weak claim without the weak tag |
weak-inert-tag |
an op that makes no object has a tag; gwt-apply only tags what an op makes, so that tag lands nowhere. Use an add_tag op. A merge_* op makes the merge note gwt-apply writes on the winner, and its tag lands there |
Nothing to check: no weak tag.
count¶
| Code | When |
|---|---|
count-in-text |
new text has a number within eight words of the word "tree" in any of the locales (or a [count] words entry): «el árbol tiene 263 personas con este apellido». Such a count is wrong as soon as another list lands. The tree word must be a whole word (Baumann or Treeside is no tree, and neither is the plural trees), and the number must stand alone, so the digits of 12-03-1850 or of an ID are no count. A number right before a comma names a record, it counts nothing: «acta 5, imágenes ya en el árbol». A four-digit number from 1000 to 2199 is read as a year. The finding quotes the words that fired |
Proving the rules¶
gwt-lint --prove LIST --snapshot DUMP --config lint.toml runs each rule twice, under your own settings: on the list, and on the rule's own mutant of it (the list with one bad op added, such as a stranger for scope or a dated event on the first protected person). A rule passes only when it could look and its expected code fires more often on the mutant than on the list. So a rule that has nothing to check under your settings, fires anyway, fires another code, or cannot fire on its mutant fails, and the run exits 1:
proved scope: scope-out fired on its mutant (15 on the list, 16 on the mutant)
FAILED protect: nothing to check under these settings: no protected people: give --protect or [protect] ids
FAILED overlap: no mutant can be built from this tree and these settings
exit 1: a rule did not catch its own mutant
Nothing is filled in for you: overlap's mutant touches an object one of your open lists touches, so an empty or missing open folder fails --prove. Run --prove with the same settings as the lint run it vouches for, in the same step.
CI runs gwt-lint --prove on examples/changes.jsonl with the invented settings in lint/fixtures/example/lint.toml, and a test makes each rule blind in turn and checks that --prove then fails.
Adding a rule¶
A rule is a class in a module listed in RULE_MODULES in lint/gwt_lint/framework.py, with a name, a codes table (code: one line) and two methods:
from gwt_lint.framework import Mutant, NothingToCheck, Rule
class NoEmptyNotes(Rule):
name = "empty-note"
codes = {"empty-note": "a note with no text"}
def check(self, ctx):
# ctx.ops (each op has _line), ctx.tree (the snapshot), ctx.view (the tree with the list laid over it),
# ctx.section("empty-note") (its settings); raise NothingToCheck(reason) when it cannot look
return [self.finding("empty-note", op, "the note is empty") for op in ctx.ops
if op["op"] == "add_note" and not op.get("text", "").strip()]
def mutate(self, ctx):
bad = {"op": "add_note", "key": "lint-prove-empty", "text": " ", "attach": [], "_line": len(ctx.ops) + 1}
return Mutant(ctx.ops + [bad], "empty-note")
check returns findings, plus plain strings for notes -v prints. Its mutant must fire its code on any list, and a test should show the rule's clean list, a list with each code, and the case where it has nothing to check.