Skip to content

Commit 8ea6865

Browse files
derek73claude
andcommitted
Fix #627's review findings: a guard row and precise docs
- Case row `Freiherr von vd PhD Jones` (parity): a particle run is one name word, and the lone-particle flag is asked only of a piece that opens a unit. A mutant asking it of every piece passed the suite until this row; it now fails that row alone. - rules.md#S2 and the code comment: the particle "counts as a name word of its own" rather than "is the surname", which is true only in the default order. - decisions.md#S2: the units-less half of the clause moves no reading and is shared so the callers agree; the grid's limits (other particles and a head before a suffix comma move the same way); the fingerprint was taken over 195c408's texts; the new guard row. The #602 entry points at the qualification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3464370 commit 8ea6865

4 files changed

Lines changed: 19 additions & 6 deletions

File tree

‎docs/design/decisions.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ for n in ('Smith, John','Smith, XYZ'): print(n, calls_for(off.parse, n), calls_f
742742
COST, `sys.setprofile` call counts (mean of 50, after one warm-up) against `git archive origin/master`: `Smith, John` 183 → 183, `Smith, JOHN` 183 → 183, `Smith, XYZ` 182 → 182, `John Smith, PhD` 217 → 217, `John Smith, MA` 252 → 252, `John Smith, Ph. D.` 254 → 254, `John Smith, CPA` 217 → 217, `John Smith, MD PhD` 260 → 260, `John Smith, MBA CPA` 261 → 261, `Dr. Juan de la Vega III` 366 → 366, `John Smith, XYZ` 251 → 266, and an all-caps record with a capitalized given name and a credential, `LLOYD WEBBER, ANDREW PhD`, 338 → 364, a constant +26 that does not grow with the name's length (`'LLOYD'*64 + ' WEBBER, ANDREW PhD'` 653 → 679; the second draft's per-character contrast test had made that +350). Past the all-caps check the contrast is linear in the words before the comma, about four frames a word (the generator, the case test's call, and the own-words walk's fold and marker test; `'GAULLE ' + 'ap '*k + 'GAULLE, CHARLES'` is +7, +26, +38, +54 over master at k = 0, 1, 4, 8), and constant in a word's letters (`'de ' + 'GAULLE'*k + ', CHARLES'` 253 → 275, 343 → 365 and 631 → 653 at k = 1, 16, 64), where the capital-then-lowercase draft grew a frame per letter; all paid only once a caps word is in hand. The comma test is on by default, so three C-level checks go before any call: two or more words before the comma (the first draft lacked it, and `Smith, JOHN`, a common record format, paid +25 for a flip it can never make — the code review), the first word in capitals, and the lone-two-letter length. Every caller of the caps predicate asks first, in C, what it would decline anyway — alphabetic capitals, not a listed suffix word — so a listed credential never pays for the call (the review of the first fix found `John Smith, CPA` +4 and `John Smith, Ph. D.` +5 before these). The 2026-09-14 entry's recompute recipe predates the enum: its `on` is `Policy(unlisted_caps_suffixes=CapsSuffixes.EVERYWHERE)` now, and its `off=Parser()` is `Policy(unlisted_caps_suffixes=CapsSuffixes.OFF)`, `Parser()` being AFTER_COMMA.
743743
- 2026-10-02 (Derek), #573 — a never-given particle opening the given part after a family comma leaves no word to spare; the member behind it is the surname it heads, unless mixed-case capitals say credential. The record is the P6 section's entry of the same date.
744744
- 2026-10-02 (Derek), #578 — A PARTICLE BETWEEN A CREDENTIAL AND A TITLE-CASE WORD STAYS SILENT, AS IT READS. `Doe, Jane PhD vd Ma` reads given 'Jane', middle 'vd Ma', family 'Doe', suffix 'PhD', reporting nothing. No new rule decides that: S2's given-part slot already says a particle in front of a member takes the member out of the slot, and where the word behind reads as a name the two are one name, asked nothing and reporting nothing — `vd` reads as `van` does (`Doe, Jane PhD van Ma`, the same reading), and S2's own boundary example is `Doe, John van Ma`. The writing is malformed whichever reading is meant: a Title-case `Ma` behind a lowercase `vd` behind a `PhD` is neither a Dutch listing nor a credential run as anyone writes one. A reader leaning anywhere leans suffix (Derek: both words follow a known credential, and the writer likely did not know how suffixes are capitalized), but that is a guess about a writer's mistake, not a convention the writing carries. Declined: (a) reading `vd Ma` as suffixes, which needs code to make a credential speak across a particle — against P2 (a particle joins the word behind it) and #554 (`vd` speaks for nothing behind it); (b) reporting the fork, which needs an emitter on the family-comma path for a decision the particle chain makes before assign knows which read the part gets (AGENTS.md: prefer leaving a fork silent and documenting the omission over machinery for input nobody reads with confidence). Where the writing does speak the parser already follows it: `Doe, Jane PhD vd MA` reads suffix 'PhD vd MA' and reports (#573). Pinned by the case row `a_particle_after_a_credential_heads_a_title_case_name`.
745-
- 2026-10-04 (Derek), #602 — A CREDENTIAL AFTER THE NAME CORE STARTS A RUN TO THE END OF ITS PART. An unambiguous Latin suffix word of two or more letters standing after two name words with no comma (a lone particle not counting toward the two), or after the given word in the part after a family comma, makes every later word of that part a suffix, except a title word, which reads as a title: `John Smith PhD Jones` → suffix 'PhD Jones', `Eric H. Holder Jr. Attorney General` → suffix 'Jr.', title 'Attorney General'. A name word the run absorbs is reported `suffix-or-name`, a reader hesitating over `Jones` there. Before this the three comma shapes disagreed — middle 'Smith PhD' with no comma, middle 'Jones' and suffix 'PhD' after a family comma, and suffix 'Jr., Jones' after a suffix comma (C2's tail) — and the no-comma reading made a credential a middle name, the one reading nobody intends. The suffix-comma path already read it this way, and the comma spelling of the Holder name always did.
745+
- 2026-10-04 (Derek), #602 — A CREDENTIAL AFTER THE NAME CORE STARTS A RUN TO THE END OF ITS PART. An unambiguous Latin suffix word of two or more letters standing after two name words with no comma (a lone particle not counting toward the two; qualified by #627, below), or after the given word in the part after a family comma, makes every later word of that part a suffix, except a title word, which reads as a title: `John Smith PhD Jones` → suffix 'PhD Jones', `Eric H. Holder Jr. Attorney General` → suffix 'Jr.', title 'Attorney General'. A name word the run absorbs is reported `suffix-or-name`, a reader hesitating over `Jones` there. Before this the three comma shapes disagreed — middle 'Smith PhD' with no comma, middle 'Jones' and suffix 'PhD' after a family comma, and suffix 'Jr., Jones' after a suffix comma (C2's tail) — and the no-comma reading made a credential a middle name, the one reading nobody intends. The suffix-comma path already read it this way, and the comma spelling of the Holder name always did.
746746
WHAT STARTS THE RUN is one predicate, `_pieces.starts_a_credential_run`, read by the trailing peel (`credential_run`, applied to every finished peel, so assign, P5's reserve, P2's chain stop and the maiden walk see one answer) and by the given-part walk. EXCLUDED, each with its reason: a TITLE WORD — 746 title words are in no suffix set (measured 2026-10-04: `len(L.titles - L.suffix_acronyms - L.suffix_words)` for the default lexicon), many of them surnames (`King`, `Bishop`, `Judge`), so `Mary Jane King Smith` keeps family 'Smith'; a title is read from the end of a name only behind a comma or with a period (H5, Derek). The AMBIGUOUS CLASS, which carries no evidence of which it is. A SINGLE LETTER, in any case: a lowercase `i` or `v` carries no `initial` tag, and found by the v1 off-switch test, `Josep Carod i Rovira` with `i` removed from the conjunctions read suffix 'i Rovira'. A CONNECTIVE. A word that is also PARTICLE or BOUND GIVEN-NAME vocabulary — found by the suite, not foreseen: `abd` is in the credential list (ABD) and heads `Abd Allah`, and `Mohamed Ali Abd Allah` read suffix 'Abd Allah' until it was excluded. The NON-LATIN HONORIFIC words, an honorific followed by a name word being a different question from a credential followed by one. A word that is also a title (`MD`, `Ms`, `Sr.`) does start the run: position decides, and after the name core it is the suffix.
747747
DECLINED: lifting the credential out — `John Smith PhD Jones` as given 'John', middle 'Smith', family 'Jones', suffix 'PhD', the family-comma path's old reading. It keeps every name word in a name field, but it reads a credential as something written in the middle of a name, which is the reading the rule rejects.
748748
REVERSED BY THIS ENTRY rather than edited: the #578 bullet above (`Doe, Jane PhD vd Ma` now reads suffix 'PhD vd Ma', the credential starting the given part's run), and two of the three shapes #544's ACCEPTED LIMITS kept out of the company's reach — a title standing between the credential and the member, and a member a particle chain had taken. The case rows `a_title_between_degree_and_member_keeps_it_a_name` and `a_member_the_particle_chain_took_is_out_of_reach` keep their ids and now pin the run. The third, the merged `Ph. D.`, stays out of reach: that piece is outside the walk the run is read over.
@@ -767,9 +767,9 @@ for n in ('Smith, John','Smith, XYZ'): print(n, calls_for(off.parse, n), calls_f
767767
/SIMPLIFY (2026-10-08), measured. Each pass of `tail_reading` had rescanned for the second unit from the front, a C-level quadratic no frame guard saw: `Freiherr von` + `Berg `*n + `MA Dr. `*n read 5.77x for 4x the input against master's 4.08x; the lookup is now made once per read and handed to every pass (4.03x), and `test_the_second_unit_is_found_once_per_read` counts it. `_weighed` is pinned to the reading by `test_a_word_the_read_takes_is_never_one_the_count_joined` (no piece marked JOINED is ever in the run the read takes; 320 grid names fail with the test answering False). The gathering of an absorbed particle run reads the BOUND marks, and #602's run start one condition. Grids byte-identical, frames unchanged. Left for a follow-up, each moving readings: merging the tail's particle runs in group before the read, which would retire the BOUND stop and the gathering (80 grid names move, `Dr. mc mc`, and the chain's particle report needs re-keying); and the name start decided once, the item below.
768768
LEFT OPEN, by Derek's scoping: reading a family comma's given part the same way, and deciding the name's start once for a title-particle head (#620's related paths, which change readings).
769769
- 2026-10-10 (Derek), #627 — A LONE PARTICLE WITH THE RUN RIGHT BEHIND IT IS THE SURNAME. #602's run start needs two name words before the credential, and "a lone particle is not a name word for the count", for `de Mesnil`, where the particle and the word behind it are one surname. Since #620 the count is taken in the chain's units, so a particle that opens a unit counts once (`van der`, `von Berg`) and a P3-joined phrase is one piece (`von und zu`); the lone particle stayed uncounted only where nothing joins it, which is exactly where a credential stands behind it. So `John von PhD Jones` read middle 'von PhD', family 'Jones' -- the chain later taking the credential as the particle's surname -- while `John van der PhD Jones` and `John von und zu PhD Jones` read family 'van der' / 'von und zu', suffix 'PhD Jones', and the comma spelling `Smith, John von PhD Jones` already read suffix 'PhD Jones'. The issue's question was which way to make them agree.
770-
DECIDED (Derek): the particle stops at the credential, so it is the surname 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 for both its callers (with the chain's units and without). 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.
770+
DECIDED (Derek): the particle stops at the credential, so it is the surname 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 input shows that half: it changes the position it returns for `Smith, John von PhD Jones` (2 where master gave 4) and nothing downstream reads the difference (#627's review, a mutant ignoring the flag there passing the suite and the grid byte-identical). 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.
771771
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.
772-
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 -- `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. No comma shape moves. Fingerprint of every `tools/differential/corpus*.jsonl` name and `tests/v2/cases.py` text under the three orders (1,925 texts, the seven fields and every report): 0 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.
772+
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).
773773

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

‎docs/design/rules.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,8 @@ S2. Rationale: generational suffixes and credentials are recognized
11611161
credential. A lone particle is
11621162
not a name word for the count, 'de Mesnil' being one surname,
11631163
unless the credential stands right behind it, leaving it nothing
1164-
to join: then it is the surname ('John von PhD Jones'). A
1164+
to join: then it counts as a name word of its own ('John von PhD
1165+
Jones'). A
11651166
particle run with the words the chain joins to it is one name
11661167
word, 'der la' and 'la Smith Secretary' each counting once (P2).
11671168
A title word

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,8 +1222,8 @@ def run_start(rest: Sequence[int], names: int,
12221222
# -- unless it opens a chain run, which is then the surname
12231223
# (with no `units` every piece opens its own), or the run
12241224
# starts right behind it, leaving it nothing to join: then it
1225-
# is the surname alone, as 'van der' and 'von und zu' are
1226-
# (#627, 'John von PhD Jones')
1225+
# counts as a name word of its own, as 'van der' and 'von und
1226+
# zu' do (#627, 'John von PhD Jones')
12271227
if units is None or units[q] == OPENS:
12281228
lone = (len(pieces[q]) == 1 and "particle" in tags
12291229
and (units is None or q + 1 == len(units)

‎tests/v2/cases.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10009,6 +10009,18 @@ def _check_cjk_shape_purity(self) -> None:
1000910009
"'van der' and 'von und zu' already did; the row above "
1001010010
"is the contrast, a lone particle with a name word "
1001110011
"behind it (1.4.0 and 2.3.0 middle 'von PhD')"),
10012+
Case("a_particle_run_before_the_run_stays_one_name_word",
10013+
"Freiherr von vd PhD Jones",
10014+
{"title": "Freiherr", "given": "von vd", "middle": "PhD",
10015+
"family": "Jones"},
10016+
ambiguities=("particle-or-given",),
10017+
classification="parity",
10018+
notes="the row above with a particle run: 'von vd' is one "
10019+
"name word, 'vd' a particle the run took, so only one "
10020+
"name word stands before 'PhD' and #602's run does not "
10021+
"start -- the lone-particle count is asked only of a "
10022+
"piece that opens a unit, not of 'vd' standing right "
10023+
"before the credential (#627's review)"),
1001210024
Case("a_dual_in_the_chain_stays_a_name_word_behind_a_title",
1001310025
"John van mc Prof.",
1001410026
{"title": "Prof.", "given": "John", "family": "van mc"},

0 commit comments

Comments
 (0)