Skip to content

fix(post_rules): the trailing particle is decided by its slot, not its spelling (#467) - #472

Merged
derek73 merged 3 commits into
masterfrom
fix/467-particle-reading
Aug 31, 2026
Merged

fix(post_rules): the trailing particle is decided by its slot, not its spelling (#467)#472
derek73 merged 3 commits into
masterfrom
fix/467-particle-reading

Conversation

@derek73

@derek73 derek73 commented Aug 30, 2026

Copy link
Copy Markdown
Owner

A trailing surname particle stops being stranded as a standalone middle name, and the given slot is given back to the declared name order.

Closes #467

The rule

A particle ending the name has nothing to link forward to, so what it is doing there is decided by what the writing says — never by the word. After a family comma it joins the family the comma named. A declared family-first order names the family the same way, so the attachment fires there too.

Jong, Anke de          GF    family 'de Jong'   given 'Anke'      (unchanged)
Jong Anke de           FF    family 'Jong'  middle 'de'   →  family 'de Jong'  given 'Anke'
Ménil Christophe de    FF    family 'Ménil' middle 'de'   →  family 'de Ménil'  given 'Christophe'
Jong Anke van de       FF    family 'Jong'  middle 'van de' →  family 'van de Jong'

The test is P6's own phrase: the run must END the name, and stand in a MIDDLE. A middle is a further given name, which a particle is not — so a particle that ends the name there is doing nothing, and belongs to the family.

Those two together name the order without asking it. The default order ends with the FAMILY and FAMILY_FIRST_GIVEN_LAST ends with the GIVEN name, so only FAMILY_FIRST can put a name's last word in a middle at all. An explicit order test was added during review and then removed: measured over 542,592 generated parses, it never decided anything the ends-the-name test had not already decided.

In the same change, P1 loses its given-position site — a correction rather than a narrowing, since that slot holds what the caller declared to be the given name:

Ménil de     FF / FFGL    family 'Ménil de'  →  family 'Ménil'  given 'de'

Why no vocabulary test appears

The comma path needs one because a comma cannot separate the Dutch reading from the Vietnamese. A declared order can — when the caller declares the right one for the name:

Beethoven Ludwig van   FF     family 'van Beethoven'      ← `van` IS ambiguous vocabulary
Nguyen Thi Van         FFGL   given 'Van'  middle 'Thi'   ← same word, same set

A never-given test here would have excluded every ambiguous particle — von, di, da, del, le, and van itself, the flagship word of the listing the rule is named for. #466 carried exactly that test and failed the Dutch case because of it.

It is one order, not both. Nguyen Thi Van read as FAMILY_FIRST gives family 'Van Nguyen' and loses the given name — the same trade the comma form already makes, recorded as accepted in rules.md#P6 and in tests/v2/cases.py.

The site also reports the fork it decides, matching its comma sibling. decisions.md#P6 settled that in #405, and the state that decision fixed is exactly what this site recreated: the comma writing reporting while the comma-less writing decided the identical fork in silence.

The premise this replaces was false

Three sites said a never-given particle "cannot be a name at all"rules.md#P6's rationale, _lexicon.py, config/particles.py — and the readings built on them were justified by it. Measured: all 33 members report given == themselves when parsed bare. NON_GIVEN_NAME_PARTICLES supplies the reading position leaves open and forbids nothing.

Second correction of the same over-claim; decisions.md#P6 already records dropping "no particle is a name by itself" for the identical reason. Commit 1 is that correction alone and moves no parse.

What review found, because the corpus could not

Four agents. Two defects, each found independently by two of them, and the differential corpus reported neither.

The site fired under the DEFAULT order. The first draft's gate was state.order is not None, which every no-comma order satisfies, while the reasoning above it argued from the FAMILY_FIRST layout. Where a conjunction stops a particle's forward chain, the particle stands in a middle under any order:

Maria Luisa y de la Cruz    plain HumanName()    family 'la Cruz'  →  'de la Cruz'

52 such names. Three documents in the first draft asserted the opposite.

It folded a particle that does not end the name. Under FAMILY_FIRST_GIVEN_LAST the trailing MIDDLE is not the trailing name word — the given name stands behind the middles — so de Anke van y gave family 'van de Anke', hoisting a particle in front of a base it was written after. 366 folds.

Of 1101 corpus names, six hold a conjunction-then-particle adjacency and none in the firing shape, so the reach recipe honestly reported 0 default-order movers throughout. The count was right; the population was blind. Both regressions are now · boundary examples in rules.md, so the doc runner holds them.

Also repaired: a stale families list (O1/O2 retag without recomputing — 33,840 stale observations; no shipped input reaches a wrong answer through it, so it is recorded rather than tested), a boundary example that asserted the one field its guard cannot move, two paragraphs still describing the removed given-position site as current — one in rendered API documentation — and P6's Accepted block still scoping the rule to the comma form.

The invariant, and how its first number was got wrong

For a name of the form Family Given [Particle], the family-comma writing under the default order and the comma-less writing under FAMILY_FIRST parse identically.

Measured over all 70 particles × 3 families × 3 givens: 0 of 630 before, 603 of 630 after.

The 27 failures are vd, mc and do — the three words that are also suffix vocabulary. P6's precedence over S2 is stated inside the comma paragraph and justified by the comma, so the comma form gives family 'vd Berg' where the comma-less form gives suffix 'vd'. A real limit, now stated in the rule.

An earlier draft of this PR claimed 216 of 216. That number came from a script that sliced sorted(particles)[:14], excluding exactly those three words — AGENTS.md axis 2 in its purest form: the detector agreed with the design because it inherited the design's blind spot. Two other numbers were wrong the same way and are corrected: 181 of 247 reproduces under no definition (real: 219 of 304), and "nothing moves under the default order" was a property of the corpus.

Reach

30 of 6606 parses, twelve names, none under the default order — this tree's 1101-name corpus × three name_order values × middle_as_family off and on.

The differential gate cannot see any of it: compare.py parses every name as HumanName(name) and sweeps no policy, so no non-default name_order behavior has ever been compared across versions. Gate output is unchanged at all three baselines apart from the corpus names this change's own example lines add. The counts carry a recompute recipe in decisions.md#P6, with the comparator named as a checkout of the parent commit reading the same corpus files.

One example line deleted rather than updated

"Nguyen Thi Van"  family-first  →  middle="Van"

The name is written in FAMILY_FIRST_GIVEN_LAST; its FAMILY_FIRST reading is wrong by construction, so the line pinned a value nobody wants. The reason is recorded in the rule. The general question — whether a rules.md example should declare its format — is #470 and is not decided here.

Split out deliberately

#471 is the leading fold's reach under the default order (de Mesnil Juan reads Juan as part of the surname). Prototyped in this branch and reverted: it takes the gate red at the 1.4.0 baseline with 5 unexplained diffs and fails a v1 parity test. That is a compatibility decision on the order every existing caller is on, where nothing in this PR has a v1 answer to break.

Verification

  • 6129 → 6164 passed; mypy, ruff and 232 sphinx doctests clean.
  • Gate green at 1.4.0 / 2.0.0 / 2.1.0, unexplained: 0, counts unchanged.
  • Six mutations verified, each killed by its own test — including both shipped defects the review found.
  • 813,888-parse sweep: 0 folds under the default order, 0 under FFGL, 0 non-trailing.

🤖 Generated with Claude Code

…ohibition

Three sites said a never-given particle "cannot be a name at all" or
"can never be a given name", and used that to justify the readings
built on it. It is false: NON_GIVEN_NAME_PARTICLES supplies the
reading position leaves open and forbids nothing. Measured, all 33
members report given == themselves when parsed bare.

The conclusions those sites draw are right; the reason given for them
was not, and it is load-bearing. A particle OPENING the name has the
rest of the name to join forward to -- that is evidence about the
writing rather than about the word, which is why no declared
name_order contradicts it. Stating it as a property of the vocabulary
invites the opposite inference for a particle ENDING the name, where
there is nothing to join forward to and the writing is all there is.

This is the premise #466 was argued from, and the second correction of
this same over-claim: decisions.md#P6 already records dropping "no
particle is a name by itself" for the same reason.

Behavior descriptions that the rest of #467 will change are left alone
here; this commit moves no parse.
…s spelling (#467)

A declared family-first order names the family as a comma does, so
P6's attachment fires without a comma too -- keyed on the SLOT the
trailing run landed in. MIDDLE is the one position that means nothing
for a particle, middles being further given names, and FAMILY_FIRST is
the only order that puts a trailing piece there.

    Jong, Anke de           family 'de Jong'   (unchanged)
    Jong Anke de     FF     family 'Jong'  middle 'de'  ->  family 'de Jong'

P1 loses its given-position site in the same change, and that half is
a correction rather than a narrowing: that slot holds what the caller
DECLARED to be the given name. `Menil de` now reports given 'de' under
both family-first orders.

No vocabulary test at the new site, and that is the design. A comma
cannot separate the Dutch reading from the Vietnamese; the declared
order can, and does both at once -- 'Beethoven Ludwig van' gives family
'van Beethoven' under FAMILY_FIRST while 'Nguyen Thi Van' keeps given
'Van' under FAMILY_FIRST_GIVEN_LAST, the same word in the same set. A
never-given test would have excluded 37 ambiguous particles including
von, di, da, del, le and `van` itself.

No re-layout either, and that is a property of the order: the roles run
family, given, middle, middle..., so dropping a trailing middle leaves
every other piece where it was. One guard remains, the base -- attaching
to an all-particle family reorders words R2 reads as ordinary names.

The invariant this rests on: `Family, Given particle` under the default
order and `Family Given particle` under FAMILY_FIRST parse the same.
0 of 216 constructed names agreed before, 216 of 216 after, including
all 72 with an ambiguous particle.

One example line was deleted rather than updated -- "Nguyen Thi Van"
under FAMILY_FIRST -- because the name is not written in that format
and its reading there pins nothing (#470 is the general question).

30 of 6594 parses move, twelve names, none under the default order.
Gate green at all three baselines and blind to all of it. The leading
fold's reach under the default order is #471, split out: that one is a
1.4.0 parity break where this is new-feature territory.
@derek73 derek73 added this to the v2.2 milestone Aug 30, 2026
@derek73 derek73 added the bug label Aug 30, 2026
@derek73 derek73 self-assigned this Aug 30, 2026
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.66%. Comparing base (caaed90) to head (5769e98).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #472   +/-   ##
=======================================
  Coverage   98.65%   98.66%           
=======================================
  Files          45       45           
  Lines        3194     3214   +20     
=======================================
+ Hits         3151     3171   +20     
  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.

The slot criterion was right and the code did not implement it. Two
defects, found independently by two reviewers each, and both invisible
to the corpus.

(i) THE SITE FIRED UNDER THE DEFAULT ORDER. The gate was
`state.order is not None`, which every no-comma order satisfies, while
the reasoning above it argued from the FAMILY_FIRST layout. Where a
conjunction stops a particle's forward chain the particle stands in a
middle under any order, so `Maria Luisa y de la Cruz` gave family
'de la Cruz' where master gives 'la Cruz' -- 52 such names, on plain
HumanName() with no policy. Three shipped documents asserted the
opposite in my words.

(ii) IT FOLDED A PARTICLE THAT DOES NOT END THE NAME. Under
FAMILY_FIRST_GIVEN_LAST the trailing MIDDLE is not the trailing name
word -- the given name stands behind the middles -- so `de Anke van y`
gave family 'van de Anke', hoisting a particle in front of a base it
was written after. 366 folds.

The repair is P6's own word: the run must END the name. That single
test subsumes the order test a first repair added -- measured over
542,592 generated parses, an explicit order test never decides
anything the ends-the-name test has not, so it is not in the code.
The default order ends with the FAMILY and FFGL with the GIVEN name,
so only FAMILY_FIRST can put a name's last word in a middle at all.

Also repaired: a stale `families` list (O1/O2 retag without
recomputing, 33,840 stale observations; no shipped input reaches a
wrong answer through it, so it is recorded rather than tested), and
the site now REPORTS the fork it decides -- decisions.md#P6 settled
that in #405, and the state that decision fixed is the one this site
recreated, the comma writing reporting while the comma-less writing
decided the identical fork in silence.

MY INVARIANT WAS MEASURED ON A RIGGED SAMPLE. It is 603 of 630 over
the whole particle vocabulary, not 216 of 216: the script sliced
`sorted(particles)[:14]` and excluded `vd`, `mc` and `do`, the three
words that are also suffix vocabulary and the only ones where the two
writings genuinely differ. AGENTS.md axis 2 exactly -- the detector
agreed with the design because it inherited the design's blind spot.
The rule now states that limit; the entry records the failure.

Docs corrected with it: two paragraphs still described the removed
given-position site as current, one in rendered API documentation;
P6's Accepted block still scoped the rule to the comma form; P6's
rationale predicted the opposite of its own example; "181 of 247"
reproduces under no definition (219 of 304); and a boundary example
asserted the one field its guard cannot move. Both regressions are now
`· boundary` examples so the doc runner holds them.

Six mutations verified. Reach unchanged at 30 of 6606, 12 names, none
under the default order -- the corpus never saw either defect.
@derek73
derek73 merged commit 07b3231 into master Aug 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ménil Christophe de under FAMILY_FIRST drops the tussenvoegsel, and de Mesnil Juan reads Juan as part of the surname

1 participant