Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Three committed contributor docs carry the parser's normative rules and their re

**Counting claims.** A bare count in prose is either an assertion or a liability, keyed by who observes its staleness: asserted counts (a test holds the number) fail CI at change time — the useful kind; dated snapshots ("51 sites at spec time") cannot go stale; standing present-tense prose counts are the forbidden class — promote to an assertion, add a date, or state the invariant and let a test count. After changing how many times something runs, sweep for counts, not for the thing's name.

**Release-log claims.** Quantified or universal behavior claims in release bullets must come from the snapshot's diff since the last release, be verified against rules examples, or -- for a view the snapshot cannot see -- carry a recompute recipe stored with the design entry the bullet cites; never write one from memory. `uv run python tools/differential/snapshot.py --since v<last release>` prints every corpus name the release moved, grouped by changed field; v2.4.1 is the first tag carrying a snapshot, and for an older release `compare.py --baseline <version>` prints the same report against the PyPI wheel, closing with the entries compared and how many moved names are Latin-only. Both say how many moved names only `corpus_cldr.jsonl` holds. The snapshot records the seven role fields, ambiguity kinds, `initials()`, the two case-repair views (`capitalized()` and `capitalized(force=True)`, each only where it changes the text, #826) and, since 3.0, the suffix's `generation` and `credentials` views (where the suffix holds a generation, rules/R8), its `honorifics` view (where it is not empty, rules/R8, #326) and the `patronymic` and `surname2` views (each where it is not empty, rules/R9 and R10; the snapshot parses the default policy, so no name fills `surname2`, which only `two_family_names` reads) for every name, so an initials, case, generation or patronymic count is read off it directly -- with two limits. A case view holds the whole repaired name, so in a name repair changes, a role move that changes the rendering moves the view too, with or without a word recased: read a view move that comes with a role move by its words' case, not by whether it moved (#808's `John Smith, otr/l` moved both, `'John Smith OTR/L'` -> `'Otr/L John Smith'`). And `--since` a tag whose snapshot predates #826 (v2.5.0 and earlier) does not compare the views at all, so 2.6.0's case claims come from `compare.py --baseline 2.5.0`. `corpus_cldr.jsonl`'s fictional renderings are in it: count them out of a release figure (both tools print how many moved names only that file holds), or say the figure includes them. A render view the snapshot does not record -- `str()` under a custom format, `initials()` under a custom spec -- stays invisible to it, and for those the first two sources cannot reach a claim: the snapshot is byte-identical across the change, and an example line witnesses an output without counting anything. The DEFAULT `str()` is mostly invisible too, the snapshot seeing it only through a case view where repair changes the text: #754's render defaults moved `str()` for 1,034 corpus entries and the case views for 159, so a change to the default rendering is measured by rendering every entry in both trees (decisions/R6's 2026-10-29 entry holds the recipe). A recipe names the corpus files, the policy sweep, and -- the part that is easy to omit and fatal -- THE COMPARATOR, which must be something the shipped tree is not: #408's first recipe said to compare `initials()` against a folded-first partition, which is what `initials()` now IS, so it reproduced 0 where the bullet claimed 660 and was the only stated provenance for the number. Run the recipe as written before shipping the bullet. Cross-version numbers (a released wheel, the pre-change tree) are dated snapshots under Counting claims, since nothing in the repository re-runs them. The same standard binds decisions entries and PR bodies: a claim that a change restores the released reading is measured over the whole moved population against the wheel, never spot-checked (#625 claimed its 774 moves landed on the 1.4.0 and 2.3.0 readings from three names; measured, the tree matched 2.3.0 on 0 of 258 moved texts, master on 152), and a population size is re-counted from its recipe, never copied from another entry (the same entry carried #624's 137,810 for a grid of 395,148). Code comments citing rule IDs follow the excerpt discipline, checked by tests/v2/test_doc_citations.py: a rule's quotable text is its fields' prose (When, Tolerated, Then and its bullets, Because, and the Except, Precedence and Accepted bullets), and its fences and the footer below `---`, where the `Signal:` line stands, are not; `rules/H` names the H section, whose Background is quotable; a decisions citation may paraphrase, but what it quotes must be verbatim; and a reference with no quote must still name something that exists. The check's reach is a quote within four words of the ID or chained to one that is: a quote further off reads exactly like a name written in double quotes, so it is NOT checked, and the second review of #632 found 14 such quotes stale by hand. Keep a quote beside its ID. Quotes of AGENTS.md itself are not checked at all. **A citation comment holds three things: the quote, a line saying which clause this code implements, and `decisions/X (#N)` pointers to the entries that hold the history.** A code-local reason a reader of that line needs may stay beside them (why an index is safe to keep, why a test runs last), but issue history, measurements and restated examples belong in decisions/, where they are dated and kept current; in a comment they are a second copy that goes stale where nobody looks. #760 trimmed five blocks of 59 to 100 lines that had grown that way (`_post_rules._mark_suffix_entries`, group's P3 both-sides test, its S2/P2/P4 chain head and S4 chain emitter, and the honorific peel's scope in `_script_segment`), moving into dated decisions bullets what those entries lacked. Two of the moved claims no longer held: a policy example that had stopped reaching its case, and a credential said to land as a title that lands as a suffix.
**Release-log claims.** Quantified or universal behavior claims in release bullets must come from the snapshot's diff since the last release, be verified against rules examples, or -- for a view the snapshot cannot see -- carry a recompute recipe stored with the design entry the bullet cites; never write one from memory. `uv run python tools/differential/snapshot.py --since v<last release>` prints every corpus name the release moved, grouped by changed field; v2.4.1 is the first tag carrying a snapshot, and for an older release `compare.py --baseline <version>` prints the same report against the PyPI wheel, closing with the entries compared and how many moved names are Latin-only. Both say how many moved names only `corpus_cldr.jsonl` holds. The snapshot records the seven role fields, ambiguity kinds, `initials()`, the two case-repair views (`capitalized()` and `capitalized(force=True)`, each only where it changes the text, #826) and, since 3.0, the suffix's `generation` and `credentials` views (where the suffix holds a generation, rules/R8), its `honorifics` view (where it is not empty, rules/R8, #326) and the `patronymic` and `surname2` views (each where it is not empty, rules/R9 and R10; the snapshot parses the default policy, so no name fills `surname2`, which `two_family_names` reads and, under `locales.ZH`, a Han surname pair, rules/W5) for every name, so an initials, case, generation or patronymic count is read off it directly -- with two limits. A case view holds the whole repaired name, so in a name repair changes, a role move that changes the rendering moves the view too, with or without a word recased: read a view move that comes with a role move by its words' case, not by whether it moved (#808's `John Smith, otr/l` moved both, `'John Smith OTR/L'` -> `'Otr/L John Smith'`). And `--since` a tag whose snapshot predates #826 (v2.5.0 and earlier) does not compare the views at all, so 2.6.0's case claims come from `compare.py --baseline 2.5.0`. `corpus_cldr.jsonl`'s fictional renderings are in it: count them out of a release figure (both tools print how many moved names only that file holds), or say the figure includes them. A render view the snapshot does not record -- `str()` under a custom format, `initials()` under a custom spec -- stays invisible to it, and for those the first two sources cannot reach a claim: the snapshot is byte-identical across the change, and an example line witnesses an output without counting anything. The DEFAULT `str()` is mostly invisible too, the snapshot seeing it only through a case view where repair changes the text: #754's render defaults moved `str()` for 1,034 corpus entries and the case views for 159, so a change to the default rendering is measured by rendering every entry in both trees (decisions/R6's 2026-10-29 entry holds the recipe). A recipe names the corpus files, the policy sweep, and -- the part that is easy to omit and fatal -- THE COMPARATOR, which must be something the shipped tree is not: #408's first recipe said to compare `initials()` against a folded-first partition, which is what `initials()` now IS, so it reproduced 0 where the bullet claimed 660 and was the only stated provenance for the number. Run the recipe as written before shipping the bullet. Cross-version numbers (a released wheel, the pre-change tree) are dated snapshots under Counting claims, since nothing in the repository re-runs them. The same standard binds decisions entries and PR bodies: a claim that a change restores the released reading is measured over the whole moved population against the wheel, never spot-checked (#625 claimed its 774 moves landed on the 1.4.0 and 2.3.0 readings from three names; measured, the tree matched 2.3.0 on 0 of 258 moved texts, master on 152), and a population size is re-counted from its recipe, never copied from another entry (the same entry carried #624's 137,810 for a grid of 395,148). Code comments citing rule IDs follow the excerpt discipline, checked by tests/v2/test_doc_citations.py: a rule's quotable text is its fields' prose (When, Tolerated, Then and its bullets, Because, and the Except, Precedence and Accepted bullets), and its fences and the footer below `---`, where the `Signal:` line stands, are not; `rules/H` names the H section, whose Background is quotable; a decisions citation may paraphrase, but what it quotes must be verbatim; and a reference with no quote must still name something that exists. The check's reach is a quote within four words of the ID or chained to one that is: a quote further off reads exactly like a name written in double quotes, so it is NOT checked, and the second review of #632 found 14 such quotes stale by hand. Keep a quote beside its ID. Quotes of AGENTS.md itself are not checked at all. **A citation comment holds three things: the quote, a line saying which clause this code implements, and `decisions/X (#N)` pointers to the entries that hold the history.** A code-local reason a reader of that line needs may stay beside them (why an index is safe to keep, why a test runs last), but issue history, measurements and restated examples belong in decisions/, where they are dated and kept current; in a comment they are a second copy that goes stale where nobody looks. #760 trimmed five blocks of 59 to 100 lines that had grown that way (`_post_rules._mark_suffix_entries`, group's P3 both-sides test, its S2/P2/P4 chain head and S4 chain emitter, and the honorific peel's scope in `_script_segment`), moving into dated decisions bullets what those entries lacked. Two of the moved claims no longer held: a policy example that had stopped reaching its case, and a credential said to land as a title that lands as a suffix.

**Writing the user docs (docs/*.rst) has its own AGENTS.md too.** `docs/AGENTS.md` carries the style the docs are written in — tables as indexes, subheadings per task, measured claims, recipe doctests — distilled from PR #588. It loads the same way as the docs/design/ one below; if your tool does not do nested discovery, read it before editing a `.rst` file under docs/.

Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/R10-second-surname-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Standing

- `ParsedName.surname2` holds the family's second name unit where rules/O6 read the family as two, and is empty otherwise; it is a view, not a role, so `family`, `surnames`, `as_dict()`, `replace()`'s fields and the repr are unchanged, item 5 of decisions/vocabulary-model's 2026-10-28 #754 output-model entry — 2026-10-29 #754, sub-phase 4d.
- `ParsedName.surname2` holds the family's second name unit where rules/O6 read the family as two (and, since 2026-11-06 #845, where rules/W5 reads a Han surname pair, decisions/W5), and is empty otherwise; it is a view, not a role, so `family`, `surnames`, `as_dict()`, `replace()`'s fields and the repr are unchanged, item 5 of decisions/vocabulary-model's 2026-10-28 #754 output-model entry — 2026-10-29 #754, sub-phase 4d.
- O6's mark is a private tag that assemble drops, carried out on `Reading.second_surname`, a field `Token.reading`'s readings gained and that is not compared, where the RFC had a stable tag: a published tag would make an O6 parse unequal to its own 2.x pickle — 2026-10-29 #754, sub-phase 4d.

Elsewhere: why the second surname is a view of `family` rather than a role, the ORCID measurements behind it, and the `surnames` redefinition declined, decisions/vocabulary-model's 2026-10-28 #754 entry, item 5; how O6 counts a family's words and which names it declines, decisions/O6; the first unit's views, `surname1` and its particles and base, decisions/R11 (#1022).
Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/R11-first-surname-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Standing

- `ParsedName.surname1`, `surname1_particles` and `surname1_base` hold the family's first name unit, its particles and its base where rules/O6 read the family as two, and the family's own views where it read one; `family_particles` and `family_base` keep reading the whole family (R2 unchanged) — 2026-11-03 #1022 (Derek), THE FIRST SURNAME GETS VIEWS OF ITS OWN.
- `ParsedName.surname1`, `surname1_particles` and `surname1_base` hold the family's first name unit, its particles and its base where rules/O6 read the family as two (and, since 2026-11-06 #845, where rules/W5 reads a Han surname pair, decisions/W5), and the family's own views where it read one; `family_particles` and `family_base` keep reading the whole family (R2 unchanged) — 2026-11-03 #1022 (Derek), THE FIRST SURNAME GETS VIEWS OF ITS OWN.
- The first unit is the unit O6 read, so a middle word O3 folds into the family afterwards is in neither unit, and a particle P6 attached is in the first; where O6 read one unit, a fold's middles are left out all the same, the first surname being the family as it stood before the fold — 2026-11-03 #1022 (Derek), THE FIRST SURNAME GETS VIEWS OF ITS OWN; 2026-11-03 #1022 (Derek), FOLLOW-UP TO QUESTION 2.
- `initials()` keeps reading the whole family's base — 2026-11-03 #1022 (Derek), THE FIRST SURNAME GETS VIEWS OF ITS OWN.
- The views are `render()` keys and assertable fields of a rule's examples, and stay out of `as_dict()`, `--json`, `comparison_key()`, the repr and the snapshot — 2026-11-03 #1022 (Derek), THE FIRST SURNAME GETS VIEWS OF ITS OWN.
Expand Down
1 change: 1 addition & 0 deletions docs/design/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Entry conventions:
- [W2 — the glued-honorific peel](W2-glued-honorific-peel.md)
- [W3 — the writer's divisions are respected](W3-writers-divisions-are-respected.md)
- [W4 — script-scoped order](W4-script-scoped-order.md)
- [W5 — a Han surname pair](W5-han-surname-pair.md)

## Tokens, initials & punctuation (T)

Expand Down
Loading
Loading