docs(design): the three keystone decisions from the post-#381 backlog review - #386
Merged
Conversation
The ambiguous-subset mechanism shipped twice -- particles (39/67, PARTICLE_OR_GIVEN) and suffix_acronyms (4/613, SUFFIX_OR_NAME) -- without its criterion written down anywhere, and titles (711) has no ambiguous subset at all. Record it. C-i: a word earns its set's ambiguous marking iff it is also borne as an ordinary name somewhere, defaulting to ambiguous under uncertainty -- the evidence standard already in NON_GIVEN_NAME_PARTICLES' docstring, generalized from "which set" to "which subset". C-ii: where two sets claim a word and neither reading is a name (vd: never-given particle vs. the British Volunteer Decoration), precedence is a per-word frequency judgement; vd reads as the Dutch van der. Also record that the 58%-vs-0.65% gap between the two shipped subsets is base rate, not disagreement -- most particles double as names, most credential acronyms do not. It reads as an inconsistency, and a reviewer who harmonizes the two shares breaks one of them. Applies to #348 (the 711-entry census, blocked on a given-name corpus this repo lacks), #360, #342, and #385 -- which the criterion resolves outright: "Do" is a borne surname, so it anchors family_base, while "van der" is never anyone's name and genuinely has no base. R2 gains the deviates: marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grouping is vocabulary's job and is order-independent; assignment is name_order's job. #364, #365 and #368 are one question seen from three angles: P1's fold sites and P2's chain are expressed over ROLES, and roles come from _effective_order, so both rules' coverage varied with the declared order by accident rather than by design. P1 now states that the fold takes the particle and the ONE name word it attaches to. "Takes everything" was never argued for -- it was the shape of v1's handle_non_first_name_prefix. Measured against the three differential corpora (782 names), filtered to the shape that actually changes, this moves exactly one name, "de Mesnil Garcia"; #364's warning that "each ledger would need re-examining" was written without the filter. #368 REVERSES: shipped behavior is correct. A mid-name chain has a head word and is positioned like any other group, so "Juan de la Vega" under FAMILY_FIRST is family="Juan". What NON_GIVEN_NAME_PARTICLES guarantees is that the bare word never reads as a given name, not that no name part may begin with one. The asymmetry with the leading case is P4's: a leading particle chains nothing, so without the fold pure position makes the bare particle the given name -- measurable today on the ambiguous half, where "van Mesnil Juan" gives given="van". The deviates: marker comes out and the issue closes won't-fix. New rule P6 records the trailing orphan: a particle ending the name attaches to the family beside it and renders before it, scoped to the comma form. The words-to-spare guard is load-bearing -- #379's own subject "van" is in the ambiguous half, so a never-given-only rule would not fix the issue it was filed for, while an unguarded rule breaks Vietnamese "Nguyen, Van". Rules P1 and P6 carry deviates: markers for #364, #379 and #380; P6 has no implemented: pointer because nothing implements it yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#326 asks whether suffix should split into generation and credentials, arguing the split is tractable because "the vocabulary is already split". That was true when the issue was written (2026-08-02). The 2.1.0 East Asian work landed five days later and put 24 postnominal honorifics -- 20 CJK, 4 Hebrew -- in suffix_words, which now reads 11 generational, 5 neither, 24 honorific. The largest group is neither of CLDR's two buckets, so "adopt CLDR's model" is not available as the cheap answer. Decision: do not split. Record the composition so #296, #291, #325 and #289 stop waiting on a distinction the model cannot express, and note the question this leaves unexamined -- whether the honorifics belong in `suffix` at all, given rules.md#W3 already calls an honorific "no part of the name on either side". Recorded as a failure mode rather than a fact: the field widened three times without any step being recorded as a widening of its MEANING. rules.md#S2's Background still calls it "two different things", accurately as written. The rules doc pinned the behavior; what slipped is the field's definition, which no rule owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #386 +/- ##
=======================================
Coverage 98.51% 98.51%
=======================================
Files 44 44
Lines 2895 2895
=======================================
Hits 2852 2852
Misses 43 43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Two measurement claims in this branch were wrong, both found by re-running rather than re-reading. decisions.md#P1 said the #364 shape filter "gives exactly ONE name". It gives three; one moves. Restated from a prefilter-free scan of all 782 corpus names -- exactly one family holds words beyond its particle's group -- with the two non-movers named and the reason each stays put. The substantive claim (one name, one ledger entry) survives; the stated method did not support it. decisions.md#P6 listed three default-order movers, two of which its own comma scope excludes: "Smith van der" and "Sander van" have no comma. They were measured before the comma scoping was chosen and carried forward unfiltered. The real count under the committed scope is one. The entry now also says why that number is not reassuring -- 245 comma names in the corpus and exactly one with a trailing particle, so the Dutch listing this rule exists for is essentially unsampled, and the implementing PR should run the differential regardless. Two field notes to mechanisms.md, both traps hit during the fact-check itself: a corpus can be near-blind to a writing CONVENTION even where the vocabulary is well covered (report the population that could move, not just how many did); and a detector that re-implements a rule's grouping gets it wrong unless it reads the same vocabulary the rule does -- walking the never-given run alone split "de la Vega" after "de la" and reported 50 false movers. AGENTS.md gains the shape-before-value triage note: the spec residue with no committed home, per the Landing-a-design checklist. The remaining homeless spec sections -- the per-issue "what this buys" table and the suggested sequencing -- are navigational and a plan respectively, and die with the branch as intended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review question: are the numbers in these entries useful later? Audited what this branch introduced. rules.md is clean -- no bare numbers, only rule IDs, which is right for a normative doc. The perishable counts are all in decisions.md, and they split three ways. Counts that carry an argument stay: "245 of 782 comma names, exactly one with a trailing particle" IS the corpus-blindness finding, and the two one-name movers are the findings themselves. One count carried no argument and is deleted rather than dated: "over all 782 names of the three differential corpora" becomes "over every name in the three differential corpora". The phrase already said whole-corpus-no-prefilter; the integer only offered a way to go stale. The two vocabulary compositions get a recompute one-liner. These are the #326 hazard reproduced one level up -- the suffix entry's own thesis is that #326 quoted a composition and rotted in five days, and the mitigation I gave it was a date, which is the same mitigation #326 had. decisions.md's convention already makes dated entries frozen evidence; what was missing is a way for a reader who wants today's answer to get it without trusting the snapshot. A test asserting the counts is explicitly declined in the entry: that is the constant-content pattern and would fail on every legitimate vocabulary addition. AGENTS.md gains the general rule, since this will recur wherever an entry quotes something that drifts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from a targeted pass over the contested pairs this branch creates. Prose reading found neither; naming the pairs and interrogating them one at a time found both. P6 and S2 contest "Berg, Jan vd" -- today S2 wins and reports suffix="vd", while P6's marker asserts family="vd Berg". Nothing said who wins. P6 does, and its statement now says so, which is how rules.md pins a contest (H2, M1 and W3 all carry precedence clauses in their statements; _CROSS_RULE_WINNERS is the ledger's instrument, not this document's). The precedence is scoped so it cannot creep: both particle and suffix vocabulary, trailing-orphan position, family comma, given word to spare. "John Smith, PhD" and "Smith, Jr." are not particles; "Jong, vd" has no given word left. R2's #385 resolution moves R3's output and R3 was silent about it. Initials read the BASE family word, so anchoring "Do" takes parse("Anh Do").initials() from "A." to "A. D."; R3 now carries its own deviates: marker, plus the "Juan van der" row where no borne name means no base and initials stay "J.". The general lesson is recorded with it: a marker lands on the rule whose STATEMENT changed, but a rule can move another rule's OUTPUT without touching its statement, and the runner cannot see it -- an unmarked downstream rule stays green because its own examples avoid the input. Walk `interacts:` when adding a marker. AGENTS.md's primary-source rule gains its missing scope. It was written for CROSS-SESSION distillation, where the source transcript is an artifact independent of the reviewing session -- #381 harvested nine such sessions. Same-session design work has no such artifact, and the instruction degenerates into re-reading the memory that produced the error. The amendment names both shapes and records the measured yield ordering from this branch: re-derived measurements 2, spec-vs-doc fidelity 1, named contested pairs 2, unaided prose reading 0. Not decided here: whether a rule with no implemented: pointer (P6 is the first) is a shape rules.md wants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nter Two review findings. P6's rationale claimed "no particle is a name by itself". That is true only of the never-given half, and decisions.md#vocabulary-collisions -- committed three commits earlier on this branch -- says the opposite in as many words: "most particles are short words that double as names (van, bin, le, do, bar, mac)". The error was substantive, not cosmetic: the words-to-spare guard exists BECAUSE the rule reaches ambiguous particles, #379's own subject being "van", so the rationale undercut its own guard. Rewritten so the guard follows from the reasoning: a never-given particle in trailing position cannot be a name and must join the family; an ambiguous one could be the name, which is what the guard is for. Noted with it: the coherence pass that ran two commits ago interrogated rule-vs-rule pairs and never checked rule-vs-decision-record, which is how this survived. P6 was also the first rule pointing at nothing -- no implemented:, because nothing implements it. Rather than allow that as a silent exception, the grammar grows `tracked: #N` as its counterpart, and every rule must carry exactly one of the two. An unimplemented rule can no longer sit in the doc untracked, and a shipped rule cannot keep a stale tracking pointer once its issues close. Documented in rules.md's preamble, where rules_doc.py says the grammar is described for humans. The new guard was mutation-tested before being believed, per the mechanisms.md field note: dropping the pointer, carrying both pointers, and malformed issue refs each fail it, and the restore was verified by diff rather than trusted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the axis named in the previous commit, over the rules this branch touched and the decision entries they rest on. P1's statement said the fold takes "the particle and the ONE name word it attaches to" -- contradicted by its own example, since "de la Vega" is two particles onto one word. Now "the particle run", with a clause saying the run is every particle in sequence, never-given and ambiguous alike. decisions.md#P1's #364 entry carried the same wording and is corrected with it, as is the P1 excerpt quoted in _post_rules.py. P6's precedence over S2 is stated for a SHAPE ("where the word is both a particle and suffix vocabulary"), but decisions.md#vocabulary-collisions C-ii says precedence between two non-name readings is a per-word frequency judgement. Measured, three words are both: vd, do and mc. Only vd was weighed, so do and mc inherit its answer unexamined. Recorded on both sides rather than papered over -- stating a per-word judgement as a general clause is how an unexamined word acquires a decision, and naming the two lets the next reader see which was actually argued. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The exactly-one-pointer guard added two commits ago claimed more than it enforced. Its docstring said a shipped rule "cannot keep a stale tracking pointer once its issues close", but the test reads the doc only and cannot see code. Meanwhile test_implemented_matches_citing_modules skipped any rule without implemented:, so a rule that gained code and a citation while keeping tracked: passed both. That is exactly the state the docstring called unrepresentable. The citations test now handles the tracked: branch, and the docstring states its own scope instead of the pair's: neither test alone makes a stale pointer unrepresentable, the pair does. Mutation-tested on the finding's own scenario -- adding a P6 citation to _post_rules.py while leaving tracked: in place now fails with the swap instruction, and the restore was verified by diff. The DEVIATION note on P1 wrote its example values in double quotes, which put four extra quoted spans inside the citation block that test_citations_are_verbatim_excerpts parses -- it passed only because the check takes the FIRST span. Moving the note above the citation line, a natural edit, would have made the excerpt resolve to "de Mesnil Juan" and failed against prose that is entirely correct. Values are unquoted now, and the note says why so the next editor does not helpfully re-add them. Considered and rejected: asserting exactly one quoted span per citation block, which would be the unrepresentable-states fix. Measured first -- 38 blocks carry one, but _group.py's M1 block legitimately carries six, so the invariant does not hold today and imposing it would mean rewriting an unrelated comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
added a commit
that referenced
this pull request
Aug 17, 2026
Two changes, one of them the checklist catching itself. The agent and /docs-review references come out. .claude/ stays personal per .gitignore's tools block, so a committed pointer to an untracked file is a dangling reference for everyone else; the axes are the shared record and stand alone. Running axis 1 (recompute every number) over this PR falsified the corpus-blindness count, which was wrong in two files merged by #386 as well as here. Of 782 corpus names 245 carry a comma, TWO of those end in a particle, and ONE clears P6's words-to-spare guard and actually moves -- "Nguyen, Van" is the second, held back by the very guard the rule needed. "Exactly one ends in a particle" conflated the population with the movers, which is the distinction that field note exists to draw. Corrected in mechanisms.md, decisions.md#P6 and AGENTS.md, with the mechanisms note recording that it made its own error inside the correction. The finding count is corrected with it: #386's passes surfaced ten issues, nine by these axes and one by the spec self-review. "Seven" was a mid-session tally that stopped being true two passes later and was never re-counted. Neither error changed an argument -- the corpus is near-blind at two names as at one, and the axes earn their place at nine as at seven. That is what the count-durability note asks for: phrase the argument so it survives the digits moving, then get the digits right anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
added a commit
that referenced
this pull request
Aug 17, 2026
The note telling you not to hard-wrap prose was hard-wrapped across two paragraphs and 19 lines, having been offered as one. Now one unwrapped line, which is both the convention it states and the length it promised. Dropped in the trim: the per-medium enumeration of what makes no difference (rendering, build, size, tokens), the blame-commit counts, the .git-blame-ignore-revs escape hatch, and the exception's field list. The measurements stay in this PR's description and #386's history; a convention needs the rule and the one number that justifies it, not the workings. Note the section it sits in was already wrapped while "2.0 API modules" below is not, so the file was mixed before either note existed -- which is why the §5 bullet in the previous commit is unwrapped and correct without anyone deciding.
derek73
added a commit
that referenced
this pull request
Aug 17, 2026
The note telling you not to hard-wrap prose was hard-wrapped across two paragraphs and 19 lines, having been offered as one. Now one unwrapped line, which is both the convention it states and the length it promised. Dropped in the trim: the per-medium enumeration of what makes no difference (rendering, build, size, tokens), the blame-commit counts, the .git-blame-ignore-revs escape hatch, and the exception's field list. The measurements stay in this PR's description and #386's history; a convention needs the rule and the one number that justifies it, not the workings. Note the section it sits in was already wrapped while "2.0 API modules" below is not, so the file was mixed before either note existed -- which is why the §5 bullet in the previous commit is unwrapped and correct without anyone deciding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Doc-only. No parser behavior changes; the one code edit is a comment
whose verbatim-excerpt citation of P1 had to track the rule's new
wording (
test_citations_are_verbatim_excerptsenforces this).Why
After #381 landed the
docs/design/system, the backlog was 30 openissues with ~13 open design questions, and no clear way to order them.
Relatedness does not partition it — nearly everything touches
particles or suffixes. What does partition it is shape before
value: does a decision change what the model can express, or fill in
a value in a shape already fixed? By that test there are three
keystones, and the rest are leaves. That triage note is now in
AGENTS.md.The three
A — order precedence (
decisions.md#P1, newrules.md#P6).Grouping is vocabulary's job and is order-independent; assignment is
name_order's job. #364, #365 and #368 turn out to be one question:P1's fold sites and P2's chain are expressed over roles, and roles
come from
_effective_order, so their coverage varied with thedeclared order by accident. The fold now takes only the particle's own
group, and #368 reverses — shipped behavior is correct.
B — the suffix field (
decisions.md#suffix-field-composition).No split.
suffix_wordsis 11 generational + 5 neither + 24postnominal honorifics, so #326's "the vocabulary is already split"
premise is stale and CLDR's two buckets do not cover the set.
C — vocabulary collisions
(
decisions.md#vocabulary-collisions). A word earns its set'sambiguous marking iff it is borne as an ordinary name somewhere;
default to ambiguous under uncertainty. Separate case C-ii for
set-vs-set collisions where neither reading is a name (
vd).Measurements
Re-run pre-merge at
1024578, clean tree. Two claims in the firstthree commits were wrong and are corrected in
bd80f6f:de Mesnil Juanbe all surname, or familyde Mesnilplus givenJuan? #364 moves 1 corpus name (de Mesnil Garcia). The originalentry said a shape filter "gives exactly one name" — it gives
three, and two do not move. Restated from a prefilter-free scan of
all 782 names.
Vega, Juan de la), not the threeoriginally listed:
Smith van derandSander vanhave no comma,and P6 is comma-scoped. But the corpus holds 245 comma names and
exactly one with a trailing particle, so it is near-blind to the
Dutch listing P6 governs — the implementing PR should run the
differential regardless of how small this looks.
suffixsplit intogenerationandcredentials? (Smith, Jr., PhD) #326's vocabulary table is stale, confirmed: it predates the2.1.0 East Asian work by five days.
Everything else verified clean: corpus size 782; suffix_words 40 =
11/5/24 (20 CJK + 4 Hebrew); particles 67/39, suffix_acronyms 613/4,
titles 711 with no ambiguous subset;
TITLES ∩ ambiguous == {do, freiherr, st};esqthe soleSUFFIX_ACRONYMS ∩ SUFFIX_WORDSmember; and the behavioral claims behind the #368 reversal
(
van Mesnil Juan→given='van',Cher→given,de la Vega→family).New deviations
The deviation backlog goes from 2 marker lines to 9:
#364×2 on P1,#379×2 and#380on the new P6,#385on R2 and on R3. Each states intended behavior andpins today's, so the implementing PR fails until it removes the
marker.
Review note: these six are the highest-consequence content here.
The runner asserts today's value strictly but cannot check the
intended value, because nothing produces it yet — so a wrong
intended value ships green and the next PR faithfully implements it.
Worth line-by-line sign-off; the rest is prose.
What this unblocks
Decided or resolved: #364, #365, #368, #379, #380, #385. Criterion
supplied: #360, #342, #348. Unblocked to proceed independently: #296,
#291, #325, #289, #326.
Still open and deliberately so: #384 (follows from A, but silent
stand-down vs. an
ORDERambiguity is unchosen), #348's census(blocked on a given-name corpus this repo lacks), and P6's comma-less
shapes.
Verification
uv run --frozen pytest— 3419 passed, 20 skipped, 11 xfailed.Landing-a-design distillation done: every substantive spec section
has a committed home. Two field notes went to
mechanisms.md(bothtraps hit during the fact-check itself), the triage pattern to
AGENTS.md; the per-issue table and the sequencing plan arenavigational and die with the branch as intended.
Review passes
Seven issues found, all in work from this branch. Each pass is a
distinct axis; unaided prose reading found none of them.
/code-review highon the .py filesTwo mechanical checks were tried and discarded on measurement:
interacts:symmetry is not a coherence property (9 of 15 declaredinteractions are asymmetric, 6 predating this branch), and "exactly one
quoted span per citation block" does not hold today (
_group.py's M1block legitimately carries six).
New grammar:
tracked:P6 was the first rule in
rules.mdthat nothing implements. Ratherthan allow a rule pointing at nothing as a silent exception, the
pointer grammar grows
tracked: #Nasimplemented:'s counterpart,with exactly one of the two required of every rule. Mutation-tested
four ways; the doc-side and code-side halves are enforced by different
tests and each docstring now states its own scope rather than the
pair's.
Verification
uv run --frozen pytest— 3419 passed, 20 skipped, 11 xfailed.Landing-a-design distillation done: every substantive spec section
has a committed home. Two field notes went to
mechanisms.md(bothtraps hit during the fact-check itself), the triage pattern to
AGENTS.md; the per-issue table and the sequencing plan arenavigational and die with the branch as intended.
Coherence pass
Done as targeted interrogation of named contested pairs rather than a
prose read — two findings, both fixed in
a1243d6:Berg, Jan vdand nothing said who wins.P6 does; its statement now carries the precedence clause, scoped so
it cannot creep (particle and suffix vocabulary, trailing-orphan
position, family comma, given word to spare).
family_base? (parse("Anh Do").initials() == 'A.'— the surname vanishes) #385 resolution moves R3's output, and R3 was silent.parse("Anh Do").initials()goes"A."→"A. D.". R3 now has itsown marker.
Two mechanical checks were tried and one was discarded:
interacts:symmetry is not a coherence property here — 9 of 15 declared
interactions are asymmetric and 6 predate this branch, so the field is
directed by usage.
Still undecided, deliberately: P6 is the first rule in
rules.mdwith no
implemented:pointer. Legitimate per the preamble("NORMATIVE, not descriptive"), but it sets a precedent and should be
a decision rather than my default.
🤖 Generated with Claude Code