Skip to content

docs(design): the three keystone decisions from the post-#381 backlog review - #386

Merged
derek73 merged 9 commits into
masterfrom
docs/keystone-decisions
Aug 17, 2026
Merged

docs(design): the three keystone decisions from the post-#381 backlog review#386
derek73 merged 9 commits into
masterfrom
docs/keystone-decisions

Conversation

@derek73

@derek73 derek73 commented Aug 16, 2026

Copy link
Copy Markdown
Owner

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_excerpts enforces this).

Why

After #381 landed the docs/design/ system, the backlog was 30 open
issues 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, new rules.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 the
declared 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_words is 11 generational + 5 neither + 24
postnominal 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's
ambiguous 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 first
three commits were wrong and are corrected in bd80f6f:

  • Should de Mesnil Juan be all surname, or family de Mesnil plus given Juan? #364 moves 1 corpus name (de Mesnil Garcia). The original
    entry 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.
  • P6 moves 1 corpus name (Vega, Juan de la), not the three
    originally listed: Smith van der and Sander van have 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.
  • Should suffix split into generation and credentials? (Smith, Jr., PhD) #326's vocabulary table is stale, confirmed: it predates the
    2.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}; esq the sole SUFFIX_ACRONYMS ∩ SUFFIX_WORDS
member; and the behavioral claims behind the #368 reversal
(van Mesnil Juangiven='van', Chergiven, de la Vega
family).

New deviations

The deviation backlog goes from 2 marker lines to 9: #364 ×2 on P1,
#379 ×2 and #380 on the new P6, #385 on R2 and on R3. Each states intended behavior and
pins 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 ORDER ambiguity 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 (both
traps hit during the fact-check itself), the triage pattern to
AGENTS.md; the per-issue table and the sequencing plan are
navigational 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.

pass found
Spec self-review 1 — A3/A5 self-contradiction
Fact-check re-run 2 — #364 filter wording, P6's out-of-scope movers
Coherence, rule↔rule 2 — P6/S2 unpinned, R2→R3 unmarked
Coherence, rule↔decision-record 2 — P1 "particle" vs "particle run", P6's clause reaching 3 words
/code-review high on the .py files 2 — stale-pointer hole, citation-block quotes

Two mechanical checks were tried and discarded on measurement:
interacts: symmetry is not a coherence property (9 of 15 declared
interactions are asymmetric, 6 predating this branch), and "exactly one
quoted span per citation block" does not hold today (_group.py's M1
block legitimately carries six).

New grammar: tracked:

P6 was the first rule in rules.md that nothing implements. Rather
than allow a rule pointing at nothing as a silent exception, the
pointer grammar grows tracked: #N as implemented:'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 (both
traps hit during the fact-check itself), the triage pattern to
AGENTS.md; the per-issue table and the sequencing plan are
navigational 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:

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.md
with 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

derek73 and others added 3 commits August 16, 2026 15:58
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>
@derek73 derek73 added the docs Documentation fixes and updates label Aug 16, 2026
@derek73 derek73 self-assigned this Aug 16, 2026
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (909c5f3) to head (6787631).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

derek73 and others added 3 commits August 16, 2026 18:15
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>
@derek73 derek73 added the tests label Aug 17, 2026
derek73 and others added 3 commits August 16, 2026 19:09
…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
derek73 merged commit 2c399c9 into master Aug 17, 2026
11 checks passed
@derek73
derek73 deleted the docs/keystone-decisions branch August 17, 2026 02:28
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation fixes and updates tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should de Mesnil Juan be all surname, or family de Mesnil plus given Juan?

1 participant