Skip to content

fix(post_rules): H1 counts name words, not annotations (#410) - #444

Merged
derek73 merged 7 commits into
masterfrom
fix/410-h1-name-words
Aug 26, 2026
Merged

fix(post_rules): H1 counts name words, not annotations (#410)#444
derek73 merged 7 commits into
masterfrom
fix/410-h1-name-words

Conversation

@derek73

@derek73 derek73 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Closes #410.

Dr. Smith reads family Smith; Dr. Smith née Jones read given Smith with 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.

input 1.4.0 master after
Dr. Smith née Jones first='Smith' middle='née' last='Jones' given='Smith' family='' family='Smith' maiden='Jones'
Dr. Smith PhD first='Smith' last='PhD' given='Smith' family='' family='Smith' suffix='PhD'
Dr. "Smitty" Smith first='Smith' last='' given='Smith' family='' family='Smith'
Sir John née Jones first='John' middle='née' given='John' family='' unchanged — given-name titles are carved out

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 the fix(#424) rule: this change narrows its diff from {given, family, suffix} to {family, suffix}, and since classify() accepts a rule whose declared fields are 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 next given regression 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_CLAIMS because that constant records the whole corpus for a rule with no name_regex. They have their own rules now, as #372's names got one off the same catch-all.

N3 moves without changing: 'Smitty' Dr. Jones declines 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.md entry 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, so Freiherr de V Jr yields family de 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 givenfamily move 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. Five cases.py rows 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.py exits 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 intended givenfamily move.

🤖 Generated with Claude Code

derek73 and others added 4 commits August 25, 2026 21:53
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

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.60%. Comparing base (b244c5a) to head (507a8f2).

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.
📢 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 3 commits August 25, 2026 23:35
…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>
@derek73
derek73 merged commit 2c05dbb into master Aug 26, 2026
11 checks passed
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.

Dr. Smith née Jones reports no family name — a maiden name suppresses the title-plus-one-word rule

1 participant