Skip to content

fix(#533): a maiden clause ends at a trailing credential, and reports either way - #534

Merged
derek73 merged 5 commits into
masterfrom
claude/533-maiden-clause-credential
Sep 20, 2026
Merged

derek73 merged 5 commits into
masterfrom
claude/533-maiden-clause-credential

Conversation

@derek73

@derek73 derek73 commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Closes #533.

What changes

A maiden marker's clause now ends before a trailing member of the ambiguous credential class (ba, do, ed, jd, ma, dotted acronyms, and all-caps unlisted words under unlisted_caps_suffixes) where the reader that will read the name left behind takes that word as a credential — and the word reports suffix-or-name whichever way it reads. After #531 this was the last trailing position where the class was read in silence.

Input Before After
Jane Doe nee Smith MA / Doe, Jane nee Smith MA maiden Smith MA, silent maiden Smith, suffix MA, reports (1.4.0 read the suffix)
JANE DOE NEE SMITH MA / jane doe nee smith ma maiden, silent suffix, reports
Jane Doe nee Smith Ma / Jane Doe nee Yo-Yo Ma maiden, silent unchanged, reports
Jane Doe nee Smith MA PhD maiden Smith MA, suffix PhD maiden Smith, suffix MA PhD (now agrees with PhD MA)
John née Jones Smith Ma (the corpus name #424 protected) maiden Jones Smith Ma unchanged, reports
John Smith nee Jones R.A.I. maiden Jones R.A.I. suffix R.A.I., reports — a restoration: 2.3.0 read the suffix and this unreleased cycle (#516) had broken it
Jane Doe nee MA / nee MA PhD maiden MA, silent unchanged, reports
Doe, Jane nee Smith do / Do maiden unchanged, reports
Doe, Jane nee Smith DO maiden Smith DO maiden Smith, suffix DO
Jane Doe nee MA Smith, nee Smith V, nee Smith PhD, (nee Smith) MA unchanged

Design calls made with Derek

  • One rule, with John van der Berg V reads family van der Berg V — the particle chain and the maiden walk take a trailing numeral as a name word #424's double check extended to the acronym. John van der Berg V reads family van der Berg V — the particle chain and the maiden walk take a trailing numeral as a name word #424 left the acronym out because the rule counts words the take removes (John née Jones Smith Ma → family Ma). The check had to be stronger than the numeral's: it asks whether the reader takes that word, not whether it takes anything (JOHN NEE JONES SMITH MA PHD would otherwise make MA the family name).
  • A lone word after the marker stays the maiden name (Jane Doe nee MA), on purpose unlike née V / née PhD, where certain suffix vocabulary makes the marker decline. Implemented as a clamp: the stop never takes the first word after the marker (Doe, J. nee MA ba → maiden MA, suffix ba).
  • The second check asks the reader that will actually read what is left. No comma: the ordinary trailing rule. After a family comma: Should Doe, John MA read MA as a credential, as 1.4.0 did, or at least report the fork? #531's given-part slot, including its floor and the do carve-out (Doe, Jane nee Smith MA do keeps MA do). That reading now lives in one shared predicate, _pieces.credential_at_the_given_slot, at a recorded cost of one frame per member asked (Doe, John MA 310 → 311). A clause in the family segment or a third comma part has no reader and is untouched.
  • Accepted: the one-case-head exception (DOE, JANE nee Smith Ma reads the credential where DOE, JANE Ma keeps the name — the case fact is computed from the person's own words; 114 of 2,016 generated pairs, carried by the agreement test as a counted structural allowlist); trailing titles are not transparent inside a clause (nee Smith MA Prof. vs nee Smith Prof. MA — follow-up drafted); one-case records with a two-word birth surname (JANE DOE NEE YO-YO MA → suffix MA, as JANE YO-YO MA reads).

The invariant (from the PR review round): every word the clause gives up ends the parse in suffix; otherwise the clause keeps it, and reports. The first version asked #531's slot even where the take removed the only name word that slot depends on, so Doe, Prof. nee Smith A.B. read given A.B. and Jane Doe, Jr nee Smith ma read given Jr, silently; and a released DO could be swallowed by a particle chain (Berg, Jane van der nee Smith DO). All of these now keep their maiden words, as master reads them, and report. That also retires the two silent movers an earlier draft accepted (Doe, Dr. nee Smith MA, Jane Doe, Jr nee Smith MA) and Berg, abdul nee Jones MA. A property test over 8,466 generated parses pins the invariant (790 failures on the pre-fix commit, 0 now); a 855,108-parse sweep finds 0 silent movers.

Delimited clauses are settled by their delimiters (Derek's ruling): Jane Doe (nee Smith MA) keeps the whole span as the maiden name and reports nothing, the writer having drawn the boundary; Jane Doe (nee Smith) MA reads suffix MA and reports. Unchanged behavior, now documented and pinned.

Commits

  1. fix(#533)_maiden_take's stop, the reader selection, the shared predicate (proved behavior-neutral by oracle before the behavior change), a report channel of its own in group, 46 case rows + 1 flipped, unit and property tests, example lines, corpora and claims.

  2. test(#533) — ledgers at five baselines, each diff attributed by measuring against the parent: pre-existing readings go under the change that caused them, not fix(#533). A (?i) 1.4.0 regex was silently claiming a name that now reads the opposite way; narrowed.

  3. docs(#533) — rules.md M2/S2/S3, decisions.md (### M2 amending the John van der Berg V reads family van der Berg V — the particle chain and the maiden walk take a trailing numeral as a name word #424 bullet without editing it; ### S2), a mechanisms field note on that ledger trap, release log, customize.rst.

  4. fix(#533): review round — the invariant above with its two view conditions; assert_never on the reader, the structure-to-reader mapping pinned by a test, required keyword parameters; two structurally dead branches removed and one kept as a measured-inert guard with a reproducible control; the agreement test now pins the exception SET by digest over 3 markers × 3 policies (1,026 of 18,144); the double-report test now actually produces the maiden + peel pair (0 → 1,944 parses); a maiden_clause benchmark shape guards linearity (4.1 clean vs 12.4 on a per-piece rebuild).

Verification

  • Suite 9351 passed, 324 skipped, 4 xfailed; mypy, ruff clean; sphinx doctest 253 / 0; coverage 100% on the four touched modules.
  • Frames: parse 412.00 / facade 449.00; every name without a marker +0 except the decided +1 per member at the given slot; maiden names that do not trigger are one frame cheaper. Linear to 400 words.
  • Independent review of commit 1: 829,740-parse oracle, 0 diffs on marker-less names; no released word lands outside suffix except where the clause-less control reads the same; no token reported twice; 10 of 12 mutants killed, the two survivors kept as measured-inert guards with their negative controls recorded.
  • Agreement sweep (clause vs clause-less spelling, 2,016 pairs): parent 984 failing; now 0 outside the counted allowlist.
  • Differential gates, all 0 unexplained: 1.4.0 1312 names / 516 intentional; 2.0.0 1319 / 463; 2.1.0 1319 / 375; 2.2.0 1319 / 228; 2.3.0 1319 / 113.
  • Blast radius on the corpus as it stood before this branch (1,264 names × six policies): 0 role movers, 1 report-only (John née Jones Smith Ma).
  • Design-docs review of commit 3 folded in; a five-reviewer toolkit round (code, tests, comments, silent readings, types) produced commit 4.

🤖 Generated with Claude Code

derek73 and others added 3 commits September 19, 2026 17:06
A maiden marker took every word after it up to the first suffix WORD
or the trailing numeral, and the ambiguous credential class was not
among the words that stopped it -- so the last trailing position in
the library where a member of that class was read in silence was the
end of a maiden clause. `parse("Jane Doe nee Smith MA")` gave maiden
'Smith MA' and reported nothing, where `John Smith MA` gives suffix
'MA' and reports. It was order-sensitive besides: `nee Smith MA PhD`
gave maiden 'Smith MA' while `nee Smith PhD MA` gave maiden 'Smith',
so whether the word was read at all depended on which side of the
unambiguous credential the writer put it.

The words a marker takes now end where a trailing credential begins,
where the rule that reads the name left standing reads the word as
one -- both as the name is written and as the take would leave it,
which is the same double question the trailing roman numeral is asked
and for the same reason: the count of words to spare includes the very
words the marker removes. Which rule does the reading depends on where
the clause stands. With no comma it is the trailing peel; after a
family comma it is the reading the end of the given part takes, where
the comma has already settled the count and the writing decides alone;
before that comma, and past a second one, no trailing rule reads those
words at all and the clause keeps them.

A member that is the ONLY word after the marker stays the maiden name,
whatever its writing says: the marker announces a name, and the rule
gives a word up only where a maiden name is left standing. That is a
clamp rather than a veto -- `Doe, J. nee MA ba` keeps maiden 'MA' and
reads suffix 'ba'.

Either reading is reported, on a channel of the walk's own: group
suppresses its particle-chain emitter after a family comma, and a
credential ending a clause is not that fork.

One walk, one peel: the numeral reading stops going through
`trailing_start` and calls the pair it wrapped, so the acronym half
needs no second peel and a maiden name that does not trigger got one
frame cheaper. #531's reading moves into
`_pieces.credential_at_the_given_slot`, shared by both callers rather
than spelled twice; measured over 12,630 parses in two processes, the
refactor is byte-identical, and it costs one frame on a family-comma
name carrying a class member.

Over every corpus name under six policies the change moves 0 roles and
adds 1 report. The agreement sweep it ships -- 2016 pairs of a name
with a clause against the same name without one -- went from 984
disagreements to 0 outside the one-case-head class, whose 114 members
are the accepted cost of `own_words` stopping at the marker.

The rows this adds put three names inside both `fix(#274)` and
`fix(#379)` in the 1.4.0 ledger, whose fields nest, so file order
alone would pick the winner and
test_every_order_decided_contest_is_declared goes red. The earlier
rule carries a precedes_narrower block saying why it wins, and the
negative control beside it records the contest either way; the
remaining ledger work is its own commit.

Review fixes, comments and tests only: `listed_lean`'s frame note
names its third caller, SUFFIX_OR_NAME's silent positions count four,
both maiden guards carry the negative controls they were measured
against, the shared predicate costs one frame PER MEMBER, the
`one_case` denominator is the recipe's own (10,740), and the span
property test compares spans -- with a count that keeps it from
being vacuous.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Written from the gate's own output rather than predicted beside the
change. Over the corpus AS IT STOOD, #533 moves ONE name -- `John
née Jones Smith Ma`, which keeps every field and gains a report -- so
at 2.0.0 and 2.1.0 the fix(#445) rule that already carried it gains
`_ambiguities` and nothing else, and at 2.2.0 and 2.3.0 it joins the
report-only rule below. Everything else here is about the 29 corpus
names this change's own rows admit.

Those 29 are not all this change's, and each was measured at the
parent 2f57ff2 as well as at its baseline to find out. Where the
tree reads a name exactly as it read before #533, the rule names the
change that DOES move it: fix(#531) gains `Doe, J. ba` and `Smith nee
Jones, Jane MA`, fix(#436/#437) gains `Jane Doe nee Smith PhD MA` at
the three 2.x baselines that still render its run with a comma, a
fix(#424) rule carries `Jane Doe nee Smith V` at 2.0.0 and 2.1.0, and
1.4.0 gains two rules named for #274 -- one for the six names whose
clause keeps a credential v1 read as a post-nominal, one for the name
whose clause an UNAMBIGUOUS credential had already ended. A
`fix(#533)` label on any of them would attribute a released reading
to this change.

One existing rule needed narrowing rather than widening. 1.4.0's
`fix(#424/#445) accepted: the maiden walk keeps a bare acronym` was
(?i)-anchored, so it reached `John née Jones Smith MA` as well as the
Title-cased spelling -- and that name now does the opposite of what
the rule says, the clause giving the MA up. The anchor is narrowed to
the two spellings whose reading it describes, `JOHN NEE JONES SMITH
MA PHD` joins it, and the caps-MA spelling gets a compound rule
naming #445 and #533 together.

The #533 rules themselves split by what they SAY rather than by field
set: the clause giving a credential up; the clause keeping one and
reporting it; the dotted by-shape spelling, which leaves v1 rather
than restoring anything and is recorded as the accepted cost it is;
the marker phrase; and the two names where the released member lands
somewhere other than the trailing peel, silently, so their rule
declares no `_ambiguities` to absorb a report with.

At 1.4.0 the change's rows also create an order-decided contest,
declared in commit 1 with a `precedes_narrower` block on the wider
rule. Its `why` said "the whole of the diff is the marker leaving the
name", which two of its three names falsify -- both also empty the
`suffix` v1 read, which is in neither rule's fields -- so the block
now says what the contest is over and where those two actually land.

One name is a RESTORATION and its rule says so: `John Smith nee Jones
R.A.I.` read suffix `R.A.I.` at 2.3.0, moved into the maiden name
during this unreleased cycle when #516 retagged the token out of the
certain-suffix class, and reads 2.3.0's way again here. It was in no
corpus file, so no gate had ever compared it.

The one radar name this change intended is classified rather than
left on the radar unread, the way #531's caseless rule was:
`田中 太郎 旧姓 佐藤 MA` at 2.1.0, 2.2.0 and 2.3.0, the marker's own
rule already carrying it at 1.4.0 and 2.0.0. Radar unclassified goes
0/5/7/8/8 to 0/5/6/7/7.

All five gates exit 0 with `unexplained: 0`:

  1.4.0  corpus 1304 names; intentional diffs 508
  2.0.0  corpus 1311 names; intentional diffs 455
  2.1.0  corpus 1311 names; intentional diffs 367
  2.2.0  corpus 1311 names; intentional diffs 223
  2.3.0  corpus 1311 names; intentional diffs 108

No parser, case row or corpus file is touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rules.md#M2's statement gains the reader and the reason behind the
credential stop commit 1 put in it: the double check stated once for
both forks -- the count of words to spare includes the very words the
marker removes -- and the reader named, the trailing rule where a
trailing rule reads the part (no comma, and before a SUFFIX comma,
measured to read the same way), the end of the given part after a
family comma, nobody before that comma or past a second one. The
lone-word floor is stated as the deliberate divergence it is from the
`née V` / `née PhD` precedent, with the reason: that class carries no
evidence of which it is. Reporting is stated at the boundary the tree
actually holds -- a member ENDING a clause some rule reads is reported
where the clause KEEPS it, while one it gives up reports only where
the position it lands in reports, which `Doe, Dr. nee Smith MA` and
`Berg, abdul nee Jones MA` would have falsified as a universal. #S2
and #S3 gain the slot in their enumerations; #P6 needs no change, the
`do` reading here being #531's unchanged.

A second Accepted row records the trailing-title boundary, where the
two spellings disagree. Its two example lines were already corpus
names, so corpus_rules.jsonl grows by two and no ledger claim moves.

decisions.md gets one bullet under M2 amending the 2026-08-22 #424
entry -- which is left exactly as it landed -- and one under S2. The
first records the double check, the reader table, the clamp and the
two names a veto got wrong, the `do` pair and the accepted costs; it
also re-measures the `one_case` plumbing those sites carry, which the
2026-09-18 entry recorded as inert under the numeral-only reading.
The pair is the finding: 0 over the corpus as it stood, 36 on six
names over the corpus with this change's own rows, the plumbing live
either way. The second records the shared predicate, its frame price,
and the one-case-head exception as the M2 instance of #492's
deferred question.

The release log gains one bullet. One correction in place: the #530
bullet said "the one slot this bullet left silent", which two bullets
now close rather than one. docs/customize.rst's two 2.4 switch entries
enumerate the positions each switch reaches, so both gain this slot.

Three sweep findings beyond the plan's table, each a claim no test
reads. The 1.4.0 ledger's fix(#424/#445) prose quoted the superseded
Accepted row and said the fork is left to assign; it now says the
WRITING declines the word. test_parser.py's "the walk takes the
numeral only" is false of the tree, and the row under it is really
about the member not being TRAILING, so it says that and gains the
mirror case. And the judged-token sentence test_properties.py
attributes to mechanisms.md is decisions.md#S2's, while the P3
sentence the same comment leans on says something P3 does not say --
both corrected to what those documents carry.

mechanisms.md gains a field note for the trap commit 2 hit: a ledger
rule EXPLAINS a diff and nothing checks that its sentence still
DESCRIBES it, so a too-wide anchor kept `unexplained: 0` green over a
name that had started reading the opposite way. Attribution against
the parent found it; the gate could not.

Review round: the design-docs review found a MOVER that reports
nothing -- `Jane Doe, Jr nee Smith MA`, where the take leaves a
post-nominals-only segment that is read whole -- which is the
pre-existing no-name-word-in-front silence, written as a title's
and reached by a post-nominal too; stated in M2 and S2, widened in
_types.py, added to the M2 bullet and the release note, and pinned
by a unit test rather than an example line, the name differing from
this tree at all five baselines for reasons mostly not this
change's. M2 and P6 now cite each other. The two _group.py
denominators are re-measured: 36 of 10,752 (1,792 names) and 0 over
21,504, the latter with the probe shown live (site reached 394
times, tag admits 310).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@derek73 derek73 added this to the 2.4 milestone Sep 20, 2026
@derek73 derek73 added bug enhancement docs Documentation fixes and updates labels Sep 20, 2026
@derek73 derek73 self-assigned this Sep 20, 2026
@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.76%. Comparing base (2f57ff2) to head (d5310b3).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #534      +/-   ##
==========================================
+ Coverage   98.74%   98.76%   +0.01%     
==========================================
  Files          45       45              
  Lines        3509     3554      +45     
==========================================
+ Hits         3465     3510      +45     
  Misses         44       44              

☔ 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.

derek73 and others added 2 commits September 19, 2026 20:19
…use keeps it

A. The clause could give a word up "as a credential" and the word then
   landed in `given`, `middle` or `family` instead -- 1,446 violating
   parses of 855,108 at d97d3eb, 0 at the parent 2f57ff2, 0 here. The
   stop now requires the release to survive: GIVEN_SLOT needs a name
   word ahead of the member in the view it would leave, and either
   reader withdraws where a join below the marker pass would take the
   word (P2's chain, P5's bound-given join). `Doe, Prof. nee Smith
   A.B.`, `Jane Doe, Jr nee Smith MA`, `Doe, Dr. nee Smith MA`,
   `Berg, abdul nee Jones MA`, `Berg, Jane van der nee Smith DO` and
   `Jane Doe nee Smith DO DO` read as 2f57ff2 read them, and report.
   Pinned by a new property over an 8,466-parse grid (790 failures at
   d97d3eb). Silent movers vs the parent: 4,100 tokens in 59 shapes
   before, 0 now.
B. Delimited clauses keep the whole span and report nothing, unchanged
   -- documented in rules.md#M2, _types.py and three case rows.
C. assert_never on the reader (with a test that reaches it and pins
   len(TailReader) == 3 and group()'s mapping), required keyword-only
   `reader`/`maiden_ambiguities`, `at = left.index(stop)`, a frame-free
   membership assert in `credential_at_the_given_slot`, and MaidenIndices
   named beside MaidenTake.
D. `len(head) == 1` fires 1,440 times in a population that holds the
   Ph. D. shape (the recorded 0 was a population artefact) and is kept:
   byte-identical over 905,796 parses but +15 frames on
   `BERG, ABDUL Z DOMU MA PH. D.`. `stop < trailing` and the emitter's
   `len(last) == 1` are REMOVED -- structural, 0 hits over 1,760,904
   parses, byte-identical, frame-neutral. Comment fixes: the sibling
   site is `segment_suffix_reading`, `peeled.names` indexes `rest`,
   OPTION 1 is THE FIRST-WORD FLOOR, the floor's example now bears a
   clause.
E. Agreement test widened to three markers x three policies (1,026 of
   18,144, exactly 9x, digest-pinned as a SET); the span test gains
   two-member tails so the maiden emitter and assign's peel actually
   pair (1,944 parses, 0 before); a `maiden_clause` shape guards M2's
   view (4.05-4.12 clean, 12.2-12.6 on a per-piece rebuild); four
   missing rows added. The bare delimiter core ending a clause predates
   this change and is recorded in decisions.md rather than fixed.

Suite 9351/324/4, mypy and ruff clean, call_count 412.00/449.00,
`Doe, John MA` 311 and marker-less names +0, coverage 100% on the three
modules touched, and all five differential gates end 0 unexplained.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No behavior change: every reading over 222,208 names (the corpora, the
case rows and 220,686 generated maiden-clause names) under six
policies -- 1,333,248 rows, plus the facade -- is byte-identical to
b45707a, and the reference frame counts hold at 412 / 449.

- _pieces: `trailing_start` loses its `numeral_only` flag. It had one
  caller, the maiden walk's numeral re-ask, which now asks the
  `peel_walk` + `peel_trailing` pair and reads `.numeral`, as the
  walk's first numeral read already did. One frame cheaper on
  'John nee Jones Smith V' (371 -> 370); no probe rose.
- _group: the maiden emitter binds its token once.
- Comments in _group, _pieces and _assign compressed to what the code
  does, the measurement and the reason; every measurement, population
  and price kept, the narration of earlier drafts removed.

Rejected by measurement: a closure for the left / view / view_tags
triple costs a frame on 5 of 12 probes, and the two views are never
the same list (they differed 30 of 30 where both run), so there is no
"compute once".

mechanisms.md#ONE-PREDICATE-PER-QUESTION said `peel_trailing` has no
caller outside `_pieces.py`; #533 gave it one, the maiden walk, and
the entry now says why that is a reader of the peel rather than a
second spelling of it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@derek73
derek73 merged commit 4665175 into master Sep 20, 2026
11 checks passed
@derek73
derek73 deleted the claude/533-maiden-clause-credential branch September 20, 2026 05:08
derek73 added a commit to IMGillusion/python-nameparser that referenced this pull request Sep 22, 2026
…nd the ph removal is on the record

Maintainer fix-up on PR derek73#521 after the merge from master, the eight
items of the 2026-09-16 review comment:

- decisions.md R4: the reach is remeasured on the merged tree with
  the recipe's comparator named (master's `_cap_word` at 23e52dc),
  and the two earlier snapshots are explained rather than replaced --
  the `ph` removal took 13 forced movers back to `Ph. D.`, and the
  rows PRs derek73#530/derek73#532/derek73#534 added account for the rest of the drift.
  The exceptions map's job is stated as the spellings
  `str.capitalize()` gets wrong, not "non-all-caps" (II/III/IV are
  all-caps); the precedence over Mac/Mc and the role-not-tag reach
  of the gate are recorded.
- decisions.md: an `Excluded (SUFFIX_ACRONYMS — ph)` block beside
  esq's, so a wordlist sweep does not put the fragment back.
- rules.md R4: the non-acronym clause is qualified by the exceptions
  map (`john smith ii` -> `II`), the acronym repair's precedence over
  the Mac/Mc convention is stated and pinned by a `john smith mcse`
  row (`McSe` without it), and the Accepted paragraph names which
  clause it is about and states the acronym repair's contrast on a
  spliced suffix.
- release_log.rst: the bullet moves from the shipped 2.3.0 section
  to 2.4.0 with the current digits, and `ph` leaving the set gets
  its own bullet in the rai/cha shape.
- corpus_rules.jsonl regenerated (three new R4 rows).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug docs Documentation fixes and updates enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should Doe, Jane nee Smith MA read MA as a credential? The maiden name keeps it, where it gives up PhD and Jr.

1 participant