Skip to content

Make a title transparent to the leading-particle exception (#367) - #370

Open
derek73 wants to merge 9 commits into
masterfrom
claude/issue-367-title-transparent-particle
Open

Make a title transparent to the leading-particle exception (#367)#370
derek73 wants to merge 9 commits into
masterfrom
claude/issue-367-title-transparent-particle

Conversation

@derek73

@derek73 derek73 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Closes #367.

A leading particle deliberately does not chain — that is what makes Van Johnson read as given Van, family Johnson. But the exception was keyed on piece index 0, so a title displaced the particle out of leading position and the chain fired. Identical name text parsed differently depending on whether a title preceded it:

Van Johnson         given='Van'          family='Johnson'
Dr. Van Johnson     given=''             family='Van Johnson'
Sir Van Johnson     given='Van Johnson'  family=''            (no surname at all)

A title is not part of the name, so it should not change whether the name begins with a particle. The exception now skips leading title pieces.

What moves

                        before                        after
Dr. Van Johnson         family='Van Johnson'          given='Van'  family='Johnson'
Dr. Van Johnson Smith   family='Van Johnson Smith'    given='Van'  middle='Johnson' family='Smith'
Mr. Van Nguyen          family='Van Nguyen'           given='Van'  family='Nguyen'
Sir Van Johnson         given='Van Johnson'           given='Van'  family='Johnson'
Sir de Mesnil           given='de Mesnil'  family=''  family='de Mesnil'
Sheik Abu Bakar         given='Abu Bakar'  family=''  given='Abu'  family='Bakar'   (#369)

Unchanged: Mr. Smith, Mrs. Smith, Mr. Nguyen, Mr. John Smith, Sir Ian, Sir Ian McKellen, King Henry, and every untitled shape.

It fixes a v1-era xfail that said it couldn't be fixed

@pytest.mark.xfail
def test_first_name_is_prefix_if_three_parts(self):
    """Not sure how to fix this without breaking Mr and Mrs"""
    hn = HumanName("Mr. Van Nguyen")

It passes now, and it does not break Mr and Mrs — every bare Mr./Mrs. Surname shape is byte-identical, because those have no particle to displace. The marker is removed.

It also reverses the case row titled_ambiguous_particle_chains, which pinned Dr. Van Johnsonfamily='Van Johnson' as intended v1 parity. That row and the xfail describe the same shape — the repository pinned it simultaneously as correct and as broken. This resolves it toward the xfail.

The naive fix is wrong, and the suite caught it

Skipping pieces that are titles breaks names that never had a title: st, do and freiherr are each both a title and an ambiguous particle, so a plain "not a title" test steps over the very piece the exception protects.

St John Smith    ->  given='St John Smith'      (with the naive test)

That is also what failed test_add_title: it adds te to the titles, and te ships as an ambiguous particle. The shipped test is not title(k) or prefix(k) — step over a piece that can only be a title, stop at one that could be the name's own first piece. With it, every untitled shape is byte-identical and test_add_title needs no change.

Suffix pieces are deliberately not skipped, by measurement: the shapes that look like they need it (Jr. Van Johnson) classify their leading piece as a title already, and skipping the rest cost Ph. D. Van Johnson its family name.

_group's PARTICLE_OR_GIVEN emitter is still live

It fired only when a title displaced the particle, so it looked like dead code. It isn't: the emitter needs all(title(x) for x in range(k)), which after this change requires a leading piece that is a title and a prefix — exactly the st/do/freiherr overlap.

Freiherr von Richthofen  ->  title='Freiherr'  family='von Richthofen'
                             PARTICLE_OR_GIVEN: 'von' was chained onto the following name piece

The five tests that covered it are re-pointed there. Two others — test_no_op_prefix_chain_is_not_a_fork and test_a_fork_is_reported_by_exactly_one_stage — turned out to be passing without exercising their branch once a plain title stopped reaching it, and now use inputs that do.

Differential

Exactly one corpus name moves, Mr. Van Nguyen, and only its given/family. All three baselines exit 0:

2.1.0   751 names; intentional diffs:   1; unexplained: 0
2.0.0   751 names; intentional diffs:  90; unexplained: 0
1.4.0   751 names; intentional diffs: 108; unexplained: 0

A ledger integrity finding came out of this. The 1.4.0 run exited 0 before any rule was added — the diff was being absorbed by fix(suffix-routing), a fields-only rule about a trailing credential whose ["given","family","suffix"] is a superset of this diff's fields. A rule was silently explaining a change it does not describe, and the exit code showed nothing. The new fix(#367) rule claims it back.

pytest            3168 passed, 20 skipped, 10 xfailed
mypy              Success: no issues found in 103 source files
ruff              All checks passed!
sphinx html       build succeeded
sphinx doctest    225 tests, 0 failures

Review round

A four-agent review found the mechanism correct and the prose wrong in nine places. Five follow-up commits.

Verified sound, at depth. leading is computed once before a loop that mutates pieces; a variant recomputing it every iteration is byte-identical over 29,105 names. The , 0 fallback is provably inert, not merely unexercised. And the best evidence the change is right: over 268 corpus names, comparing parse(N) against parse("Dr. " + N), the base tree has 30 non-transparent names and this branch has 19 — a strict subset. Nothing becomes newly non-transparent; 11 are fixed.

What was wrong:

  • "no other field moves on any name" was false twice. title moves (Dr. St John Smith → title Dr. St) and maiden moves (Mr. Van Johnson nee Brown → maiden Brown, because un-chaining exposes the marker). The claim appeared in the release log and, load-bearingly, as the justification for the ledger rule's fields in all three ledgers. suffix and nickname genuinely never move.
  • The ledger rule claimed the repo's regression canaries. (?i)^\S+\.?\s+van\b reached 11 corpus names and explained 1 — the others including Vincent van Gogh, which AGENTS.md names as a standard canary. Since name_regex rules outrank fields-only ones, a future regression there would have been labelled fix(#367) and exited 0. This PR discovered that exact defect in fix(suffix-routing) and then reproduced it; now narrowed to (?i)^mr\.\s+van\b, reaching 1. (\.? was inert — \S+ is greedy.)
  • Three sites still called A title displaces a particle out of leading position, so Dr. Van Johnson reads as one surname #367 an open bug, one a #: comment rendered into the published API docs. A fourth was wrong end to end: it described Mr. de Mesnil as two pieces with rule 1b declining; it is three pieces and 1b fires. The output is unchanged, which is why no test caught it.
  • "Exactly one shape reaches the emitter" was asserted at five sites and is false — 2,520 hits across four structurally distinct shapes, including Dr. Do van Johnson, where a plain title is present. The reachability argument itself is sound and survives the conjunction-merge path; only the framing was wrong.
  • The suffix rationale was inverted. Ph. D. Van Johnson has no family name under the shipped rule; skipping suffixes gives it one. The real reason is the other half — skipping moves Van into the middle for Ph.D./II/Msc.Ed. shapes. Corrected and inlined; the citation pointed at a release-log section that never existed.
  • The suffix decision was unpinned — the mutation flipping it survived all 3169 tests. Now pinned.
  • Shapes the release log names (Sir Van Johnson, Sir de Mesnil, Jr. Van Johnson, Dr. Van Johnson Smith) had zero assertions; the Sheik abdul salam splits the bound given name that abdul salam ahmed joins #369 regression had none and is not in the corpus. Both now covered, the latter by a strict xfail so fixing Sheik abdul salam splits the bound given name that abdul salam ahmed joins #369 announces itself.
  • The family-first count was seven and is now eight; release-log examples now name their order; test_first_name.py's docstring named the rule this PR rejected.

Filed rather than fixed here: #371 (a leading suffix displaces the particle exactly as titles used to — Ph. D. Van Johnson has no surname) and #372 (pre-existing ledger rules classifying diffs they do not describe, plus a rule that stops explaining anything being invisible to the harness).

pytest            3178 passed, 20 skipped, 11 xfailed
mypy / ruff       clean
sphinx html       build succeeded
sphinx doctest    225 tests, 0 failures
differential      2.1.0: 1 · 2.0.0: 90 · 1.4.0: 108 intentional, 0 unexplained, exit 0

Known regression, tracked

Sheik Abu Bakar → given Abu, family Bakar. It read as one given name only because abu happens to be a particle as well as a bound given name — Sheik abdul salam shows bound-given alone never chained. #369 covers giving that reading a path of its own.

🤖 Generated with Claude Code

derek73 and others added 4 commits August 9, 2026 20:20
A leading particle deliberately does not chain -- that is what makes
"Van Johnson" read as given Van, family Johnson rather than as one
surname. But the exception was keyed on piece index 0, so a TITLE
displaced the particle out of leading position and the chain fired.
Identical name text then parsed differently depending on whether a
title preceded it:

    Van Johnson         given='Van'          family='Johnson'
    Dr. Van Johnson     given=''             family='Van Johnson'
    Sir Van Johnson     given='Van Johnson'  family=''

The last row compounded it: with a given-name title, post_rules rule
1's carve-out handed the whole chain to `given`, so the name came out
with no surname at all.

A title is not part of the name, so it cannot decide whether the NAME
begins with a particle. The chain loop's `k == 0` becomes "the first
piece of the name", computed once before the loop.

Not the plain "first piece that is not a title" the rule is stated as,
and the difference is not academic: `st`, `do` and `freiherr` are each
BOTH a title and an ambiguous particle, so a plain title test skipped
over the very piece the exception exists to protect. Measured, the
untitled "St John Smith" collapsed from title St, given John, family
Smith into one given name "St John Smith", and "Do John Smith" and
"Freiherr Blah Blah" with it -- none of which has a title in front of
it at all. It also broke test_constants.py::test_add_title, whose
"Te Awanui-a-Rangi Black" adds `te` to the titles while `te` ships as
an ambiguous particle, manufacturing the same overlap from config. So
the scan steps over a piece that can ONLY be a title and stops at one
that could be the name's own first piece; with that, every untitled
shape is byte-identical and test_add_title needs no change.

Suffix-flagged pieces are deliberately not skipped too. The shapes
that look like they need it -- "Jr. Van Johnson", "MD Van Johnson",
"PhD Van Johnson" -- classify their leading piece as a TITLE and are
already covered. What remains is a leading piece assign puts in GIVEN
("Ph.D. Van Johnson", "II Van Johnson", "Msc.Ed. Van Johnson"), which
is part of the name in the output and holds the leading name position
exactly as John does in "John Van Johnson"; skipping it also measured
"Ph. D. Van Johnson" losing its family name outright.

What moves, all of it a titled name whose first name-piece is a
particle:

    Dr. Van Johnson         family='Van Johnson'   -> given='Van' family='Johnson'
    Dr. Van Johnson Smith   family='Van Johnson Smith'
                                                   -> given='Van' middle='Johnson' family='Smith'
    Mr. Van Nguyen          family='Van Nguyen'    -> given='Van' family='Nguyen'
    Sir Van Johnson         given='Van Johnson'    -> given='Van' family='Johnson'
    Sir Van Johnson Smith   given='Van Johnson Smith'
                                                   -> given='Van' middle='Johnson' family='Smith'
    Sir de Mesnil           given='de Mesnil'      -> family='de Mesnil'
    Sheik Abu Bakar         given='Abu Bakar'      -> given='Abu' family='Bakar'

The last one is a regression, tracked as #369: it worked before only
because `abu` happens to be a particle as well as a bound given name,
and "Sheik abdul salam" shows bound-given alone does not chain.

test_first_name_is_prefix_if_three_parts drops a v1-era xfail whose
docstring said "Not sure how to fix this without breaking Mr and Mrs".
It does not break Mr and Mrs -- a bare "Mr./Mrs. Surname" has no
particle to displace, so nothing about it reaches the rule that moved
-- and those shapes are now asserted in the test rather than only
asserted about.

cases.py's titled_ambiguous_particle_chains pinned the opposite and
cited v1 parity. Parity was real (1.4.0 gives last 'Van Johnson') and
still not the tiebreaker it looked like: "Dr. Van Johnson" and
"Mr. Van Nguyen" are the same shape, so v1 pinned one shape both as
correct and as broken. Resolved toward the xfail. The fork is still
reported either way, from assign rather than group -- the same place
the untitled "Van Johnson" reports it.

_group's PARTICLE_OR_GIVEN emitter is NOT dead, though every test that
used to reach it went through a plain title. It fires only when a
piece before the particle is both a title and a prefix, which is the
`st`/`do`/`freiherr` class again: "Freiherr von Richthofen" is the one
natural spelling of it, and the four tests that pinned the emitter
move onto it. The no-op-chain tests move to "Do Van Jr." for the same
reason -- with a plain title the loop now skips the particle before
the j > k + 1 guard is ever consulted, so they were passing without
exercising the branch they exist to pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first 2.2 change to move the default-order corpus, so
expected_since_2.1.0.toml gains its first rule and its header stops
saying it is empty on purpose.

Exactly one of 751 corpus names moves -- 'Mr. Van Nguyen', given ''
-> 'Van' and family 'Van Nguyen' -> 'Nguyen' -- measured by running
every corpus name through fd8dd8d and through the tree rather than by
reading the harness's summary, so the count covers names that already
diff from a baseline for other reasons. No field outside
given/middle/family moves on any name.

    baseline   intentional   unexplained
    2.1.0                1             0
    2.0.0               90             0
    1.4.0              108             0

The 1.4.0 copy has a second job. That run already exited 0 WITHOUT a
rule, because the diff was landing on fix(suffix-routing), a
fields-only rule about a trailing credential whose
["given", "family", "suffix"] is a superset of this diff's fields --
a rule silently absorbing a change it does not describe, which is the
mis-classification the README warns about and is invisible in an exit
code. A name_regex rule outranks every fields-only one, so the new
rule claims it back: that class goes 26 -> 25 and #367 takes 1, with
no other class moving at any baseline.

The regex is deliberately narrower than the class it documents. The
class is any title followed by a particle -- 'Dr. Van Johnson', 'Sir
de Mesnil', 'Sheik Abu Bakar' all move -- and only `van` is written,
because it is the only member the corpora exercise; a name that joins
the class later should arrive UNEXPLAINED and be read once. The
leading `\S+\.?` title slot is the loose half, since no regex over the
raw string can ask whether a word is a title, and `fields` carries the
tightness instead.

_CORPUS_CLAIMS records the rule at 11 names in each ledger -- the
corpus names the regex reaches, not the one it explains, which is what
that roster measures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four tests that pinned _group's PARTICLE_OR_GIVEN emitter moved to
"Freiherr von Richthofen" in the first commit, which leaves the
mechanism pinned only from the public API. Two more pins, one per half
of the rule, at the stage that owns it:

test_a_title_does_not_make_the_particle_behind_it_non_leading is the
change itself in `pieces` -- "Mr. Van Johnson" groups as
["Mr.", "Van", "Johnson"], the untitled grouping with a title in front
of it.

test_a_title_that_is_also_a_particle_stops_the_scan is why the rule is
not spelled "the first piece that is not a title", and it is written
against a CONFIGURED overlap rather than against `st`/`do`/`freiherr`.
That is the same shape from the other direction: this module's fixture
lexicon is a small hand-built one, and adding `van` to its titles is
exactly what tests/test_constants.py::test_add_title does with `te` --
which is how the plain-title spelling was caught, since it broke that
test as well as the untitled "St John Smith".

test_a_leading_ambiguous_particle_is_reported_once_and_only_once gains
"Freiherr " to its lead tuple. It swept "", "Dr. " and "Dr. Ann " over
all 39 ambiguous particles and four tails, and after #367 not one of
those reaches _group's emitter -- the sweep of the coordination
between two emitters would have been measuring one. The new lead is
the shape that still reaches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every claim below was re-measured by parsing rather than reasoned
about, per AGENTS.md's rule for mechanism claims.

docs/release_log.rst gains the Behavior Changes bullet, which has to
say plainly what moves because it reverses documented behaviour. It
names the shapes that move, the shapes that provably do not (bare
Mr./Mrs., and the title-that-is-also-a-particle class), the detail-text
change that follows from the fork moving stages, and the
"Sheik Abu Bakar" regression tracked as #369. The 2.2 summary
paragraph above it claimed no default-order parse changed at all; that
was true through #354, #358 and #361 and is not true now.

AGENTS.md, in the two places it asserted the old reading. The
particles.py entry recorded "Sir de Mesnil" -> given "de Mesnil", no
family, as an output believed wrong and tracked as #367; it is now the
untitled reading, and #367 got there by removing the chain rather than
by touching post_rules rule 1b, which is the part worth keeping. The
ambiguity-emitter entry said _group reports "when a title shifts it
off index 0", which was the whole of that emitter's reach and is no
longer any of it -- and since "is this emitter now dead?" is the
question a reader arrives with, the answer and the input that settles
it are written down.

nameparser/_types.py's PARTICLE_OR_GIVEN docstring, published API
reference: the second shape it describes is no longer "a particle a
title shifted off the front".

nameparser/config/particles.py said the chain "skips the first piece
unconditionally, membership in this set or any other never entering
into it". The first half now reads "the first piece of the NAME", and
the second half is false as written -- the titles vocabulary does
enter into it, in exactly one way, which is the st/do/freiherr overlap
and is worth naming where a reader adding a particle will see it.

nameparser/_lexicon.py's particles_ambiguous docstring and
_pipeline/_assign.py's module docstring each named the leading piece
in a way the change makes ambiguous; both now say which "leading" they
mean.

_group.py's emitter comment is rewritten around the reachability
argument rather than the example: `all(title(x) for x in range(k))`
and `leading` can now both hold only if one of the leading titles is
also a particle, which is what makes the branch narrow rather than
dead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73 derek73 added this to the v2.2 milestone Aug 10, 2026
@derek73 derek73 added the bug label Aug 10, 2026
@derek73 derek73 self-assigned this Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #370   +/-   ##
=======================================
  Coverage   98.51%   98.51%           
=======================================
  Files          44       44           
  Lines        2894     2895    +1     
=======================================
+ Hits         2851     2852    +1     
  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.

_group's PARTICLE_OR_GIVEN emitter needs a leading piece that is both a
title and an ambiguous particle, and since this branch that is the ONLY
shape reaching it -- an ordinary title is now transparent to the
leading-particle exception, so it takes _assign's branch. The five tests
covering the emitter therefore lead with "Freiherr", and a comment names
the class (freiherr/st/do) they depend on.

A comment is the wrong medium for that. #360 is considering moving words
between the particle sets, and freiherr and st are both on its candidate
list. If either moves, those tests fail as five unrelated-looking parse
mismatches and the reader has to find the comment to learn why.

The precondition is now a test, and it distinguishes the two cases,
which want opposite fixes. A missing WORD means pick another from the
set -- the message prints what is left. An EMPTY intersection means the
emitter is unreachable, every test of it is measuring nothing, and the
right move is to delete the emitter rather than repoint its tests.

Verified both branches by evaluating the guard against mutated sets:
dropping 'freiherr' reports "need a lead from ['do', 'st']", dropping all
three reports "remove the emitter rather than repointing".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73 and others added 4 commits August 9, 2026 22:10
The release log and all three ledgers claimed the change only ever
redistributes given/middle/family and "never touches title, suffix,
nickname or maiden". Two of those four are wrong.

`title` moves: a word in BOTH vocabularies stops the transparency scan
and stays a title piece instead of being chained onto the name, so
"Dr. St John Smith" goes from title 'Dr.', family 'St John Smith' to
title 'Dr. St', given 'John', family 'Smith'. `maiden` moves: un-chaining
lets a marker standing behind the particle be seen at all, so
"Mr. Van Johnson nee Brown" goes from family 'Van Johnson nee Brown'
with no maiden name to given 'Van', family 'Johnson', maiden 'Brown'.
Over a 20,979-name stress set of this class, `title` moved on 2,583
names (always growing, never shrinking) and `maiden` on 1,638 (always
appearing, never lost); `suffix` and `nickname` moved on none.

The ledgers keep fields = ["given", "middle", "family"], which is still
what they should claim -- it is now stated as deliberately narrower than
the change, so a title- or maiden-moving corpus name arrives UNEXPLAINED
rather than being absorbed, the same posture the `van` literal already
takes.

Two more counts in the release log:

* the release summary said the family-first orders move "the same seven
  names under each". Measured against 2.1.0 over the 751 corpus names it
  is eight under each -- the seven from #359 plus "Mr. Van Nguyen", which
  the title fix moves in every order.
* the #367 bullet stated every worked example in default-order fields
  without saying so. Under both family-first orders "Dr. Van Johnson"
  reads family 'Van', given 'Johnson'; the #359 bullet below already
  names its orders explicitly, so this one now does too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three sites still described #367 as an open bug. The prose sweep caught
AGENTS.md and missed these:

* nameparser/config/particles.py -- "'Sir de Mesnil' gives given
  'de Mesnil' and no surname at all -- that is an open bug (#367)".
  Measured on HEAD: title 'Sir', family 'de Mesnil'. The remaining case
  where a chain IS reported as the given name is the one this set means
  to draw -- FAMILY_FIRST "Juan de la Vega", given 'de la Vega' -- so
  that is what it now cites. This is a `#:` comment and renders into the
  published API docs.
* _post_rules.py's 1b preamble -- same claim, and the guard now fires.
* _post_rules.py's worked example for 1b's single-token invariant was
  wrong end to end. "Mr. de Mesnil" is not "three tokens in two pieces";
  it is three tokens in THREE pieces, [['Mr.'], ['de'], ['Mesnil']].
  Both sites are one token long, so 1b FIRES rather than declines, and
  rule 1 cannot be producing the family reading -- rule 1 is gated on
  `not families` and 'Mesnil' is already the family. Output is unchanged
  either way, which is why no test caught it.

"Exactly one shape" / "the only shape" reaches _group's PARTICLE_OR_GIVEN
emitter was asserted at five sites (_group.py, tests/v2/cases.py,
tests/v2/test_parser.py twice, AGENTS.md) and is false. Measured over a
20,979-name stress set: 2,520 hits, and "Freiherr von Richthofen",
"St Van Johnson", "Do St Johnson" and "Dr. Do van Johnson" are four
structurally different ways in. The last one matters most, because
_group.py said "a plain title no longer reaches here at all" and it has
one. The real reach is: any number of plain title pieces, then a piece in
BOTH vocabularies, then any number of further titles, then an ambiguous
particle whose chain claims something.

The reachability argument itself is sound and is now stated the way it
actually works. `leading` is STRICTLY before k (the loop skips
k == leading), so it is one of the titles ahead of k and must also be a
prefix -- a word in both vocabularies. The conjunction merge is the only
other way a piece acquires those tags and cannot manufacture the pair: it
derives from one neighbor, the left one whenever there is one, and its
right operands are always fresh pieces. 243,280 conjunction-bearing
probes produced no title+prefix piece without a both-vocabulary word.

tests/test_first_name.py's docstring stated the rule as "the first piece
that is not a title" -- precisely the naive rule this PR shows is wrong.
Measured: that predicate collapses "St John Smith" into one given name
and fails 15 tests including test_add_title. The shipped predicate is
`not title(k) or prefix(k)`; the Mr/Mrs conclusion holds under either,
which is why no test caught the docstring.

_group.py's suffix rationale had the sign backwards. Skipping suffix
pieces does not cost "Ph. D. Van Johnson" its family name -- shipped
gives given 'Van Johnson', family '', and the skipping variant gives it
given 'Van', family 'Johnson'. The real reason is the other half:
"Ph.D. Van Johnson", "II Van Johnson" and "Msc.Ed. Van Johnson" put
their leading piece in `given` and read family 'Van Johnson', and
skipping moves Van out of the family into the middle name. The pointer
to the 2.2.0 release log, which says nothing about suffixes, is replaced
by the reasoning inline.

Minors, all measured: _lexicon.py said "Dr. de Mesnil" groups into two
pieces (it is three; the NAME is two); the `, 0` fallback now says it is
inert by construction, since no piece being a prefix means the loop
merges nothing; and cases.py's `titled_ambiguous_particle_chains` is
renamed to `..._does_not_chain`, its id having come to contradict its own
expectation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ries (#367)

The fix(#367) rule shipped with name_regex = "(?i)^\S+\.?\s+van\b". The
title slot matched any first word at all, so the rule reached 11 corpus
names and explained 1. The other ten include 'Vincent van Gogh',
'VINCENT VAN GOGH' and 'Alex van Johnson' -- the "Van Johnson" family
AGENTS.md names as a standard regression canary. Since name_regex rules
sort ahead of fields-only ones and `fields` matches by subset, a future
regression on those names was labelled fix(#367) and exited 0. At the
2.1.0 baseline that is a strict loss: the ledger was empty before this
PR. It is the same defect this PR found in fix(suffix-routing).

Narrowed to "(?i)^mr\.\s+van\b", matching the comment's own
deliberately-narrower-than-the-class posture. `\.?` went with it, being
inert -- `\S+` is greedy, so it had already taken the period; both
spellings reach the same 11 names.

Demonstrated rather than argued. With a simulated chain regression in a
scratch tree, the wide regex absorbs 7 corpus names under fix(#367) --
'Alex van Johnson', 'Vincent van Gogh', 'VINCENT VAN GOGH',
'Vincent van Gogh van Beethoven', 'Charles van der van der Berg',
'Mike van der Velt' and the one it explains -- while the narrow one
absorbs 1 and every canary arrives UNEXPLAINED. The three _CORPUS_CLAIMS
entries go from _Claim(11, ..., "b9d738c0e73b") to
_Claim(1, ..., "dce0ae6df4be"), read off the guard rather than predicted.
All three baselines still exit 0 and still classify 'Mr. Van Nguyen'.

Four things the prose asserted and nothing tested:

* The suffix decision. "Also skip suffix pieces" survives all 3169 tests
  while changing output, so
  test_a_suffix_shaped_leading_piece_is_not_stepped_over pins the two
  shapes that decide it. Kill proven: with the mutation applied to a
  scratch copy of this tree, that test is the only failure (1 failed,
  3169 passed).
* Four shapes the release log names with zero assertions anywhere get
  cases.py rows: "Dr. Van Johnson Smith", "Sir Van Johnson",
  "Sir de Mesnil" and "Jr. Van Johnson", each classified fix(#367)
  against measured 1.4.0 output. The existing "Dr. Van Johnson" row is
  reclassified from the default "parity" for the same reason: 1.4.0
  gives last 'Van Johnson', so parity stopped holding when this PR
  changed the row's expectation.
* #369's regression. "Sheik Abu Bakar" lived only in ledger comments and
  is in no corpus, so nothing would notice it moving again. A strict
  xfail now asserts the DESIRED post-#369 output; verified strict by
  flipping the assertion to today's output in a scratch tree and
  watching it fail XPASS(strict).
* test_chained_particle_detail_is_order_invariant covered all three
  orders for "Freiherr von Richthofen" -- the shape #367 did NOT move.
  It now also pins "Dr. Van Johnson" against the bare "Van Johnson" in
  each order, including family 'Van', given 'Johnson' under both
  family-first ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This PR respelled titled_ambiguous_particle_no_op_chain from "Dr. Van
Jr." to "Do Van Jr." so it would still reach the chain loop's j > k + 1
guard -- a plain title is transparent now, so the old spelling never
gets there. The row kept its default `parity` classification through
the respelling, and the new text does not hold parity:

    1.4.0        first='Do Van'  last='Jr.'
    this branch  title='Do'  given='Van'  suffix='Jr.'

cases.py's own header says changing a row means changing its
classification, and the respelling is this PR's doing even though the
divergence is not: 'do' being a title and 'Jr.' routing to suffix are
both 2.0-era, so neither belongs to #367. Classified `fix` rather than
a specific slug because there are two independent causes, and the note
now records what they are so the next reader does not have to
re-measure 1.4.0 to find out.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A title displaces a particle out of leading position, so Dr. Van Johnson reads as one surname

1 participant