Skip to content

Commit 647e6c7

Browse files
derek73claude
andcommitted
Fix #625's review: two-run tests, the pre-merge title run, the record
The review found two particle runs in one name reached by no test (an off-by-one in the merge's bookkeeping passed the suite), and the read's title run, taken before the merge, held only by the frame bands. Two parity rows and a unit-test half now hold the first, a unit test the second; the merge's docstring names a shape whose read the choice moves. decisions.md's #625 entry claimed the moved `St und` shapes read as the released wheels do; they do not (master matches 2.3.0 on 152 of the 258 moved texts, the tree on none), so the claim and the test docstring repeating it are corrected. The grid was 395,148 texts, not the 137,810 copied from #624's entry. The review's further move classes (any P3 join on a title-particle, a mid-name title behind a run, H3's give-back before a dual run) are recorded, all garbage, no rows (Derek). A stale "merges nothing" case note is updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b602e12 commit 647e6c7

5 files changed

Lines changed: 69 additions & 14 deletions

File tree

‎docs/design/decisions.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,9 +775,10 @@ for n in ('Smith, John','Smith, XYZ'): print(n, calls_for(off.parse, n), calls_f
775775
DECIDED (Derek): the particle stops at the credential, so it is a name word of its own and the rest is suffix: `John von PhD Jones` → family 'von', suffix 'PhD Jones'. The input is malformed (a particle is not written before a post-nominal), so the decision is whichever reading the shared count already gives the other forms, not a special case for this one: the existing clause gains its own limit, a lone particle counting when the run starts right behind it, in `run_start`'s one loop. The caller without the chain's units (`trailing_start`) shares the expression so the two cannot disagree, but no reading shows that half: it changes the position it returns for `Smith, John von PhD Jones` (2 where master gave 4), so group's chain takes its stopped path and asks `trailing_start` again, reaching the same reading at 16 more frames (339 → 355 on py3.11; a rare shape no frame guard sees, decided for the shared expression over a per-caller copy); a mutant ignoring the flag there passed the suite with the grid byte-identical (#627's reviews). The 1.4.0 and 2.3.0 wheels read middle 'von PhD', as they read `von und zu PhD`; the joined forms changed this cycle with #602.
776776
Declined: counting every unit, a lone particle included. It moved 2,836 S2-grid parses rather than 2,432, the extra ones all `de Mesnil <credentials>`, where it counted the surname as two words, and it split the two callers: a leading `van` counted before another particle (`van la Smith Jr. Jones` → suffix 'Jr. Jones') but not before a name word (`van Smith Jr. Jones`), and the case row `van la Smith Secretary Jr. Dr. Smith` caught it.
777777
MEASURED 2026-10-10 against 195c4081 (#630's merge), py3.11, `nameparser.__file__` asserted on each side. S2 stress grid (the #614 entry's recipe above, 324,552 parses): 2,432 role moves, no report-only move; every move is a lone particle -- in that grid `van`, or a dual `do`/`mc`/`vd` in any case -- with an unambiguous credential right behind it becoming the family (under the family-first orders the given name, the family staying first), the run taking the rest. The grid's comma shapes do not move; the review found the same move in shapes the grid lacks, as intended: other particles (`von`, `de`, `du`, `la`, `bin`), and a head before a suffix comma (`John von PhD Jones, Jr.` → family 'von', suffix 'PhD Jones, Jr.'). Fingerprint of every `tools/differential/corpus*.jsonl` name and `tests/v2/cases.py` text at 195c4081 under the three orders (1,925 texts, the seven fields and every report): 0 moves; this change adds one text, its own example, which moves. The five differential gates exit 0. Held by the case row `John von PhD Jones` (`fix(#627)`) against the row `van la Smith Secretary Jr. Dr. Smith`, whose lone particle has a name word behind it and still does not count, and the row `Freiherr von vd PhD Jones`, where the particle standing right before the credential is one the run took and the flag is not asked of it (a mutant asking it of every piece passed the suite until that row).
778-
- 2026-10-10 (Derek), #625 — THE TAIL'S PARTICLE RUNS ARE MERGED IN GROUP BEFORE S2'S READ, SO THE READ NEEDS NO BOUND MARK AND NO GATHERING. Since #614 split the trailing run off ahead of the chain, the read sees particles the chain has not merged yet, and #620 re-created the chain's particle-to-particle merge in the reader: `_chain_units` marked a particle standing straight behind another BOUND, the peel stopped at it, and `credential_run` gathered an absorbed run of them into one report. Now group merges each run of two or more adjacent particles past the chain's leading position (`_pieces.merge_particle_runs`, at the trailing site, after P3's joins and before the read) into the piece the chain would open its run with, so the read counts, stops at and absorbs the run as one piece, and the BOUND mark, the peel's stop for it and the gathering are gone. The merged piece keeps `prefix` and drops `title`: a P3 join carries the title tag in from a word in both vocabularies (`St und Berg`), and on the merged run it read the run as a leading title. The title run and the lead are read once, before the merge, and handed to the read, which had asked both again. The chain's PARTICLE_OR_GIVEN report keyed on its run claiming something past the particle (`j > k + 1`), which a merged run has done already though the chain then claims nothing more (`Freiherr von der`); it now keys on that or on the run being one the merge made (`premerged`) -- not on the piece holding several tokens, which a P3-joined phrase does too, and which moved 11,358 reports on the grid below (`Dr. St Do und`). The rule text is unchanged: rules.md#S2 already counts a particle run, the duals straight behind a particle included, as the one name word the chain makes. What grows is code, not concepts: the read loses a mark and two branches and re-creates nothing of the chain, group gains the merge (about 35 more lines than it removes, docstrings included).
779-
DECIDED (Derek): retiring the two reader-side mechanisms is worth the moves -- what moves is garbage, and it moves to the readings the released wheels give.
780-
MEASURED 2026-10-10 against 505d6dec (#632's merge), py3.11, `nameparser.__file__` asserted on each side. A particle-run grid -- `{head} {run} {tail}` for every head in (none), `Dr.`, `Freiherr`, `St`, `John`, `Dr. John`, `Freiherr von`, `John Smith`, `Sir`, `Freiherr St`; every run of one to three words over `van der de la von und mc Mc vd do Do St du`; every tail in (none), `Berg`, `MA`, `PhD`, `Ma`, `Jr.`, `Berg MA`, `Prof.`, `Berg Ph. D.`, `V`, `VI`, `X.Y.Z.`, `née Smith`, `Berg Jr. PhD`, `Berg van`, `, Jr.`, `, John` (a comma tail written against the run) -- 137,810 names under the three orders, comparing the seven fields and every report (kind, detail, tokens): 774 role moves, no report-only move. Every move holds `St und`, the joined piece carrying the title tag, which master merged into the particle run in front with the tag kept and read as a title (`Freiherr Do St und Berg MA` → title 'Freiherr Do St und Berg', family 'MA'); now title 'Freiherr', family 'Do St und Berg', suffix 'MA', the family the 1.4.0 and 2.3.0 wheels read too. The S2 stress grid (#614's recipe, above): no role move, and one text's report under its four policies -- `de Mesnil Ma do mc and Ph. D.` no longer reports 'do': P3 joins `mc and Ph. D.` into one suffix-tagged piece, the merge folds 'do' into it, and the peel takes the piece whole rather than picking 'do', the report adf6da88 did not make either and #620 added (its entry above). The #624 title-particle head grid (decisions.md#P2, 2026-10-10): 0. Every corpus name and case text, and #624's comma-tail grid, under the three orders: 0. The five differential gates exit 0, their reports identical line for line to the tree before. The 2026-10-08 prototype's 80 moves (`Dr. mc mc`) are not in the population: #624 already reads `Dr. Mc Mc` as H3 states. Recorded negative controls (2026-10-10): with the merge off, 27 tests fail, `test_an_absorbed_particle_run_reports_as_the_one_word_the_chain_makes` among them; with the report keyed on `j > k + 1` alone, the case rows `a_particle_takes_the_dual_behind_it_at_the_head` and `a_particle_run_before_the_run_stays_one_name_word`; with the merge starting at the segment's second piece rather than past the lead, 13; with `title` kept, only `test_a_merged_particle_run_is_a_particle_and_no_title`, the unit test added for it. Cost: +3 frames on the reference row, which holds `de la` (decisions.md#parse-cost, this date).
778+
- 2026-10-10 (Derek), #625 — THE TAIL'S PARTICLE RUNS ARE MERGED IN GROUP BEFORE S2'S READ, SO THE READ NEEDS NO BOUND MARK AND NO GATHERING. Since #614 split the trailing run off ahead of the chain, the read sees particles the chain has not merged yet, and #620 re-created the chain's particle-to-particle merge in the reader: `_chain_units` marked a particle standing straight behind another BOUND, the peel stopped at it, and `credential_run` gathered an absorbed run of them into one report. Now group merges each run of two or more adjacent particles past the chain's leading position (`_pieces.merge_particle_runs`, at the trailing site, after P3's joins and before the read) into the piece the chain would open its run with, so the read counts, stops at and absorbs the run as one piece, and the BOUND mark, the peel's stop for it and the gathering are gone. The merged piece keeps `prefix` and drops `title`: a P3 join carries the title tag in from a word in both vocabularies (`St und Berg`), and on the merged run it read the run as a leading title. The title run and the lead are read once, before the merge, and handed to the read, which had asked both again. The chain's PARTICLE_OR_GIVEN report keyed on its run claiming something past the particle (`j > k + 1`), which a merged run has done already though the chain then claims nothing more (`Freiherr von der`); it now keys on that or on the run being one the merge made (`premerged`) -- not on the piece holding several tokens, which a P3-joined phrase does too, and which moved 11,358 reports on the grid below (`Dr. St Do und`). The rule text is unchanged: rules.md#S2 already counts a particle run, the duals straight behind a particle included, as the one name word the chain makes. What grows is code, not concepts: the read loses a mark and two branches and re-creates nothing of the chain, group gains the merge (about 45 more lines than it removes across `_pieces.py` and `_group.py`, docstrings included).
779+
DECIDED (Derek): retiring the two reader-side mechanisms is worth the moves -- every one is garbage input, now read consistently, and none is a released reading restored.
780+
MEASURED 2026-10-10 against 505d6dec (#632's merge), py3.11, `nameparser.__file__` asserted on each side. A particle-run grid -- `{head} {run} {tail}` for every head in (none), `Dr.`, `Freiherr`, `St`, `John`, `Dr. John`, `Freiherr von`, `John Smith`, `Sir`, `Freiherr St`; every run of one to three words over `van der de la von und mc Mc vd do Do St du`; every tail in (none), `Berg`, `MA`, `PhD`, `Ma`, `Jr.`, `Berg MA`, `Prof.`, `Berg Ph. D.`, `V`, `VI`, `X.Y.Z.`, `née Smith`, `Berg Jr. PhD`, `Berg van`, `, Jr.`, `, John` (a comma tail written against the run) -- 395,148 texts, 1,185,444 parses under the three orders, comparing the seven fields and every report (kind, detail, tokens): 774 role moves (258 texts under each order), no report-only move. Every move holds `St und`, the joined piece carrying the title tag, which master merged into the particle run in front with the tag kept and read as a title (`Freiherr Do St und Berg MA` → title 'Freiherr Do St und Berg', family 'MA'); now title 'Freiherr', family 'Do St und Berg', suffix 'MA', the name part read alike with and without the trailing 'MA'. Not a released reading: 1.4.0 reads first 'Do St und Berg', last 'MA', and 2.3.0 family 'Do St und Berg MA', and over the 258 moved texts under the default order master's whole reading equals 2.3.0's on 152, the tree's on none. The S2 stress grid (#614's recipe, above): no role move, and one text's report under its four policies -- `de Mesnil Ma do mc and Ph. D.` no longer reports 'do': P3 joins `mc and Ph. D.` into one suffix-tagged piece, the merge folds 'do' into it, and the peel takes the piece whole rather than picking 'do', the report adf6da88 did not make either and #620 added (its entry above). The #624 title-particle head grid (decisions.md#P2, 2026-10-10): 0. Every corpus name and case text, and #624's comma-tail grid, under the three orders: 0. The five differential gates exit 0, their reports identical line for line to the tree before. The 2026-10-08 prototype's 80 moves (`Dr. mc mc`) are not in the population: #624 already reads `Dr. Mc Mc` as H3 states. Recorded negative controls (2026-10-10): with the merge off, 27 tests in tests/v2 fail (29 over the whole suite), `test_an_absorbed_particle_run_reports_as_the_one_word_the_chain_makes` among them; with the report keyed on `j > k + 1` alone, the case rows `a_particle_takes_the_dual_behind_it_at_the_head` and `a_particle_run_before_the_run_stays_one_name_word`; with the merge starting at the segment's second piece rather than past the lead, 13; with `title` kept, only `test_a_merged_particle_run_is_a_particle_and_no_title`, the unit test added for it. Cost: +3 frames on the reference row, which holds `de la` (decisions.md#parse-cost, this date).
781+
THE REVIEW (2026-10-10) found the change reaching further, every shape garbage, none given a row (Derek): no move goes the other way, and nothing moved over comma shapes (an empty or nickname-only head, a family comma's given part), connective-separated runs, runs beside a maiden marker or holding a nickname, bound given names, one-case names, or caller lexicons adding title or suffix words as particles. (1) The `St und` class is any P3 join whose left word is in both the title and particle vocabularies, `und`, `and`, `of` or `y`, a caller's title-particle included (`Freiherr Do Freiherr and Berg MA`: master title 'Freiherr Do Freiherr and Berg', family 'MA'; now title 'Freiherr', family 'Do Freiherr and Berg', suffix 'MA'). (2) A title-particle head, a particle run, then a title and a dotted word (`Freiherr van der Dr. G.J. MA`, 88 texts on its grid under each order): the run now counts as one name word, so 'G.J.' has none to spare, the read takes nothing and group's own chain takes 'Dr. G.J.' -- family 'van der Dr. G.J.', suffix 'MA', as the one-particle `Freiherr van Dr. G.J. MA` reads (master title 'Freiherr Dr.', family 'van der', suffix 'G.J. MA'). (3) H3's give-back behind a leading title where every word is suffix vocabulary, the last two a run of duals (`Dr. vd Jr. mc mc`, about 1,800 parses over two grids): the merged run is a particle piece and no suffix word, so the title stays -- title 'Dr.', family 'vd', suffix 'Jr. mc mc', where master gave 'Dr.' back as the given name with 'vd' still standing; `Dr. vd Jr. mc` still gives it back. The review also found two runs in one name unreached by any test (an off-by-one in the merge's bookkeeping passed the suite), now the rows `two_particle_runs_merge_each` and `a_second_run_of_duals_is_the_family` (parity) and a half of the merge's unit test, which also holds that a lone particle is no merged run; and the read's title run taken before the merge held only by the frame bands, now `test_the_read_takes_the_title_run_as_written_before_the_merge` (`Dr. mc Jr mc mc`, which with the run asked again after the merge reads family 'mc Jr mc mc').
781782

782783
### indic-honorifics — the renunciate class and the Indic honorific vocabulary (2026-09-06, #346/#344/#343)
783784

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,9 +1863,12 @@ def read_trailing_run(pieces: Sequence[Sequence[int]],
18631863
`n` is the end of the leading title run (`leading_titles`) and
18641864
`lead` the chain's leading position where the caller has asked it,
18651865
both read before `merge_particle_runs` merged the pieces: the merge
1866-
starts past the lead and drops the title tag, so it moves neither
1867-
-- save where H3 had given a title back before a particle that is
1868-
suffix vocabulary ('St mc mc'), which reads as written."""
1866+
starts past the lead and drops the title tag, so it moves the lead
1867+
never, and the title run only where H3 had given a title back
1868+
before a run of particles that are suffix vocabulary, which the
1869+
read takes as written: 'Dr. mc Jr mc mc' reads family 'mc', suffix
1870+
'Jr mc mc', where the run read after the merge would leave 'mc Jr
1871+
mc mc' in the family."""
18691872
if n == len(pieces):
18701873
return None
18711874
units = _chain_units(pieces, ptags, tokens, n, lead)

‎tests/v2/cases.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9867,9 +9867,30 @@ def _check_cjk_shape_purity(self) -> None:
98679867
"a title. #614's read merged 'van Secretary' into one "
98689868
"piece and the title went into the suffix (its second "
98699869
"review); since #620 the read counts the chain's units "
9870-
"and merges nothing, so the run's title test sees every "
9871-
"word as written (1.4.0 middle 'Smith MD', family 'van "
9870+
"rather than reading a merged copy, and only particle "
9871+
"runs are merged ahead of it (#625), so the run's title "
9872+
"test sees 'Secretary' as written (1.4.0 middle 'Smith MD', family 'van "
98729873
"Secretary Jones'; bisected to #602's run)"),
9874+
Case("two_particle_runs_merge_each",
9875+
"John van der Smith de la Berg",
9876+
{"given": "John", "middle": "van der Smith",
9877+
"family": "de la Berg"},
9878+
classification="parity",
9879+
notes="each particle run joins the words after it, the final "
9880+
"group the family (rules.md#P2); group merges both runs "
9881+
"ahead of S2's read (#625), whose bookkeeping across the "
9882+
"first merge no other row reached (#625's review: an "
9883+
"off-by-one there read family 'van der Smith de la "
9884+
"Berg')"),
9885+
Case("a_second_run_of_duals_is_the_family",
9886+
"Anna van der Berg van mc",
9887+
{"given": "Anna", "middle": "van der Berg",
9888+
"family": "van mc"},
9889+
classification="parity",
9890+
notes="'van mc' is one particle run, the dual a particle the "
9891+
"run took and no suffix (rules.md#P2, #S2); merged "
9892+
"ahead of the read after a first run (#625's review: an "
9893+
"off-by-one left 'mc' a suffix)"),
98739894
Case("a_second_particle_run_is_a_unit_of_its_own",
98749895
"Jan van Berg de Mc",
98759896
{"given": "Jan", "middle": "van Berg", "family": "de Mc"},

‎tests/v2/pipeline/test_pieces.py‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,10 +1171,11 @@ def test_a_merged_particle_run_is_a_particle_and_no_title() -> None:
11711171
P3's join carries the title tag in from a word in both vocabularies
11721172
('St und Berg'), and kept on the merged run it read the run as a
11731173
leading title: 'Freiherr Do St und Berg MA' read title 'Freiherr Do
1174-
St und Berg' with the tag kept, family 'Do St und Berg' without it
1175-
(as the 1.4.0 and 2.3.0 wheels read the family). The particle
1176-
leading the name stays out of the merge (P4), so 'de la van Vega'
1177-
merges 'la van' alone."""
1174+
St und Berg' with the tag kept, family 'Do St und Berg' without it.
1175+
The particle leading the name stays out of the merge (P4), so 'de
1176+
la van Vega' merges 'la van' alone; two runs in one name merge
1177+
each, and a lone particle is no run ('John van Smith de la
1178+
Berg')."""
11781179
state = _state_through("classify", "Freiherr Do St und Berg MA")
11791180
tokens = list(state.tokens)
11801181
pieces = [[i] for i in state.segments[0]]
@@ -1197,6 +1198,35 @@ def test_a_merged_particle_run_is_a_particle_and_no_title() -> None:
11971198
assert lead == 0
11981199
assert [" ".join(tokens[i].text for i in p) for p in pieces] \
11991200
== ["de", "la van", "Vega"]
1201+
for text, want in (
1202+
("John van der Smith de la Berg",
1203+
["John", "van der", "Smith", "de la", "Berg"]),
1204+
("John van Smith de la Berg",
1205+
["John", "van", "Smith", "de la", "Berg"])):
1206+
state = _state_through("classify", text)
1207+
tokens = list(state.tokens)
1208+
pieces = [[i] for i in state.segments[0]]
1209+
ptags = [set() for _ in pieces]
1210+
merged, _lead = merge_particle_runs(pieces, ptags, tokens, 0)
1211+
assert [" ".join(tokens[i].text for i in p) for p in pieces] \
1212+
== want
1213+
assert merged == {p[0] for p in pieces if len(p) > 1}
1214+
1215+
1216+
def test_the_read_takes_the_title_run_as_written_before_the_merge(
1217+
) -> None:
1218+
"""#625: group hands the read the title run read before the merge.
1219+
Merging 'mc mc' makes it no suffix piece, which would undo H3's
1220+
give-back of 'mc' and move the name's start; the read keeps the
1221+
run as written, so the credential run behind the first 'mc' stays
1222+
the read's. Recorded negative control (2026-10-10): with the read
1223+
asking `leading_titles` again after the merge, the read takes
1224+
nothing here and the parse reads family 'mc Jr mc mc'."""
1225+
state, found = _read("Dr. mc Jr mc mc")
1226+
assert found is not None
1227+
read, start = found
1228+
assert start == 2
1229+
assert _texts(state, read.tail) == ["Jr", "mc", "mc"]
12001230

12011231

12021232
_WEIGHED_GRID_HEADS = ("John van", "Freiherr von", "anh van", "Jan de la",

0 commit comments

Comments
 (0)