fix(post_rules): H1 counts name words, not annotations (#410) - #444
Merged
Conversation
A title followed by one name word names the family, but the guard declined whenever a suffix, a nickname or a maiden name stood beside it, so "Dr. Smith née Jones" read given 'Smith' with no family where "Dr. Smith" reads family 'Smith'. None of the three is a name word: each stands beside the name rather than in it. The rule and its implementation move together -- H1 said "and nothing else", which the code read as "and no annotation either", and test_doc_citations couples the two. H1 now says name words, carries the maiden example that showed the gap, and names the rules it meets there; M2 points back at H1, which #410 asked for. corpus_rules.jsonl is generated from the doc's example lines, so it regenerates here to take the new ones in, and two recorded ledger effects move by exactly those names. The v1 suite already recorded the correct reading for the nickname half as a strict xfail; it passes now and the marker goes. The corpus-wide maiden-clause invariant loses the shape it was skipping and gains M2's own boundary in its place. N3 moves with it: "'Smitty' Dr. Jones" declines N3's one-piece count as it always did, and H1 now names the family behind it, so the rule and its decision entry stop claiming an empty family there. Closes #410 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One rule each at 2.0.0 and 2.1.0, keyed on the names by literal rather than on a fields-only shape: "a title, one name word, and an annotation" is a parse tree, not a string, and a fields-only rule would classify every future given/family swap in the corpus. 1.4.0 takes TWO rules. Three of the names move given/family there as they do at the later baselines, but 'Dr. Smith née Jones' moves four fields -- that baseline has no maiden support, so the marker sat in `middle` and the birth surname in `family` -- and one rule carrying the union of those fields would let the other three move a middle or a maiden name unnoticed. At the 1.4.0 baseline this also takes three of them off fix(suffix-routing), which was absorbing them silently -- its heading went 14 -> 17 while _CORPUS_CLAIMS, recording the whole corpus for a rule with no name_regex, could not see the growth. Same surgery as #372's names, on the same catch-all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
H1 gains the decision entry it never had -- its rationale is doing normative work now, so the argument needs a durable home and a history: pointer like every other rule. #410 leaves Open (M2), and the two places that cited its lone-residual shape as a standing boundary stop describing behavior that no longer exists. The entry is explicit that the rationale is stated for H1 alone: N3 counts a suffix the other way, and which reading is right for a nickname-led name is not decided here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five corpus names change reading, not four: `Freiherr von Richthofen V` moves too, and it never surfaced because the existing fix(#424) rule's fields are a superset of its narrowed diff -- a real movement absorbed by a green gate. It is also the suffix flavor, which three ledger comments claimed no corpus carries. N3's new sentence swept in the given-name titles it should have carved out (`'Smitty' Sir John` names no family), the release log contradicted itself inside one unreleased section, and H1's known gap -- the statement counts name words, the guard tests which roles are unoccupied -- is now on record rather than implied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #444 +/- ##
==========================================
- Coverage 98.60% 98.60% -0.01%
==========================================
Files 45 45
Lines 3091 3090 -1
==========================================
- Hits 3048 3047 -1
Misses 43 43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ew round The skip predicate is reverted: widening it with `nickname` was backwards. A nickname-only name belongs in the skip class, not out of it -- '(Bud) née Jones' reads given 'née', family 'Jones' and no maiden, M2 having left the marker a word -- so the widening would have sent such a name to an assertion it must fail. `maiden` was worse than wrong: the parametrization filters out every name carrying a marker, so the term could never fire. The nickname case is documented in the docstring, where it belongs, instead. fix(#424)'s `fields` is narrowed to drop `given`, closing the absorber the previous commit only described. Since #410 that name's diff is {family, suffix}; the declaration kept claiming {given, family, suffix}, and a subset match let the diff shrink underneath it without a word from any gate. All three baselines stay at exit 0. The rest is prose the reviewers caught: two ledger comments claimed the suffix flavor appears in no corpus, the known-gap paragraph counted tokens where it meant units, H1 was missing from the module docstring's history list and the guard comment had no pointer to the gap recorded against it, and the release log's "one of them reaches the default name order" framing is retired rather than incremented -- nearly all of this cycle's fixes reach it. Two stale counts beside machine-checked twins are recounted: 34 -> 46 and 47 -> 63 (with 11 -> 9 alongside it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four independent mutations of the widened guard survived the whole suite: declining on any comma, on a second maiden token, on a second suffix token, and on a nickname and a suffix together. The rule fires across far more shapes than the four single-annotation rows pinned it at, and each mutation is a plausible mis-rewrite of it. "Dr. King, Jr." is the one that matters most. It is the most ordinary real-world shape the rule touches, and nothing in the suite could see the class: the corpus-wide maiden-clause property test filters commas out of its parametrization, and "Smith, Dr." takes its family from the comma rule rather than from H1. 1.4.0 had the same empty family, so the row records a v1 bug fixed. The other four cover the arities and combinations the single- annotation rows cannot reach, including the given-name carve-out at a two-word maiden clause -- the rule now says "whatever maiden name stands beside it", and the "whatever" was asserted at one word only. Two smaller repairs: tests/test_titles.py lost its assertion that `first` is empty when the expectation moved to `last`, and the carve-out row's note claimed `given` without saying that this is a default-order claim -- under either family-first order that name reads family 'John', H1 being a no-op once assign has placed the word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ragraph form The #410 entries came in hard-wrapped at ~70 columns; every other entry in this file is one paragraph on one line, its neighbours running 728 and 5,211 characters. Pure reflow: no rewording, no re-paragraphing, and the doc parsers see the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
to SavantEnvs/python-nameparser
that referenced
this pull request
Aug 29, 2026
Six corpus names change reading under rules.md#M3 against 2.1.0 and
2.0.0, five against 1.4.0. One rule per ledger, keyed on the names by
literal: the shape is "a bracketed clause whose first word is a
marker", which is a question about the parse and not about the string,
so a fields-only rule standing in for it would pre-excuse every future
nickname/maiden confusion in the corpus.
`fields` is exactly what each baseline's diff moves. Against 2.1.0 all
six move {nickname, maiden}. Against 2.0.0 the five Latin names move
that pair while the CJK name also flips given/family, so that ledger
gets two rules rather than one declaring the union -- a union would
have let a given/family regression on any Latin name pass unreported,
which is what PR derek73#444 found behind fix(derek73#424) and had to narrow.
Against 1.4.0 the CJK name is absent, and that is the interesting
half. Its diff there is now {given, middle, family, maiden}: 1.4 read
the fullwidth brackets as name text, so no nickname changes hands, and
fix(cjk-maiden-marker) already describes exactly that diff and takes
the name. fix(cjk-fullwidth-paren-nickname), whose only name it was,
is declared dormant rather than deleted -- the reasoning it carries
about its `fields` boundary is recorded nowhere else, and the
declaration is load bearing in reverse.
_carries() could not see the markers in question: it strips a token's
abbreviating period but not its bracket, so 'Anna Müller (geb.
Schmidt)' read as carrying no maiden vocabulary and the presence guard
refused a rule that was right. Stripping delimiter characters only
ever finds markers that are really there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
to SavantEnvs/python-nameparser
that referenced
this pull request
Aug 29, 2026
rules.md. The M Background gains Polish and the fact a marker need not
be one word. Three statements were wrong for a phrase and are fixed,
each with its citing code comment in this same commit
(test_doc_citations quotes them verbatim):
M1 "a leading recognized marker word inside a multi-word clause
being dropped; a one-word clause keeps its word" -- a phrase
makes '(z domu)' a multi-word clause whose marker is NOT
dropped. Now keyed on a word standing past the marker, which is
what the code has always tested.
M2 gains a sentence: a marker recognized only whole, its own first
word standing alone being an ordinary name word.
M3 "opens with a recognized marker word" -> "a recognized marker".
Four examples land with them and so enter the rules corpus. Two move:
'Maria Kowalska z domu Nowak' (middle/family/maiden) and 'Maria
Kowalska (z domu Nowak)' (nickname/maiden), at all three baselines,
each with its own ledger rule declaring exactly its own fields -- not
the union, which is derek73#444's post-mortem, and not an alternative inside
the fix(derek73#335) rule, which this is not: M3's reach alone leaves the
bracketed name a nickname, and the phrase entry is what moves it.
'Anna z Nowak' and 'Maria Kowalska (z domu)' are parity and move
nothing. Gates green at 2.1.0, 2.0.0 and 1.4.0.
Two guards had the same blind spot the change itself had, and both are
real: they asked "is any WORD of this name a marker", which no phrase
entry can answer yes to.
tests/v2/test_parser.py's clause-free corpus filter let 'Maria
Kowalska z domu Nowak' through, so the appended clause was not the
only variable and the invariant failed. It asks the predicate now;
the recount is in its comment (642 names, 632 after the delimiter
strip, 629 once the predicate decides).
test_ledger_guards._carries said the same name carries no maiden
vocabulary, which would have refused the ledger rule above. It
matches consecutive token RUNS up to the longest entry now, which
is the single tokens themselves for any vocabulary of words, so
every count in that module is unchanged.
Re-recorded, each because the corpus grew and not because a rule did:
fix(suffix-routing)'s whole-corpus claim (1080 -> 1084), the
parenthesized-clause exclusion (51 -> 53 captures, absorbed_by still
empty), and the two new derek73#434 claims.
decisions.md, under M2 and unwrapped: why a run predicate was rejected
(z and domu are markers in no vocabulary, so no run can be identified
before the lookup, where 'lt' and 'col' each are); that
given_name_titles is the storage precedent and not the matching one,
and that no shipped set held a multi-word entry before this; C-i
answered at the phrase level, with the roz removal as the contrast --
a phrase changes the unit that CLAIMS rather than passing the test
with the old one, and roz has no longer unit to move onto; the four
sites, the contract test, and the preserved whitespace/token
divergence with the row that pins it; the commit ordering, including
that filterwarnings = ["error"] is what makes the warning carve-out
load-bearing rather than tidy; and the measured 6.7% -> 2.2% cost with
its method.
Swept for the rest of the single-word claim rather than stopping at
the two files already fixed: docs/customize.rst (the caller-facing
statement, now naming both exceptions and how they differ),
rules.md's S Background, decisions.md's Excluded (multi-word) header,
_policy.maiden_delimiters' field doc, tools/differential/README.md's
vocabulary-coverage count and its recompute one-liner (4 of 16 -> 5 of
17, counting runs), and docs/design/AGENTS.md's derek73#291 anecdote, whose
premise was true when it was written.
Release log: what works, that the previously advised workaround was
wrong and what it produced, and the migration note to remove z and
domu if they were added separately. Every value in it measured on this
commit, on the 1.4.0 wheel and on the 2.1.0 wheel; the roz bullet's
"now 16 entries" became "this removal takes the default set to 16" so
the two bullets do not contradict inside one release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
added a commit
that referenced
this pull request
Sep 1, 2026
Derek: "could we change it to Jean" -- `de Mesnil` is French and `Juan` is Spanish, so the pair read as fuzz rather than as a name. rules.md already uses `Ménil Christophe de` for the same shape, so `Jean` brings the odd one out into line. Renamed everywhere rather than only in the docs he was reading, because the alternative is user docs and contributor docs disagreeing about the same example: rules.md (which GENERATES corpus_rules.jsonl), decisions.md, customize.rst, the release note, cases.py, and four code comments. Two expected VALUES had to move with the inputs, and neither was caught by the rename itself -- `given="Juan"` is not the string `de Mesnil Juan`, so a search-and-replace on the input leaves the assertion behind and the example silently asserts the old answer. rules.md#P1's example line and cases.py's family_comma_no_name_word_family_first row both failed on exactly that, which is what those tests are for. _CORPUS_CLAIMS records a digest per rule, so the two 1.4.0 rules holding this name needed re-recording: names 280 -> 280 and roles unchanged in both, only the digest moving, which is what a pure rename should look like. A count that GREW would have meant something was absorbed. Verified the way the #444 lesson asks -- not just that the gate is green but that nothing MOVED under it: 1104 corpus names and 226 / 198 / 106 intentional diffs at 1.4.0 / 2.0.0 / 2.1.0, unexplained 0, the same 66 / 59 / 52 headings as before the rename, and the 1.4.0 per-heading counts byte-identical. 6197 tests, 244 doctests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #410.
Dr. Smithreads familySmith;Dr. Smith née Jonesread givenSmithwith no family. H1's "and nothing else" guard counted a suffix, a nickname or a maiden name as a further name word, and all three suppressed the rule — one term in one guard, so the term went rather than the maiden role being special-cased.Dr. Smith née Jonesfirst='Smith'middle='née'last='Jones'given='Smith'family=''family='Smith'maiden='Jones'Dr. Smith PhDfirst='Smith'last='PhD'given='Smith'family=''family='Smith'suffix='PhD'Dr. "Smitty" Smithfirst='Smith'last=''given='Smith'family=''family='Smith'Sir John née Jonesfirst='John'middle='née'given='John'family=''Five corpus names change reading. Four arrive as new diffs and are classified in all three ledgers. The fifth,
Freiherr von Richthofen V, was already claimed by thefix(#424)rule: this change narrows its diff from{given, family, suffix}to{family, suffix}, and sinceclassify()accepts a rule whose declaredfieldsare a superset of the observed diff, a shrinking diff kept matching and no run ever named the movement. That declaration is now narrowed to match, so the nextgivenregression on that name surfaces instead of being absorbed.At the 1.4.0 baseline three of the four were being absorbed by the fields-only
fix(suffix-routing)catch-all (heading 14 → 17), invisible to_CORPUS_CLAIMSbecause that constant records the whole corpus for a rule with noname_regex. They have their own rules now, as #372's names got one off the same catch-all.N3 moves without changing:
'Smitty' Dr. Jonesdeclines its one-piece count as always, and H1 then names the family. Its Accepted clause dropped a half that is no longer true and gained two executable examples, including'Smitty' Sir John, where a given-name title means no family is named after all.H1 also gains the
decisions.mdentry it never had, including one thing left unfixed on purpose: the statement says "exactly one name word", but the guard tests which roles are unoccupied and retags every given token, soFreiherr de V Jryields familyde V. No corpus name or example reaches it; recorded rather than repaired, since this change is about which roles disqualify the rule, not how it counts.The v1 suite already shipped the correct reading for the nickname shape as a strict xfail (
tests/test_nicknames.py::test_nickname_and_last_name_with_title); it passes now and the marker is gone.Review rounds. Six in total (spec compliance, design-docs ×2, full-branch code review, then comment / test-coverage / code review on the open PR). No Critical code findings at any point — the behavior change reproduces as exactly the intended
given→familymove across 16,000 parses and six policies. What they found was prose, and mutation gaps: four independent guard mutations survived all 5,819 tests, the worst being that H1 could be made to decline on every comma-bearing name unnoticed, because the one broad property test filters commas out of its corpus. Fivecases.pyrows now pin those shapes —Dr. King, Jr.among them, which 1.4.0 also read with an empty family — and each kills exactly one mutation.Verification: suite 5844 passed;
compare.pyexits 0 at 1.4.0, 2.0.0 and 2.1.0; mypy and ruff clean. Three review rounds (spec compliance, design-docs ×2, full-branch code review) found 16 issues, all in prose, all fixed — the code survived a 16,000-parse sweep across six policies with every diff being exactly the intendedgiven→familymove.🤖 Generated with Claude Code