The leading-particle claim takes its group, not the rest of the name (#390, #365) - #391
Closed
derek73 wants to merge 1 commit into
Closed
The leading-particle claim takes its group, not the rest of the name (#390, #365)#391derek73 wants to merge 1 commit into
derek73 wants to merge 1 commit into
Conversation
…390) rules.md#P1 says a leading never-given particle makes the particle run and the ONE name word it attaches to the family, with anything beyond that read by position. The parser swept every remaining name word in, so "de Mesnil Juan" gave family="de Mesnil Juan" where the rule says family="de Mesnil" plus given="Juan". Closes #390 and #365. The claim moves from post_rules into assign, and that relocation is the fix rather than a side effect. decisions.md#P1 diagnosed the inversion in #386: assign handed out positions from _effective_order and post_rules then inspected a fixed list of ROLES, so the fold's reach depended on which role the declared order happened to give the particle. Claiming before positions exist is what makes the reading order-independent by construction instead of by three separate site checks -- which is also why #365 needs no third fold site. The leftover reuses _name_positions(order, count + 1) with FAMILY dropped. Asking for one more slot than there are pieces and discarding the family keeps one definition of "by position"; a plain count would hand the leftover the family a second time under FAMILY_FIRST. P1 is now split across two stages, deliberately. The opening half belongs where the claim happens; the second site -- a lone particle positioning has already dropped into GIVEN ("Mesnil de" under FAMILY_FIRST) -- can only be seen afterwards, so it stays in post_rules. Both docstrings say so, and rules.md's implemented: names both modules. _leading_name_piece retires with the site it served: assign reaches the same piece by peeling titles before it counts. Ledger, and the reason it is three entries rather than none: the new diff was silently absorbed by fix(suffix-routing), the fields-only catch-all that sorts last and "takes whatever nothing narrower named". Its own comment says its prose fits all four of its names, and "Garcia" is not a suffix -- so leaving it there would have made that claim false for a fifth. It gets its own rule in all three ledgers, on the precedent #372 set for the 21 names that fell there before, and suffix-routing is back to four. The recorded-roster guard then caught the claim counts and they are recorded. Verification: differential exit 0 at 1.4.0, 2.0.0 and 2.1.0; 3459 tests, ruff and mypy clean. One corpus name moves, "de Mesnil Garcia", the v1 parity break #364 weighed and accepted. tests/test_particles.py records it; the interaction that test guards (middle_name_as_last has nothing left to do) is unchanged, since middle is empty either way. 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 #391 +/- ##
=======================================
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:
|
Owner
Author
|
Closing unmerged. Three routes still violate Full findings with measurements recorded on #390. The next attempt needs #360 to land first and the claim to live in grouping rather than assign. |
derek73
added a commit
that referenced
this pull request
Aug 18, 2026
Second attempt. PR #391 put the claim in assign and could not reach three routes; this one puts it in GROUPING, where the pieces are built. de la Vega Juan family='de la Vega Juan' -> family='de la Vega' given='Juan' de la Cruz Maria family='de la Cruz Maria' -> family='de la Cruz' given='Maria' de Mesnil Juan family='de Mesnil Juan' -> family='de Mesnil' given='Juan' Smith, de Mesnil family='Smith de Mesnil' -> family='Smith' given='de Mesnil' What the first attempt could not do from assign: split a chained piece ("de la Vega Juan" groups as [de][la Vega Juan], so taking two pieces takes the whole name), reach the FAMILY_COMMA branch, or survive the particle-vocabulary gaps that made "de los Santos" regress to given='Santos'. #360 fixed the third by landing first, and it holds: de los Santos still reads family='de los Santos'. P4 is AMENDED rather than worked around. A never-given particle leading the name now chains, through its run onto one word; an ambiguous one still chains nothing, so "Van Johnson" survives. A run INSIDE the name still joins greedily -- P2 is untouched -- and the asymmetry is the two positions meaning different things: "pennie von bergen wessels" is a US politician whose surname is all three words, while "de la Vega Juan" is a surname plus a given name. Identical shape, and only position tells them apart. The comma path changed deliberately. "Smith, de Mesnil" was family="Smith de Mesnil" in v1; nobody writes a comma to mean "all of this is the family name", so the comma stays the primary order signal and the post-comma run reads as given text. A v1 parity break, and the three tests that pinned the old reading now pin this one. The guard was wrong twice before it was right, both times by testing a NEGATIVE. "vocab:particle-ambiguous" not in tags passes vacuously for a conjunction-merged piece headed by "and", which swallowed "and van Buren"; and a both-vocabulary word led the chain until `not title` was added -- the trap decisions.md#P1 already records under Declined. The head token must POSITIVELY be a never-given particle. Ledger: the new diff was again absorbed by fix(suffix-routing), the fields-only catch-all whose own comment says its prose fits all four of its names. Its own rule in all three ledgers, suffix-routing back to four, claims recorded, and a _CROSS_RULE_WINNERS row pins that 'de Mesnil Garcia' keeps the #390 rule -- without it the name falls to the catch-all with the gate still at 0 unexplained. Verification: differential exit 0 at 1.4.0, 2.0.0 and 2.1.0; 3470 tests, ruff and mypy clean. Both deviates: #364 markers removed from rules.md#P1, P4's statement and examples amended, P1's implemented: now names group/assign/post_rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 18, 2026
derek73
added a commit
that referenced
this pull request
Aug 18, 2026
A never-given particle opening the name took every remaining word into
the family in all three orders. Under a declared family-first order it
now takes one name word and leaves the rest to the order:
FAMILY_FIRST "de Mesnil Juan" -> family 'de Mesnil',
given 'Juan'
FAMILY_FIRST_GIVEN_LAST "de la Cruz Juan Carlos" -> middle 'Juan',
given 'Carlos'
Declaring a family-first order asserts that what follows the family is
not more surname, which is the question the stopping point asks. The
DEFAULT order is unchanged and that is the accepted cost: with nothing
declared, "de Mesnil Juan" has the shape of "pennie von bergen
wessels", whose whole text is the surname. Callers who mean otherwise
write the comma, which already parses that way.
Where it lives, and why not the two sites that failed before: the fold
in post_rules. Grouping was PR #394 and assignment PR #391 -- and the
piece is the obstacle, since "de la Cruz Juan Carlos" groups as [de]
[la Cruz Juan Carlos] once the ambiguous particle chains, so the stop
must cut INSIDE a piece. post_rules can: roles are per token, and
nothing downstream reads pieces (measured -- only _assign, which runs
before it). Grouping stays order-independent.
The order is read, not re-derived. assign records the order it used on
ParseState.order and the fold keys on that; policy.name_order would
disagree with the roles assign already wrote whenever a script_orders
entry overrides it.
The run counts UNITS: a conjunction join (P3) and a bound given-name
pair (P5) each count once, so "de la Vega y Santos Juan" cannot stop
between Vega and Santos, and "abdul Rahman" cannot be halved. Both are
read off the tags -- the prefix chain has already merged the joined
piece away by then.
Measured: one differential corpus name moves, "de Mesnil Garcia",
under each family-first order; all 751 are byte-identical in the
default order. The corpus cannot see more than that -- it runs under
the default policy against 1.4.0, which has no name_order -- so the
verification that counts is the two-leftover case rows, which
mutation-checking confirms are the only thing in the suite that fails
when name_order is discarded from the leftover placement.
Closes #395
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
added a commit
that referenced
this pull request
Aug 18, 2026
A never-given particle opening the name took every remaining word into
the family in all three orders. Under a declared family-first order it
now takes one name word and leaves the rest to the order:
FAMILY_FIRST "de Mesnil Juan" -> family 'de Mesnil',
given 'Juan'
FAMILY_FIRST_GIVEN_LAST "de la Cruz Juan Carlos" -> middle 'Juan',
given 'Carlos'
Declaring a family-first order asserts that what follows the family is
not more surname, which is the question the stopping point asks. The
DEFAULT order is unchanged and that is the accepted cost: with nothing
declared, "de Mesnil Juan" has the shape of "pennie von bergen
wessels", whose whole text is the surname. Callers who mean otherwise
write the comma, which already parses that way.
Where it lives, and why not the two sites that failed before: the fold
in post_rules. Grouping was PR #394 and assignment PR #391 -- and the
piece is the obstacle, since "de la Cruz Juan Carlos" groups as [de]
[la Cruz Juan Carlos] once the ambiguous particle chains, so the stop
must cut INSIDE a piece. post_rules can: roles are per token, and
nothing downstream reads pieces (measured -- only _assign, which runs
before it). Grouping stays order-independent.
The order is read, not re-derived. assign records the order it used on
ParseState.order and the fold keys on that; policy.name_order would
disagree with the roles assign already wrote whenever a script_orders
entry overrides it.
The run counts UNITS: a conjunction join (P3) and a bound given-name
pair (P5) each count once, so "de la Vega y Santos Juan" cannot stop
between Vega and Santos, and "abdul Rahman" cannot be halved. Both are
read off the tags -- the prefix chain has already merged the joined
piece away by then.
Measured: one differential corpus name moves, "de Mesnil Garcia",
under each family-first order; all 751 are byte-identical in the
default order. The corpus cannot see more than that -- it runs under
the default policy against 1.4.0, which has no name_order -- so the
verification that counts is the two-leftover case rows, which
mutation-checking confirms are the only thing in the suite that fails
when name_order is discarded from the leftover placement.
Closes #395
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
added a commit
that referenced
this pull request
Aug 18, 2026
A never-given particle opening the name took every remaining word into
the family in all three orders. Under a declared family-first order it
now takes one name word and leaves the rest to the order:
FAMILY_FIRST "de Mesnil Juan" -> family 'de Mesnil',
given 'Juan'
FAMILY_FIRST_GIVEN_LAST "de la Cruz Juan Carlos" -> middle 'Juan',
given 'Carlos'
Declaring a family-first order asserts that what follows the family is
not more surname, which is the question the stopping point asks. The
DEFAULT order is unchanged and that is the accepted cost: with nothing
declared, "de Mesnil Juan" has the shape of "pennie von bergen
wessels", whose whole text is the surname. Callers who mean otherwise
write the comma, which already parses that way.
Where it lives, and why not the two sites that failed before: the fold
in post_rules. Grouping was PR #394 and assignment PR #391 -- and the
piece is the obstacle, since "de la Cruz Juan Carlos" groups as [de]
[la Cruz Juan Carlos] once the ambiguous particle chains, so the stop
must cut INSIDE a piece. post_rules can: roles are per token, and
nothing downstream reads pieces (measured -- only _assign, which runs
before it). Grouping stays order-independent.
The order is read, not re-derived. assign records the order it used on
ParseState.order and the fold keys on that; policy.name_order would
disagree with the roles assign already wrote whenever a script_orders
entry overrides it.
The run counts UNITS: a conjunction join (P3) and a bound given-name
pair (P5) each count once, so "de la Vega y Santos Juan" cannot stop
between Vega and Santos, and "abdul Rahman" cannot be halved. Both are
read off the tags -- the prefix chain has already merged the joined
piece away by then.
Measured: one differential corpus name moves, "de Mesnil Garcia",
under each family-first order; all 751 are byte-identical in the
default order. The corpus cannot see more than that -- it runs under
the default policy against 1.4.0, which has no name_order -- so the
verification that counts is the two-leftover case rows, which
mutation-checking confirms are the only thing in the suite that fails
when name_order is discarded from the leftover placement.
Closes #395
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.
Closes #390. Closes #365.
First of the three particles-bundle PRs.
The change
rules.md#P1says a leading never-given particle makes the particlerun and the one name word it attaches to the family, with anything
beyond read by position. The parser swept everything in:
All three orders, unchanged by
name_order— the particle is evidenceabout how the name is written, and
decisions.md#O4gives the declaredorder only what no vocabulary has claimed.
It relocated, and that's the fix
The claim moves from
post_rulesintoassign.decisions.md#P1diagnosed the inversion in #386:
assignhanded out positions andpost_rulesthen inspected a fixed list of roles, so the fold'sreach depended on which role the order happened to give the particle.
Claiming before positions exist makes the reading order-independent by
construction — which is also why #365 needs no third fold site.
The leftover reuses
_name_positions(order, count + 1)withFAMILYdropped. One more slot than there are pieces, discard the family: that
keeps a single definition of "by position", where a plain count would
hand the leftover the family a second time under
FAMILY_FIRST.P1 is now split across two stages, deliberately. The opening half
lives where the claim happens; the second site — a lone particle
positioning already dropped into
GIVEN(Mesnil deunderFAMILY_FIRST) — can only be seen afterwards and stays inpost_rules. Both docstrings say so andimplemented:names both._leading_name_pieceretires with the site it served;assignreachesthe same piece by peeling titles before it counts.
The ledger finding
The new diff was silently absorbed by
fix(suffix-routing)— thefields-only catch-all that sorts last and "takes whatever nothing
narrower named". Its own comment says its prose fits all four of its
names, and
Garciais not a suffix, so leaving it there would havemade that claim false for a fifth. This is the
mechanisms.mdfieldnote about checking the receiver after narrowing a rule, hit for real.
It gets its own rule in all three ledgers, on the precedent #372 set
for the 21 names that fell there before.
fix(suffix-routing)is backto four. The recorded-roster guard then caught the claim counts, and
they're recorded.
Verification
tools/differentialexit 0 at 1.4.0, 2.0.0 and 2.1.0deviates: #364markers removed fromrules.md#P1— the runnerasserts today's value, so the suite would fail if they stayed
de Mesnil Garcia— the v1 parity break Shouldde Mesnil Juanbe all surname, or familyde Mesnilplus givenJuan? #364weighed and accepted.
tests/test_particles.pyrecords it; theinteraction that test guards (
middle_name_as_lasthas nothing leftto do) is unchanged, since middle is empty either way
Note on a garbage probe
Dr. de MD Mesnilnow readsfamily='de MD',given='Mesnil'—MDis suffix vocabulary sitting mid-name, so it's the piece the particle
attaches to. Both readings of that input are garbage; it's pinned only
so the claim's reach is visible, and per review it is explicitly not
a shape to design around. Suffixes are not particles; a caller who
wants one to join can add it to a custom
Lexicon.🤖 Generated with Claude Code