Skip to content

fix(post_rules): a maiden name makes the lone name word the family (#445) - #450

Merged
derek73 merged 3 commits into
masterfrom
fix/445-maiden-marked-lone-name
Aug 28, 2026
Merged

fix(post_rules): a maiden name makes the lone name word the family (#445)#450
derek73 merged 3 commits into
masterfrom
fix/445-maiden-marked-lone-name

Conversation

@derek73

@derek73 derek73 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Closes #445.

parse("Smith née Jones") reported given Smith with an empty family. A maiden marker announces a surname the bearer no longer uses, and that is only worth writing where there is a current one to tell it apart from — so the lone name word is the family.

Smith née Jones      before: given 'Smith', family ''      after: family 'Smith', maiden 'Jones'
Smith (née Jones)    before: given 'Smith'                 after: family 'Smith'   (restores 1.4.0)
Jane née Jones Smith before: given 'Jane'                  after: family 'Jane', maiden 'Jones Smith'

Two rules, because the convention was never written down

rules.md#O4 says "the first name word is the given name, the last is the family name" — with exactly one word, first is last, so O4 decides nothing at n=1 and the shipped behaviour was recorded nowhere.

  • O5 records it, and deliberately records it as a convention rather than a determination: with one name word there is no information either way, and the library fixes one reading so the same input always parses the same way. parse("Andrew") reports no ambiguity, but given-or-family is a guess #449 tracks reporting that as an ambiguity; O5 is worded so that change amends a rule which already describes the doubt rather than contradicting one that denied it.
  • M4 is the exception: a maiden name standing beside exactly one name word makes that word the family.

M4 is keyed on the maiden name, not the marker — the guard tests a Role.MAIDEN token, and M1's configured delimiter pair produces one with no marker anywhere (Smith (Jones) under maiden_delimiters → family Smith). Review caught the statement claiming otherwise.

Two carve-outs, neither invented

mechanisms.md#TWO-LAYER-ASSIGN: the vocabulary layer claims words for what they are, the positional layer takes the rest. M4 changes the positional default, so it does not reach a word already claimed:

abd née Jones          given 'abd'   vocab:bound-given claims it
J. née Jones Smith V   given 'J.'    an initial is not a family name

Both witnesses are corpus names rather than constructed cases, and each is the sole catcher of its own mutation. H1's given-name-title carve-out is untouched: Sir John née Jones keeps given John.

Blast radius

Nine of 1,090 corpus names change reading — measured by disabling the branch and re-parsing, not inferred from the gates, because the ninth is invisible to them: Smith (née Jones)'s family now agrees with every baseline again, so its diff shrank. Every one of the nine is exactly givenfamily with all six other fields standing still.

What review found

Three reviewers, and the sharpest finding was a silent absorber that survived the round meant to close it. The # revisit when #445 lands markers planted by #446 were answered by narrowing one ledger and asserting the other two needed nothing "because the baseline is what read given". Measured on the wheels, the baseline read family — the bracketed spelling has never read given on any release — so two ledgers were left declaring four fields over a two-field diff, which classify accepts silently. Found by instrumenting the comparator to print real diff-field sets, and now narrowed with the measurement recorded.

One limit is recorded rather than closed: at 1.4.0 a {given, family} regression on that name is still absorbed by the fields-only fix(suffix-routing) catch-all, which declares no name_regex. Long-standing and outside this change.

Also recorded rather than fixed: M4 counts GIVEN tokens where H1 counts nothing, so a joined name word declines where H1 fires (Dean of Chemistry née Jones). Widening moves zero corpus names but is a behaviour change outside this issue's scope.

Verification

Suite 6059; compare.py exits 0 at 1.4.0, 2.0.0 and 2.1.0 with zero unexplained; mypy, ruff and the Sphinx doctest build clean; every commit independently green at the suite and all three gates.

🤖 Generated with Claude Code

derek73 and others added 3 commits August 27, 2026 17:49
…me word the family

A maiden name announces a FORMER surname, which only means anything
beside a current one -- so where the clause has taken its name and one
name word is left, that word is the family name. `parse("Smith née
Jones")` reported given 'Smith' with an empty family; it now reports
family 'Smith', maiden 'Jones'.

Keyed on the maiden NAME rather than on a marker, which is what the
code does and what M1's caller-configured pair needs: `Smith (Jones)`
under `maiden_delimiters` carries no marker anywhere and reads family
'Smith' all the same. M2's take, M3's marker-led clause and M1's
declared pair announce the same thing, so the rule covers all three.

Written as H1's sibling in post_rules and placed under it, so the
interaction is decidable by reading: where H1 fired there is no GIVEN
left, and where it declined on a given-name title the `titles` guard
keeps this rule off the same word ("Sir John née Jones" is unchanged).
Sibling except in what it counts -- this guard counts GIVEN tokens
where H1 counts nothing -- so a joined name word declines here where
H1 fires. That gap is recorded, not closed: widening moves no corpus
name and is a behaviour change of its own.

Two carve-outs, both read off the tags classify already recorded
rather than a predicate of this rule's own, because the rule changes
what POSITION decided and must not reach what a word IS
(mechanisms.md#TWO-LAYER-ASSIGN): `vocab:bound-given` ("abd née
Jones") and `initial` ("J. née Jones Smith V"), each a corpus name and
each caught alone by its row under mutation.

Two rules recorded, not one. O4 is silent at exactly one name word --
first and last are the same word -- so the shipped reading had no
rule. O5 records it as the convention it is: a guess fixed in advance
so the same input reads the same way, not a determination, with H1, N3
and M4 named as the rules that decide instead and each one's carve-out
named with it. M4 is the exception this commit implements. The code
comments cite both verbatim, which is why the rules and the code land
together.

The differential lands here too, so the gates are green at this commit
rather than at the next one. Nine corpus names change reading; eight
arrive UNEXPLAINED at all three baselines and are classified with
field sets that are exactly what each name moves, splitting where the
diffs do:

- 1.4.0: seven move {given, middle, family, maiden}; 'John née Jones
  Smith Ma' moves those plus `suffix`.
- 2.0.0/2.1.0: six move {given, family} alone, the marker consumption
  being the baseline's own reading already.
- Two carry compound diffs and get rules naming both causes. The #424
  rules are renamed to fix(#424/#445) rather than widened, since one
  rule explains a name's whole diff; #412's rule keeps its other name
  at its own three fields, so nothing pre-excuses a `given` loss
  there.

The ninth name, 'Smith (née Jones)', is the one the gates do NOT
report: its family agrees with 1.4.0 again, so that ledger's rule is
NARROWED to {nickname, maiden} -- the shrink its own revisit note
asked to be re-measured for, in the opposite direction from the one it
feared.

The four `# revisit when #445 lands` markers are corrected rather than
deleted: each predicted the fix would leave the diff a subset of the
fields its rule already declared and be absorbed in silence. This rule
MOVES the word rather than adding one, so `given` empties as `family`
fills and the gate goes loud. The notes named the right rule, the
right field and very nearly the right set, and saying so is worth more
than a clean deletion.

Also here: the corpus-wide maiden-clause property in test_parser.py,
where the one-name-word class now asserts the flip instead of no
movement; the clause-free corpus counts, recounted by pasting the
one-liner (the first two were already one low); and two stale
`_CORPUS_FLOORS` comments, 175 and 97 where the files hold 234 and 98.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
decisions.md#M4, written unwrapped, one paragraph per line:

- Derek's rationale in his own terms -- a marker marks a previous
  surname, so including one makes no sense without another surname for
  it to clarify, and a clause that leaves one name word switches the
  assumption from given to family.
- The three empty-family repairs of this bundle share a SYMPTOM, not a
  cause, and the entry says so rather than claiming one pattern: #410
  and #411 were counts that included words which are no part of the
  name, and the grep that finds a fourth is a rule counting name words
  without asking which of them are name words. #445 changes no count
  at all -- its cause is O4's silence at n=1, and the site that grep
  would miss is _name_positions's `count == 1` branch, which is what
  O5 records.
- Both carve-outs and the mechanism they rest on, with the corpus
  witness for each and the mutation each one catches.
- That the interior half is a new reading while the bracketed spelling
  restores 1.4.0.
- The N3 precedence corrected while drafting, and the M1 keying
  corrected in review: M4 is keyed on the maiden NAME, since a
  configured pair produces one with no marker anywhere.
- The M4/H1 counting gap, recorded rather than closed.
- What the ledger markers bought and what they got wrong, with the two
  different counts named: nine corpus names change reading, eight
  arrive as new diffs, and the markers named nine names of which seven
  moved.
- The relationship to #449: this rule reduces that issue's population
  rather than competing with it.

rules.md#M4 gains its `history: decisions.md#M4` pointer, held back
from the previous commit so that commit's anchors all resolve.

The release-log bullet is written from measurement: nine of the 1,090
corpus names change reading, eight arriving as new diffs at every
baseline and the ninth having its 1.4.0 diff shrink instead. It says
which spellings move -- the declared-pair spelling included, which
carries no marker -- which two do not and that both carve-out
witnesses are corpus names, and it tells a caller reading the lone
word out of `given` that this is the release where it moves.

Two bullets already in this release stated readings this change
falsifies, and both are corrected: #335's interior example said the
bare "Jane née Jones Smith" has no family at all, and #412's said
"Jane née and Jones Smith" reads given `Jane`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Smith (née Jones)` reads family 'Smith', nickname 'née Jones' on
1.4.0, 2.0.0 and 2.1.0 alike -- measured on all three wheels. Only the
BARE spelling ever read `given`. The first pass narrowed the 1.4.0
rule on a measurement of that wheel and left the 2.x copies at
["given", "family", "nickname", "maiden"] on the reasoning that those
baselines "read `given`, and the baseline cannot move". They did not,
so two rules stood over a {nickname, maiden} diff declaring four
fields, and classify's subset test would have explained a
`given`/`family` regression on that name in silence -- the absorption
those very `# revisit when #445 lands` notes existed to prevent,
surviving the round that answered them.

Both are narrowed to ["nickname", "maiden"], the matching
_CORPUS_CLAIMS role tuples with them, and both passages rewritten to
say what is true. Verified after narrowing: declared fields now equal
the real diff at all three baselines, the name is still explained, and
a {given, family} diff on it is UNEXPLAINED at 2.0.0 and 2.1.0. At
1.4.0 it is not, because the fields-only fix(suffix-routing) catch-all
matches every name with {given, family, suffix}; that is long-standing
and out of scope, and the ledger now says so rather than implying the
narrowing closed the hole everywhere.

The seed was cases.py's note, whose "Was given 'Smith' on 1.4.0's bare
spelling ..., on 2.0.0, on 2.1.0 and here" reads correctly about the
bare spelling and falsely about the row it sits in. Disambiguated, and
it now records that a ledger took the loose reading.

Also corrected, all measured:

- decisions.md's pattern bullet, which the previous commit's message
  described but whose edit never landed -- a script asserted two
  replacements, failed the second and wrote neither. The text now says
  what the message said: the three repairs share a symptom, not a
  cause; #445 changes no count; and `_name_positions`'s `count == 1`
  branch is the site the recommended grep would miss.
- decisions.md: THREE of the four markers moved opposite to their own
  prediction, not one, with how the miss happened.
- decisions.md: the bound-given mutation list was one short (six, not
  five -- O5's own `abd née Jones` line fails too).
- release_log: the corpus is 1,090, not 1,089; the ninth name's diff
  shrank at all three baselines, not just 1.4.0; and the #434 bullet's
  split-entry argument said `"Anna z Nowak"` has no family at all,
  where it now reads family `Anna` (the argument survives, the symptom
  did not).
- test_parser: the recount was itself one low at every stage, in a
  sentence criticising an earlier author for not recounting. Pasting
  the one-liner gives 646/636/632. #445 adds five rules-corpus rows,
  three of which clear the marker predicate, so every stage moved by
  three.
- expected_since_1.4.0: `J. née Jones Smith V` is explained by
  fix(#424) accepted, not fix(#274) -- its diff carries `suffix`,
  which #274's three fields cannot hold. Asked of classify directly.
  The safety argument holds through that rule, which declares no
  `given` either.
- expected_since_1.4.0: "Four claimed names still do not move"
  measures five, re-measured name by name against the wheel over all
  31 the regex claims.
- test_ledger_guards: the roster comment's arithmetic (six plus two
  where the set holds seven), rewritten from the measured sets.
- Three over-long comment lines in the new blocks, re-wrapped.

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

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.64%. Comparing base (c38e5e7) to head (e274ca0).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #450   +/-   ##
=======================================
  Coverage   98.64%   98.64%           
=======================================
  Files          45       45           
  Lines        3171     3177    +6     
=======================================
+ Hits         3128     3134    +6     
  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
derek73 merged commit 609f632 into master Aug 28, 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.

parse("Smith née Jones") reports no family name — a maiden clause empties a one-word name, where 1.4.0 kept it

1 participant