diff --git a/docs/design/decisions.md b/docs/design/decisions.md index 696ee4bc..ccc115f1 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -535,6 +535,20 @@ Declined: - 2026-08-26 #335 — M3 makes the marker vocabulary act in a THIRD position. M2's claim acts on the word after a name word; M1's on the content of a configured pair; M3's now on the first word of any bracketed clause, under the default policy. #vocabulary-collisions C-i is keyed on the position a claim acts on, so extending where a rule acts re-asks C-i of the whole set it reads, and nothing in the process prompts that — the rule was designed, reviewed and approved without the set being re-examined. Review afterwards found one entry that fails C-i, the Czech abbreviation roz, and its disposition is this file's Excluded (MAIDEN_MARKERS) block, which is keyed to the vocabulary set rather than to any rule: it was removed. Two things worth separating, since the finding surfaced during this work and is easy to file under it — the defect was M2's and predates #335 (bare "Rosalind Roz Smith" lost its family name on master), and M3's own boundary already excluded the common one-word "(Roz)" spelling. What belongs to M3 is only the widening, and the lesson is the general one: re-ask C-i of a vocabulary set whenever a rule extends where its claim acts. - 2026-08-26 #335 — what configuring maiden_delimiters still buys, since M3 removes the commonest reason to reach for it: markerless clauses, and one-word clauses. "Cherice J. (Johnson) Williams" is a real US convention and a corpus name, and nothing in that clause says maiden, so it stays a nickname by default — only a caller who knows their data can say otherwise. The knob is a declaration about the DATA; M3 is a reading of what a clause declares about itself. +### M4 — a maiden marker makes the lone name word the family + +- 2026-08-27 #445 (M4, PR on fix/445-maiden-marked-lone-name) — Derek's rationale, in his terms: normally if there is only one name we assume it is a given name because we have to pick one, but maiden markers mark a previous surname before marriage, so it would not make sense to include one if there was no other surname for it to clarify — and given that, when a marker leaves only one name word we switch the assumption and read that word as the family name. `parse("Smith née Jones")` reported given 'Smith' with an EMPTY family through 2.1 and reads family 'Smith', maiden 'Jones' since. The rule is written in post_rules as H1's sibling and placed directly under it, so the interaction is decidable by reading rather than by running: where H1 fired there is no GIVEN left for M4 to move, and where H1 declined because the title addresses by given name ("Sir John née Jones") the `not titles` guard keeps M4 off the same word. A titled name is H1's at both outcomes, which is what keeps H1's given-name-title carve-out intact. +- 2026-08-27 #445 — the third time in the 2.2 bundle that a name carrying a maiden clause reported no family, and the three share a SYMPTOM rather than a cause. State that carefully, because the first draft of this entry claimed one cause for all three and was wrong. #410 (H1): a title plus one name word reported no family the moment a suffix, nickname or maiden name stood beside it. #411 (P5's reserve): the bound given-name join counted the marker and the maiden name as words it could spend, and spent the family name. Those two ARE one cause -- a count that included words which are no part of the name -- and the grep that would find a fourth is a rule counting name words without first asking which of them are name words. #445 is not that: it changes no count anywhere (post_rules gains a new block, assign gains only a comment), and `parse("Smith")` with no clause beside it at all already read given 'Smith' with an empty family, so nothing was being swallowed. Its cause is the one the next entry gives: O4 decides nothing at one name word, and the reading came from a convention nobody had written down. The site that grep would miss is `_name_positions`'s `count == 1` branch, silent for every order and every name -- which is exactly what O5 now records. +- 2026-08-27 #445 — O4 was SILENT at exactly one name word, which is why the shipped reading had no rule to point at and why O5 now exists. O4 reads a name by comparing where its words stand ("the first name word is the given name, the last is the family name"); with one word the first IS the last, so there is nothing to compare and the rule decides nothing. O5 records the reading as the convention it is — a guess fixed in advance so that the same input always reads the same way, not a determination about the word — and names H1, N3 and M4 as the rules that DO decide such a name. Written that way deliberately: a rule asserting "a lone name word is the given name" as a fact would have made #445 a contradiction of the documented behavior instead of an exception to a documented guess, and would make #449 one too. +- 2026-08-27 #445 — the two carve-outs are not inventions, and both rest on mechanisms.md#TWO-LAYER-ASSIGN: the positional layer never overrides a vocabulary claim, and M4 changes only what POSITION decided. A word the vocabulary claims as a given name keeps that reading ('abd née Jones' — `vocab:bound-given`), and so does a word written as an initial ('J. née Jones Smith V' — the `initial` shape tag). Both are read off tags classify already recorded rather than off a predicate of M4's own, and both witnesses are real corpus names rather than constructed cases, so each carve-out has something in the differential that would notice its loss. Mutation-checked on a scratch copy: dropping the `initial` carve-out fails that name's case row, its facade twin, M4's own boundary example and the pre-existing assertion in test_the_chain_and_the_walk_stop_where_the_peel_begins; dropping `vocab:bound-given` fails the other name's two runners, M4's boundary, P5's own doc example, O5's own `abd née Jones` line and the corpus-wide maiden-clause property on 'abdul' -- six in all, the O5 line having been added by the review round, and the list is exhaustive as re-run against the final tree. +- 2026-08-27 #445 — the two halves of Derek's answer differ in their relationship to 1.4.0, and a reader should not have to re-derive it. The bracketed spelling RESTORES v1: `Smith (née Jones)` read family 'Smith', nickname 'née Jones' on 1.4.0, 2.0.0 and 2.1.0 alike, and now reads family 'Smith', maiden 'Jones' — the clause changes hands and the family name stays put, so the 1.4.0 gate's diff on that name SHRANK to {nickname, maiden}. The interior spelling is a NEW reading: `Jane née Jones Smith` read first 'Jane', middle 'née Jones', last 'Smith' on 1.4.0 and reads family 'Jane', maiden 'Jones Smith' now, the real surname being inside the maiden value by M2's greedy take. That the marker stands inside the name changes nothing for M4, which counts what the take LEAVES rather than where the marker stood — one name word left that way is one name word, which is the widest half of the decision and the half no earlier version agrees with. +- 2026-08-27 #445 — a precedence claim corrected while drafting, and it is exactly the class this bundle keeps producing. The first draft of M4 said a name carrying a nickname is N3's rather than this rule's. Measured false: `'Smitty' Jones Jr. née Smith` reads family 'Jones', because N3's count does not set a suffix aside, so N3 declines and M4 fires. A nickname holds nothing off — where N3 has already named the family M4 finds nothing left to move, and where N3 declined M4 names it. The general lesson is the one #410 and #411 already taught from the other side: a rule that counts name words and a rule that counts something else will disagree at the edges, and the disagreement is only visible if the interaction is measured rather than asserted. +- 2026-08-27 #445 — what the ledger's `# revisit when #445 lands` markers bought, and what they got wrong. Four blocks carried them, and each predicted that a "keep the family" fix would leave the diff a subset of the fields its rule already declared, so the rule would go on explaining the name and the change would be absorbed with the gate green. That was wrong in a way the rule's own shape settles: M4 MOVES the one name word rather than adding one, so `given` empties as `family` fills, the diff outgrows every declaration, and eight names arrived UNEXPLAINED at all three baselines. Two counts run through this and they are different sets, so name which is which: NINE corpus names change reading, of which eight arrive as new diffs — the ninth, 'Smith (née Jones)', has its 1.4.0 diff SHRINK instead, its family agreeing with v1 again. The four markers between them named NINE names to re-measure, of which seven moved: the two that did not are M4's carve-outs, and the two movers the markers could not name were not corpus names until this change's own examples added them. The markers earned their keep all the same — they named the right rule, the right field and very nearly the right set — so they are corrected in place rather than deleted, and the correction says what actually happened. THREE of the four moved in the opposite direction from their own prediction, and getting that number right took two passes. The fix(#335) rule for 'Smith (née Jones)' appears in all three ledgers, and every copy feared absorbing a WIDENED diff while the diff in fact SHRANK to {nickname, maiden}. The first pass narrowed only the 1.4.0 copy, on a measurement of that wheel, and left the other two at four fields on the reasoning that 2.0.0 and 2.1.0 'read `given`, and the baseline cannot move'. They did not: only the BARE spelling ever read `given`, and this rule holds the bracketed one, which reads family 'Smith', nickname 'née Jones' on every released version. So two rules stood over a two-field diff declaring four, and classify's subset test would have explained a `given`/`family` regression on that name in silence -- the absorption those very notes existed to prevent, surviving the round that answered them. A third reviewer found it by instrumenting the comparator to dump real diff-field sets, and that is the lesson worth keeping: ledger prose cannot be checked by reading it, and the round that corrects a marker is exactly where a wrong premise gets written down with confidence. +- 2026-08-27 #445 — the corpus-wide property test is the repair worth copying. tests/v2/test_parser.py's `test_a_maiden_clause_changes_nothing_else` asserts over the corpus that appending " née Jones" adds a maiden name and moves no other field, and M4 falsifies it for fourteen names. Skipping them would have bought a green suite and lost the check; instead the test computes M4's guard from the base parse's tokens and asserts the flip — family takes what `given` held, every other field standing still — so the class that used to be a hole is now the strongest witness the rule has, fourteen names against the six rows cases.py carries. It is also what executes M4's nickname precedence, `'Smitty' Jones Jr.` being one of the fourteen. +- 2026-08-27 #445 (review round) — M4 is keyed on the maiden NAME, not on a marker, and the statement was corrected to say so after two reviewers measured the same defect independently. The guard tests for a token in the MAIDEN role, and M1's caller-configured pair produces one with no marker anywhere: under `Policy(maiden_delimiters=frozenset({("(", ")")}))`, `Smith (Jones)` reads family 'Smith', maiden 'Jones', and so do M1's own boundary examples `Smith (Nee)` and `Smith (z domu)`. The BEHAVIOUR is right — the rationale transfers to a declared pair without a word of change, since what announces a former surname is the clause, not the vocabulary that marked it — so the fix was to the rule, which had said "a recognized maiden marker that takes its name and leaves exactly one name word" and now says "a maiden name standing beside exactly one name word". That covers M1, M2 and M3 uniformly, M1 is in M4's `interacts:` and M4 in M1's, and the configured path has an example line of its own. The general lesson is the one this bundle keeps teaching from a new angle: a statement written from the case that prompted the change describes that case, not the code, and only measuring the OTHER paths into the same guard finds the difference. +- 2026-08-27 #445 (review round) — a known gap recorded rather than closed, the way H1 carries its own. M4's guard counts GIVEN tokens; H1 counts nothing at all (it tests which roles are unoccupied). So a name word another rule has joined counts as several here and M4 declines where H1 fires: `Dr. Dean of Chemistry` reads family 'Dean of Chemistry' while `Dean of Chemistry née Jones` keeps given 'Dean of Chemistry', the connective join (P3) having left three GIVEN tokens. rules.md#P3 says the joined part is one name word wherever another rule counts them, so the two siblings genuinely disagree and this is a gap rather than a boundary. Not closed here: widening the count to units moves zero corpus names (measured), and a behaviour change nobody has approved does not belong in a branch whose blast radius was settled. The sibling claim in the code comment and in this entry is narrowed to match — sibling except in what it counts. +- 2026-08-27 #445 — the relationship to #449, briefly, because the two look like the same question and are not. #449 asks whether a lone name word with nothing to decide it should REPORT an ambiguity; #445 removes a class of names from that population by giving them something that decides. So #445 reduces #449's reach rather than competing with it, and #449's measurement — a naive "one name word, nothing decided it" condition fires on 19 of 1,085 corpus names, only three or four of them the case it is about — is re-taken after this lands. O5 cites #449 in prose and deliberately carries no `deviates:` marker: that marker asserts an intended output the runner then checks strictly, and #449 has not decided one. + ### O1 — East Slavic rotation - (v1 era, PR #154) — why patronymic handling is OPT-IN at all: unconditional detection breaks ordinary Latin names whose endings collide (Martin, Franklin, Benjamin), the finding that forced v1's Russian work behind a flag and set the opt-in shape v2 inherited. diff --git a/docs/design/rules.md b/docs/design/rules.md index f2ee4df6..0c2eb032 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -531,18 +531,19 @@ N3. Rationale: a person set down as a nickname plus one name word is Accepted: a marker-led clause is a maiden clause and not a nickname one (M3), so this rule does not reach a name written that way, and the one name word keeps the reading the bare - spelling gives it rather than becoming the family name. The - empty family that leaves is the bare spelling's, and #445 tracks - whether it is right — 1.4.0 read the bare spelling as first - Smith, middle née, last Jones, emptying nothing, and the same - empty family has been repaired twice already, at H1 (#410) and at - P5's reserve (#411). - "Smith (née Jones)" → given="Smith" - history: decisions.md#N3 · interacts: H1, M3 · implemented: nameparser/_pipeline/_assign.py + spelling gives it. Since #445 that reading is the family name + all the same, M4 reaching both spellings from the other side. + The two rules agree on THIS name and not in general: M4 counts + name words alone, so where a suffix stands beside them it fires + and this rule declines. The Accepted line above reads a bare + nickname-plus-word-plus-suffix name as given with no family, and + the same name carrying a maiden clause reads family instead. + "Smith (née Jones)" → family="Smith" + history: decisions.md#N3 · interacts: H1, M3, M4 · implemented: nameparser/_pipeline/_assign.py ## Maiden names (M) -Background: a maiden name is written beside the current name, set off by a marker or by enclosure. Markers are attested across French née/né and the unaccented nee English writing uses for both, German geb./geborene, Dutch geboren, Czech/Slovak rozená (the abbreviation roz. shipped through 2.1 and was removed in 2.2 -- it collides with the English diminutive Roz, and a caller who needs it adds it to their own Lexicon), Scandinavian født/fødd/född, Russian урожд. and its full participles урождённая/урождённый (the participles in both the ё and е spellings, which case normalization does not fold), Japanese 旧姓, and Polish z domu — both grammatical genders where attested. Every one of these is a marker wherever it stands, the unaccented nee included: an enclosure holding a word past it reads as the maiden name and the marker is dropped, so a configured pair reading "(Nee Jones)" gives maiden Jones and not Nee Jones. A marker need not be one word. z domu is two, and a phrase marker is recognized only whole and only where its words stand together: neither of its words is a marker standing alone, and neither is the pair once a bracketed clause or a comma divides them. That is what makes shipping it safe where shipping its words would not be — z is an ordinary Polish preposition, and a name that merely contains one keeps its family name. Japanese more often writes the marker with a fullwidth colon (旧姓:佐藤), which is no separator, so marker and name arrive as a single word. Which enclosures mean "maiden" rather than "nickname" is a caller convention, so the maiden reading of a delimiter pair is opt-in — except where the clause announces itself. A clause of two words or more led by a marker word has said which convention it means, and reads as the maiden name inside a nickname pair as well (M3) — unless its content is suffix-shaped, which S1 takes ahead of both. A lone marker has said nothing, and neither has one the colon spelling above glues to the name. +Background: a maiden name is written beside the current name, set off by a marker or by enclosure. Markers are attested across French née/né and the unaccented nee English writing uses for both, German geb./geborene, Dutch geboren, Czech/Slovak rozená (the abbreviation roz. shipped through 2.1 and was removed in 2.2 -- it collides with the English diminutive Roz, and a caller who needs it adds it to their own Lexicon), Scandinavian født/fødd/född, Russian урожд. and its full participles урождённая/урождённый (the participles in both the ё and е spellings, which case normalization does not fold), Japanese 旧姓, and Polish z domu — both grammatical genders where attested. Every one of these is a marker wherever it stands, the unaccented nee included: an enclosure holding a word past it reads as the maiden name and the marker is dropped, so a configured pair reading "(Nee Jones)" gives maiden Jones and not Nee Jones. A marker need not be one word. z domu is two, and a phrase marker is recognized only whole and only where its words stand together: neither of its words is a marker standing alone, and neither is the pair once a bracketed clause or a comma divides them. That is what makes shipping it safe where shipping its words would not be — z is an ordinary Polish preposition, and a name that merely contains one keeps its family name. Japanese more often writes the marker with a fullwidth colon (旧姓:佐藤), which is no separator, so marker and name arrive as a single word. Which enclosures mean "maiden" rather than "nickname" is a caller convention, so the maiden reading of a delimiter pair is opt-in — except where the clause announces itself. A clause of two words or more led by a marker word has said which convention it means, and reads as the maiden name inside a nickname pair as well (M3) — unless its content is suffix-shaped, which S1 takes ahead of both. A lone marker has said nothing, and neither has one the colon spelling above glues to the name. A marker also says something about the name standing beside it: it 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 (M4). M1. Rationale: an enclosure the caller has declared to mean maiden holds the former family name; a recognized marker word inside it @@ -567,7 +568,7 @@ M1. Rationale: an enclosure the caller has declared to mean maiden "Jane Smith (Nee) (Jones)" maiden-parens → maiden="Nee Jones" "Andrew Perkins (MBA)" maiden-parens → suffix="MBA" · boundary "Maria Kowalska (z domu)" maiden-parens → maiden="z domu" · boundary - history: decisions.md#M1 · interacts: S1, M2, M3 · implemented: nameparser/_pipeline/_extract.py, nameparser/_pipeline/_group.py + history: decisions.md#M1 · interacts: S1, M2, M3, M4 · implemented: nameparser/_pipeline/_extract.py, nameparser/_pipeline/_group.py M2. Rationale: a maiden marker announces that what follows it is the former family name; the marker is an announcement, not a name. @@ -662,6 +663,44 @@ M3. Rationale: an enclosure says nothing about whether it means "Jane Smith (née V)" → maiden="V" history: decisions.md#M3 · interacts: M1, M2, S1, N1 · implemented: nameparser/_pipeline/_extract.py +M4. Rationale: a maiden name is a FORMER family name, and a former + one only means something beside a current one — nobody announces + the surname they used to carry where there is no surname beside + it to tell it apart from. So where a maiden name has been read + out of a name and a single name word is left standing, that word + is the surname the bearer uses now; writing the clause at all is + what rules out the given-name reading O5's convention would + otherwise leave it with. How the maiden name was written does + not enter into it — a marker taking the words after it (M2), a + marker-led clause (M3), and a clause in a pair the caller + declared to mean maiden (M1) announce the same thing, and the + last of those carries no marker at all. + A maiden name standing beside exactly one name word makes that + word the family name, whatever suffix or nickname stands beside + it — an annotation beside the name is no part of the name, which + is how H1 reads the same shape. Two kinds of word are beyond its + reach, for one reason: the rule changes what POSITION would have + decided, so it cannot overrule what a word already IS + (mechanisms.md#TWO-LAYER-ASSIGN). A word the vocabulary has + claimed as a given name keeps that reading, and so does a word + written as an initial, which is nobody's family name. A name + carrying a TITLE is H1's rather than this rule's, H1's + given-name-title carve-out included, which keeps the word a + given name. A nickname holds nothing off: where N3 has already + named the family this rule finds nothing left to move, and where + N3 declined — its count does not set a suffix aside — this rule + names it. Where the maiden name stood does not matter, only what + is left beside it: a marker inside the name takes the rest of it + (M2), and one name word left that way is one name word. + "Smith née Jones" → family="Smith" + "Smith (née Jones)" → family="Smith" + "Smith (Jones)" maiden-parens → family="Smith" + "Jane née Jones Smith" → family="Jane" + "Smith née Jones PhD" → family="Smith" + "abd née Jones" → given="abd" · boundary + "J. née Jones Smith V" → given="J." · boundary + history: decisions.md#M4 · interacts: M1, M2, M3, H1, N3, O5 · implemented: nameparser/_pipeline/_post_rules.py + ## Commas & structure (C) Background: a comma in a name signals one of two conventions — the listing form "Family, Given" or trailing credentials "Name, PhD" — and which is meant can only be judged from what stands after the first comma. Recognizing a credential run is by nature a vocabulary judgment, so this is the one structural decision that consults the suffix word lists. Which characters COUNT as the comma is part of the rule: the Arabic comma (U+060C) and the fullwidth comma (U+FF0C) both signal the listing form, while the ideographic comma (U+3001) is not a name-structure comma at all (#265). @@ -810,6 +849,40 @@ O4. Rationale: what no vocabulary claims can only be read by where exceptions from; its boundaries are the other rules. implemented: nameparser/_pipeline/_assign.py +O5. Rationale: O4 reads a name by comparing where its words stand, + and a name of exactly one name word gives it nothing to compare + — the first name word is also the last, so the rule that decides + every longer name is silent here. Nothing in such an input says + whether the word is a given name or a family name: both readings + fit it equally well, and the library still has to report one. + A name of one name word that nothing else has decided reads that + word as the given name under the default given-first order, and + as the family name under a declared family-first one. That is a + convention rather than a determination — the same input has to + read the same way every time, so one of two equally consistent + readings is fixed in advance — and it is not evidence about the + word. Every rule that DOES decide such a name outranks it, and + each of them carves out a case it declines to decide — where the + convention is what is left. A title makes the word the family + name (H1), except a given-name title, which addresses by given + name; a nickname beside it does the same (N3), except where a + suffix stands beside them too, which its count does not set + aside; and a maiden name beside it does the same (M4), except + where the vocabulary or the word's own shape has claimed the + word already. What the library should SAY about a reading it + merely fixed — whether the convention is worth reporting as an + ambiguity — is open, and #449 holds the measurement that makes + it a design question rather than an implementation one. + "Smith" → given="Smith" + "Garcia" family-first → family="Garcia" + "Sir John" → given="John" + "'Smitty' Jones Jr." → given="Jones" + "abd née Jones" → given="abd" + "Mr. Johnson" → family="Johnson" · boundary + "'Smitty' Jones" → family="Jones" · boundary + "Smith née Jones" → family="Smith" · boundary + interacts: O4, H1, N3, M4 · implemented: nameparser/_pipeline/_assign.py + ## Scripts & writing systems (W) Background: script-conditional behavior is permitted exactly where the writing system itself — not statistics about it — settles the convention; a language can never be inferred from Latin-script text, because transliteration destroys the signal. The facts this section builds on: Chinese and Japanese both write the family name first in native script, so the script settles the order without knowing the language. Hangul is written by exactly one language and Korean family names are a small closed census set. Han text does not identify its language — a Chinese surname list would divide Japanese 高橋一郎 as 高 + 橋一郎 — which is why Han division is opt-in and there is no Korean pack to opt into. Hiragana never transcribes a foreign name (transcriptions are katakana alone), so kanji-plus-kana is a Japanese name in Japanese order, while wholly-katakana is predominantly a transcribed foreign name already in given-first order. Real Chinese text is unspaced (毛泽东); the spaced 毛 泽东 is an artifact. A fuller narrative lives in docs/usage.rst's East Asian section. diff --git a/docs/release_log.rst b/docs/release_log.rst index b0882ce0..da67c853 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -47,9 +47,9 @@ Release Log - Remove the Czech/Slovak abbreviation ``roz.`` from the default maiden markers, where it had been eating English given names. Marker matching is whole-token, case-folded and period-insensitive, so ``Roz`` -- the ordinary diminutive of Rosalind -- was the same string as the marker, and a maiden marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**, and ``"Rosalind Roz Jones Smith"`` read maiden ``Jones Smith``. Both now read the way 1.4.0 read them, given ``Rosalind`` with ``Roz`` a middle name and ``Smith`` the family. The defect is as old as the marker vocabulary and belongs to the bare marker rule, not to the bracketed-clause change in the bullet below; it was found while reviewing that change, which would have widened it -- with ``roz`` still shipped, ``"Jane Smith (Roz Jones)"`` reads maiden ``Jones`` where 2.1.0 read nickname ``Roz Jones``. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` now reads middle ``Nováková roz.``, family ``Svobodová`` rather than family ``Nováková``, maiden ``Svobodová`` -- which is also how 1.4.0 read it, so this is a return to the v1 reading rather than a new one. The full participle is untouched and needs no configuration: ``"Anna Nováková rozená Svobodová"`` still reads family ``Nováková``, maiden ``Svobodová``, and it is only the abbreviation that was ambiguous. A caller who wants the abbreviation back adds it to their own lexicon -- ``Parser(lexicon=dataclasses.replace(lex, maiden_markers=lex.maiden_markers | {"roz"}))`` -- which restores the old reading exactly. This removal takes the default set to 16 entries. No differential corpus name contains the string in any case or position, so nothing moves against any baseline (closes the collision found in #335's review) - - Add the Polish maiden marker ``z domu`` to the default vocabulary, and let a ``maiden_markers`` entry be more than one word. ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it -- ``"Maria Kowalska (z domu Nowak)"`` read nickname ``z domu Nowak`` and now reads family ``Kowalska``, maiden ``Nowak`` -- while a clause holding nothing but the marker stays a nickname, as a lone ``(née)`` does: ``"Maria Kowalska (z domu)"`` keeps nickname ``z domu``. **This changes what a caller who worked around the old limit should have configured.** Through 2.1 a multi-word entry in any vocabulary field could never match, and the ``UserWarning`` it raised said to split it into separate entries. For maiden markers that advice was wrong, not merely unhelpful: with ``z`` and ``domu`` stored as two entries, ``"Maria Kowalska z domu Nowak"`` reads maiden ``domu Nowak`` -- the marker's own second word inside the value -- and ``"Anna z Nowak"`` reads maiden ``Nowak`` with **no family name at all**, because a bare preposition is then a marker and a marker takes every word after it. If you added ``z`` and ``domu`` separately, remove both: the shipped phrase does the job, and dropping the two word entries is what restores ``"Anna z Nowak"`` to given ``Anna``, middle ``z``, family ``Nowak`` -- which is how it reads on the default vocabulary, unchanged from 1.4.0. The same applies to any other field-splitting workaround for this one field. A phrase is matched over whole tokens that stand together -- a bracketed clause or a comma between the words ends the run, so ``"Anna z (domu) Nowak"`` keeps its family name too -- and longest first, so a phrase and a word entry that starts it can both be configured: with ``geb`` shipped and ``geb von`` added, ``"Jane Smith geb von Braun"`` reads maiden ``Braun`` and ``"Jane Smith geb Braun"`` still reads maiden ``Braun``. ``given_name_titles`` was the only field exempt from the multi-word warning; ``maiden_markers`` is now the second, and the warning still fires for every other field. This addition takes the default marker set to 17 entries. Two differential corpus names move, at all three baselines, and both are examples this change adds to the rules doc; no corpus name held ``domu`` or a bare ``z`` before it (#434) + - Add the Polish maiden marker ``z domu`` to the default vocabulary, and let a ``maiden_markers`` entry be more than one word. ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it -- ``"Maria Kowalska (z domu Nowak)"`` read nickname ``z domu Nowak`` and now reads family ``Kowalska``, maiden ``Nowak`` -- while a clause holding nothing but the marker stays a nickname, as a lone ``(née)`` does: ``"Maria Kowalska (z domu)"`` keeps nickname ``z domu``. **This changes what a caller who worked around the old limit should have configured.** Through 2.1 a multi-word entry in any vocabulary field could never match, and the ``UserWarning`` it raised said to split it into separate entries. For maiden markers that advice was wrong, not merely unhelpful: with ``z`` and ``domu`` stored as two entries, ``"Maria Kowalska z domu Nowak"`` reads maiden ``domu Nowak`` -- the marker's own second word inside the value -- and ``"Anna z Nowak"`` reads maiden ``Nowak`` and loses its surname, because a bare preposition is then a marker and a marker takes every word after it -- family ``Anna`` today, the given name standing in for the surname the clause ate, and no family at all before #445 below. If you added ``z`` and ``domu`` separately, remove both: the shipped phrase does the job, and dropping the two word entries is what restores ``"Anna z Nowak"`` to given ``Anna``, middle ``z``, family ``Nowak`` -- which is how it reads on the default vocabulary, unchanged from 1.4.0. The same applies to any other field-splitting workaround for this one field. A phrase is matched over whole tokens that stand together -- a bracketed clause or a comma between the words ends the run, so ``"Anna z (domu) Nowak"`` keeps its family name too -- and longest first, so a phrase and a word entry that starts it can both be configured: with ``geb`` shipped and ``geb von`` added, ``"Jane Smith geb von Braun"`` reads maiden ``Braun`` and ``"Jane Smith geb Braun"`` still reads maiden ``Braun``. ``given_name_titles`` was the only field exempt from the multi-word warning; ``maiden_markers`` is now the second, and the warning still fires for every other field. This addition takes the default marker set to 17 entries. Two differential corpus names move, at all three baselines, and both are examples this change adds to the rules doc; no corpus name held ``domu`` or a bare ``z`` before it (#434) - - Fix a bracketed maiden clause reading as a nickname because its brackets were not declared. ``"Jane Smith nee Jones"`` gave maiden ``Jones``, while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones`` -- the parenthesis pair is a nickname delimiter by default and nothing looked inside it, so the marker word the writer put there was read as part of an alias. This is about every delimiter pair the parser ships, not only the parentheses these examples use: quotes are how a nickname is most often written, and ``"Jane Smith "née Jones""`` and ``"Jane Smith 'née Jones'"`` move from nickname to maiden exactly as the parenthesized spelling does. A bracketed clause whose content opens with a recognized marker word and carries a word after it now reads as the maiden name whichever bucket its pair sits in: ``"Jane Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, and so does the Japanese ``"山田 花子(旧姓 佐藤)"`` (family 山田, given 花子, maiden 佐藤), which needed ``Policy(maiden_delimiters=...)`` through 2.1. The clause is lifted out as a region rather than having its brackets erased, which is what keeps an INTERIOR clause from eating the name behind it: ``"Jane (née Jones) Smith"`` reads given ``Jane``, family ``Smith``, maiden ``Jones``, where erasing the brackets would leave the bare ``"Jane née Jones Smith"``, whose marker takes every word to the end of the name -- maiden ``Jones Smith`` and no family at all. The closing bracket is a boundary the writer typed, and it is now where the maiden name stops. Two clauses beside each other no longer merge: ``"Jane "Janey" Smith (née Jones)"`` read nickname ``Janey née Jones`` through 2.1 and now reads nickname ``Janey``, maiden ``Jones``, each clause keeping its own role, and this reaches ``HumanName``. What does NOT move is a clause that says nothing about maiden names. A lone marker in brackets is just a word in brackets -- ``"Jane Smith (née)"`` stays nickname ``née``, as 1.4.0 read it -- because a marker with no name after it marks nothing, and because a one-word clause may be the surname it looks like (``Nee`` is Irish Ní/Nee and a Chinese romanization). A markerless clause stays a nickname too: ``"Cherice J. (Johnson) Williams"`` keeps nickname ``Johnson``, which is what ``Policy(maiden_delimiters=...)`` remains for -- the parenthesized birth surname is a real convention, and only a caller who knows the data can say that is what these brackets mean. One accepted consequence: the word after the marker is not tested for being a name word, so a bracketed clause does not stop at a credential where the bare form does -- ``"Jane Smith (née V)"`` reads maiden ``V`` while ``"Jane Smith née V"`` reads suffix ``V``. The clause goes to ``maiden`` whole and the bracket is its boundary, so the trailing-suffix rule never sees the word as trailing the name. One more shape moves with it, and it is the one to know about if you parse names that are a surname and a clause: a name that is ONLY a marker clause and one name word now reads that word as the given name rather than the family. ``"Smith (née Jones)"`` reads given ``Smith``, maiden ``Jones`` where every earlier version read family ``Smith``, nickname ``née Jones``. That is the bracketed form agreeing with the bare one -- ``"Smith née Jones"`` has read given ``Smith`` since 1.4.0 -- because a marker-led clause is not a nickname clause and so does not reach the rule that would have made the word a family name. The empty ``family`` it leaves behind is the bare form's behavior and is not new here, though it is not 1.4.0's either -- v1 read ``"Smith née Jones"`` as first ``Smith``, middle ``née``, last ``Jones``, and #445 tracks whether the family should be kept. Seven of the 1,085 differential corpus names move against the 2.1.0 and 2.0.0 baselines and six against 1.4.0: the Japanese name is the odd one, and against 1.4.0 it is not a new diff at all, since 1.4 read the fullwidth brackets as name text and had no nickname there to change hands. Eight corpus names have the shape and seven move (measured); the eighth is ``"Jane Smith (née Jr.)"``, which the suffix rule takes first. So the count says how few bracketed marker clauses the corpora hold rather than how narrow the change is -- six of the seven are hand-written rule or issue examples, only the Japanese name arriving from elsewhere, and the coverage that bounds the behavior is ``tests/v2/cases.py`` (closes #335) + - Fix a bracketed maiden clause reading as a nickname because its brackets were not declared. ``"Jane Smith nee Jones"`` gave maiden ``Jones``, while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones`` -- the parenthesis pair is a nickname delimiter by default and nothing looked inside it, so the marker word the writer put there was read as part of an alias. This is about every delimiter pair the parser ships, not only the parentheses these examples use: quotes are how a nickname is most often written, and ``"Jane Smith "née Jones""`` and ``"Jane Smith 'née Jones'"`` move from nickname to maiden exactly as the parenthesized spelling does. A bracketed clause whose content opens with a recognized marker word and carries a word after it now reads as the maiden name whichever bucket its pair sits in: ``"Jane Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, and so does the Japanese ``"山田 花子(旧姓 佐藤)"`` (family 山田, given 花子, maiden 佐藤), which needed ``Policy(maiden_delimiters=...)`` through 2.1. The clause is lifted out as a region rather than having its brackets erased, which is what keeps an INTERIOR clause from eating the name behind it: ``"Jane (née Jones) Smith"`` reads given ``Jane``, family ``Smith``, maiden ``Jones``, where erasing the brackets would leave the bare ``"Jane née Jones Smith"``, whose marker takes every word to the end of the name -- maiden ``Jones Smith``, with the real surname inside that value and the given name standing in ``family`` (it read no family at all until #445, below). The closing bracket is a boundary the writer typed, and it is now where the maiden name stops. Two clauses beside each other no longer merge: ``"Jane "Janey" Smith (née Jones)"`` read nickname ``Janey née Jones`` through 2.1 and now reads nickname ``Janey``, maiden ``Jones``, each clause keeping its own role, and this reaches ``HumanName``. What does NOT move is a clause that says nothing about maiden names. A lone marker in brackets is just a word in brackets -- ``"Jane Smith (née)"`` stays nickname ``née``, as 1.4.0 read it -- because a marker with no name after it marks nothing, and because a one-word clause may be the surname it looks like (``Nee`` is Irish Ní/Nee and a Chinese romanization). A markerless clause stays a nickname too: ``"Cherice J. (Johnson) Williams"`` keeps nickname ``Johnson``, which is what ``Policy(maiden_delimiters=...)`` remains for -- the parenthesized birth surname is a real convention, and only a caller who knows the data can say that is what these brackets mean. One accepted consequence: the word after the marker is not tested for being a name word, so a bracketed clause does not stop at a credential where the bare form does -- ``"Jane Smith (née V)"`` reads maiden ``V`` while ``"Jane Smith née V"`` reads suffix ``V``. The clause goes to ``maiden`` whole and the bracket is its boundary, so the trailing-suffix rule never sees the word as trailing the name. One more shape moves with it, and it is the one to know about if you parse names that are a surname and a clause: a name that is ONLY a marker clause and one name word. A marker-led clause is not a nickname clause, so the rule that reads a nickname plus one name word as a surname no longer sees this shape -- and what names the family instead is the maiden marker itself, which is #445 below, in this same release. ``"Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, where 1.4.0, 2.0.0 and 2.1.0 all read family ``Smith``, nickname ``née Jones``: the clause changes hands and the family name stays put. It is also the bracketed form agreeing with the bare ``"Smith née Jones"``, which #445 moves to family ``Smith`` from the given name every version through 2.1 read there. Without that rule this change would have left both spellings with an empty ``family``. Seven of the 1,085 differential corpus names move against the 2.1.0 and 2.0.0 baselines and six against 1.4.0: the Japanese name is the odd one, and against 1.4.0 it is not a new diff at all, since 1.4 read the fullwidth brackets as name text and had no nickname there to change hands. Eight corpus names have the shape and seven move (measured); the eighth is ``"Jane Smith (née Jr.)"``, which the suffix rule takes first. So the count says how few bracketed marker clauses the corpora hold rather than how narrow the change is -- six of the seven are hand-written rule or issue examples, only the Japanese name arriving from elsewhere, and the coverage that bounds the behavior is ``tests/v2/cases.py`` (closes #335) - Fix a particle chain and a maiden name taking a trailing generational numeral as a name word: ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``. Both stops asked "is this a suffix?" with the test that vetoes a bare ``V`` as an initial -- the same question the bound given-name join asked until #401 -- and now ask the suffix rule's own walk, over the name as they would leave it: family ``van der Berg``, maiden ``Jones Smith``, suffix ``V``, for ``I`` and ``X`` alike; ``"John van der J. V"`` and ``"J. née Jones Smith V"`` keep their readings, the word before the numeral being an initial as written or as left. The chain stops before a bare credential with words to spare too, as ``"John Smith Ma"`` does, where the suffix rule will still find them once the chain has run: ``"John van der Berg Ma"`` reads suffix ``Ma``, which is how 1.4.0 read it, while ``"Freiherr von Berg MA"`` keeps family ``von Berg MA``; a maiden name keeps such a credential, since the words the marker takes are the very words that made it one. A particle that is also suffix vocabulary now ends the chain where it opens the trailing run: ``"John Smith Mc V"`` reads suffix ``Mc, V``. The chain also no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van``, as ``"Dr. van Johnson"`` has since #367, the chain and the bound given-name join now asking the same test that reads the abbreviation as a title (closes #424) @@ -79,10 +79,12 @@ Release Log - Fix a bound given-name join leaving no family name when the name also carries a maiden clause, and stop the join absorbing the marker itself. ``"abdul Berg née Jones"`` read given ``abdul Berg`` with an EMPTY family, where ``"abdul Berg"`` alone correctly reads given ``abdul``, family ``Berg``. The join reserves a name word so it always leaves a family name behind -- two name words alone do not join -- but the reserve was counted while the maiden marker and the maiden name were still part of the name, and the pass that removes them runs afterwards. Four words counted, the join fired, and when the two departed nothing was left for the family. The words a marker will take no longer count toward the reserve, so that name reads given ``abdul``, family ``Berg``, maiden ``Jones``. Excluding them is not sufficient on its own: where a suffix sits INSIDE the name the marker walk stops early, enough words survive to clear the reserve, and the join fired and took the marker -- ``"abd née Jones Jr Smith Berg"`` read given ``abd née``. The join now declines outright when the piece it would absorb is a marker, which is what the rule meant all along: it joins the bound word to a name word, and a marker is not one. A name with a word to spare is untouched -- ``"abd Allah Smith née Jones"`` still joins, giving given ``abd Allah``, family ``Smith``, maiden ``Jones``. This is pre-existing for the spellings that were already vocabulary: ``abdul``, ``abdel`` and ``abdal`` read this way at 2.0.0 and 2.1.0 alike. ``abd`` is different -- it reads CORRECTLY at 2.1.0 and acquired the defect only when this same release made it a bound given-name word, so for that spelling this fixes a regression introduced earlier in the cycle; and the particle spellings reached the reserve for the first time through the maiden-marker chain stop above. Two consequences worth knowing. The bound-given join no longer swallows a maiden marker at all, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``, middle ``Jones``; the connective join is the remaining case, tracked at `#412 `_. And where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading and the name has no given name: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, matching how ``"Berg, abd"`` alone has always parsed. ``abd`` is the only shipped word in both sets (closes #411) - - Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname. A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words -- the three-word test that keeps a single-letter connective a name word, and the bound given-name reserve -- counted them, because the pass that removes them ran after those rules. ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, but ``"juan y garcia nee jones"`` counted five words, joined the ``y``, and read given ``juan y garcia`` with NO family name at all; ``"John e Smith nee jones"``, ``"Lt.Gov. juan e garcia nee jones"`` and six more differential corpus names lose the family the same way under an appended clause. The marker pass now runs before every join, so the joins and the counts see only the name that remains, and a name of two or more name words reads as it reads without its maiden clause, plus the maiden name: ``"juan y garcia nee jones"`` is given ``juan``, middle ``y``, family ``garcia``, maiden ``jones``. (A title plus ONE name word is the #410 shape, fixed separately below: ``"Dr. Jane née Smith"`` reads family ``Jane`` now, as ``"Dr. Jane"`` does.) The same order closes the last of the join-swallows. The connective join used to merge the marker into a multi-word piece before the marker rule could see it, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` with no maiden name; it now reads family ``van der Berg``, maiden ``y Jones`` -- the marker takes the words after it, connective included -- and ``"Jane née and Jones Smith"`` reads given ``Jane``, maiden ``and Jones Smith`` where it read middle ``née and Jones``, family ``Smith``. It also retires the particle chain's marker stop from the #399 fix above, whose condition restated the marker rule's and disagreed with it one suffix later (``"Jane van der Berg née Jr Jones"``, #417): a marker the rule declines -- nothing after it but a suffix -- is an ordinary word and rides inside the chain, which is the reading ``"Jane van der Berg née"`` already had, and the parsed fields of that name do not change. Two limits. A bound given-name word still never joins onto a marker standing as a word of its own, so ``"Berg, abdul née PhD"`` keeps given ``abdul``; a declined marker the particle chain has taken travels with the chain, so ``"Abd van der Berg née Jr Jones"`` reads given ``Abd van der Berg née`` where it read given ``Abd van der Berg``, middle ``née Jr``. And the marker rule now sees the words as written rather than as joined, so a suffix-vocabulary word inside the maiden name stops it even with a connective beside it: ``"Jane née Jr y Jones"`` read maiden ``Jr y Jones`` and now reads family ``Jr y Jones`` with no maiden name, and ``"Jane Smith née Jones Jr y Smith"`` read maiden ``Jones Jr y Smith`` and now reads maiden ``Jones``, family ``Jr y Smith``. One consequence: ``"abd née Jones Jr Smith Berg"`` reads given ``abd Jr`` -- as ``"abd Jr Smith Berg"`` already does -- where the #411 fix above read given ``abd``. Four of the 1025 differential corpus names move. The two connective-join names move at the 2.0.0 and 2.1.0 baselines alike; at 1.4.0 both were already classified as maiden-marker changes, 1.4 having no maiden field. ``"Juan y Garcia née Jones"`` and ``"Jane née Jr y Jones"``, the examples this fix adds to the rules doc and so to the rules corpus, move at all three and at 2.0.0/2.1.0 respectively. The eight corpus names that lose a family only under an APPENDED clause are not in the corpora in that form, so the gate cannot see them; the case table pins that half (closes #412, closes #417, closes #418) + - Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname. A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words -- the three-word test that keeps a single-letter connective a name word, and the bound given-name reserve -- counted them, because the pass that removes them ran after those rules. ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, but ``"juan y garcia nee jones"`` counted five words, joined the ``y``, and read given ``juan y garcia`` with NO family name at all; ``"John e Smith nee jones"``, ``"Lt.Gov. juan e garcia nee jones"`` and six more differential corpus names lose the family the same way under an appended clause. The marker pass now runs before every join, so the joins and the counts see only the name that remains, and a name of two or more name words reads as it reads without its maiden clause, plus the maiden name: ``"juan y garcia nee jones"`` is given ``juan``, middle ``y``, family ``garcia``, maiden ``jones``. (A title plus ONE name word is the #410 shape, fixed separately below: ``"Dr. Jane née Smith"`` reads family ``Jane`` now, as ``"Dr. Jane"`` does.) The same order closes the last of the join-swallows. The connective join used to merge the marker into a multi-word piece before the marker rule could see it, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` with no maiden name; it now reads family ``van der Berg``, maiden ``y Jones`` -- the marker takes the words after it, connective included -- and ``"Jane née and Jones Smith"`` reads maiden ``and Jones Smith`` where it read middle ``née and Jones``, family ``Smith`` -- the one name word the marker leaves goes to ``family`` since #445 below, and read as given ``Jane`` until it did. It also retires the particle chain's marker stop from the #399 fix above, whose condition restated the marker rule's and disagreed with it one suffix later (``"Jane van der Berg née Jr Jones"``, #417): a marker the rule declines -- nothing after it but a suffix -- is an ordinary word and rides inside the chain, which is the reading ``"Jane van der Berg née"`` already had, and the parsed fields of that name do not change. Two limits. A bound given-name word still never joins onto a marker standing as a word of its own, so ``"Berg, abdul née PhD"`` keeps given ``abdul``; a declined marker the particle chain has taken travels with the chain, so ``"Abd van der Berg née Jr Jones"`` reads given ``Abd van der Berg née`` where it read given ``Abd van der Berg``, middle ``née Jr``. And the marker rule now sees the words as written rather than as joined, so a suffix-vocabulary word inside the maiden name stops it even with a connective beside it: ``"Jane née Jr y Jones"`` read maiden ``Jr y Jones`` and now reads family ``Jr y Jones`` with no maiden name, and ``"Jane Smith née Jones Jr y Smith"`` read maiden ``Jones Jr y Smith`` and now reads maiden ``Jones``, family ``Jr y Smith``. One consequence: ``"abd née Jones Jr Smith Berg"`` reads given ``abd Jr`` -- as ``"abd Jr Smith Berg"`` already does -- where the #411 fix above read given ``abd``. Four of the 1025 differential corpus names move. The two connective-join names move at the 2.0.0 and 2.1.0 baselines alike; at 1.4.0 both were already classified as maiden-marker changes, 1.4 having no maiden field. ``"Juan y Garcia née Jones"`` and ``"Jane née Jr y Jones"``, the examples this fix adds to the rules doc and so to the rules corpus, move at all three and at 2.0.0/2.1.0 respectively. The eight corpus names that lose a family only under an APPENDED clause are not in the corpora in that form, so the gate cannot see them; the case table pins that half (closes #412, closes #417, closes #418) - Fix a title-plus-surname name losing its family name whenever anything stood beside it. ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A title followed by a single name word names the family, and that rule declined whenever the name also carried a suffix, a nickname or a maiden name -- it counted them as further name words, which none of them is: each stands beside the name rather than in it, and none makes the name any longer. The rule now counts name words alone. ``"Dr. Smith née Jones"`` reads family ``Smith``, maiden ``Jones``; ``"Dr. Smith PhD"`` family ``Smith``, suffix ``PhD``; ``"Dr. "Smitty" Smith"`` family ``Smith``, nickname ``Smitty``; and the particle spelling this was found through, ``"Freiherr von Richthofen geb. Albrecht"``, reads family ``von Richthofen``, maiden ``Albrecht`` -- 2.2's own particle-chain stop had routed that canonical shape into the rule for the first time (#399). A given-name title is unchanged and still names no family: ``"Sir John née Jones"`` keeps given ``John`` with an empty family, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS, rather than standing beside a name that already reads: ``"'Smitty' Dr. Jones"``. The lone-word nickname rule declines there because the title counts against its piece count, which leaves a title and one name word for this fix to read as the family -- given ``Jones`` through 2.1, family ``Jones`` now. Five names of the differential corpora change reading, the same five at every baseline. Four of them arrive as new diffs and are classified with this fix: ``"Dr. Smith née Jones"``, ``"Senator "Rick" Edmonds"``, ``"Xyz. (Bud) Smith"`` and ``"'Smitty' Dr. Jones"``. The fifth, ``"Freiherr von Richthofen V"``, was already classified under 2.2's particle-chain fix and stays there -- it is the suffix flavor in corpus form, and reads family ``von Richthofen`` with suffix ``V`` where it read given ``von Richthofen`` before. v1 read the two nicknames as 2.1 did; it had no maiden support at all, reading ``"Dr. Smith née Jones"`` as first ``Smith``, middle ``née``, last ``Jones``. The v1 test suite shipped the correct reading for the nickname shape as a known-failing test, which now passes (closes #410) + - Fix a name that is a surname and a maiden clause reporting no family at all. ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one -- nobody writes a maiden clause where there is no surname for it to set apart -- so where the marker has taken its name and a single name word is left standing, that word is the surname the bearer uses now. With one name word and nothing else to go on the library has to pick a field and picks ``given``, which is a convention rather than a determination and is now written down as such; a maiden marker joins the title and the nickname as one of the things that decides such a name instead. Every spelling of the shape moves, wherever the clause stands and however it is written -- a marker taking the words after it, a marker inside a bracket, or a bracket pair you declared to mean maiden, which carries no marker at all: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, which RESTORES 1.4.0 -- v1 read family ``Smith`` with the clause as a nickname, and only the clause changes hands now. ``"Jane née Jones Smith"``, where the marker stands INSIDE the name and takes the rest of it, reads family ``Jane``, maiden ``Jones Smith``, which no earlier version read (1.4.0 gave first ``Jane``, middle ``née Jones``, last ``Smith``): the rule counts what the clause LEAVES rather than where the marker stood. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``, and ``"'Smitty' Jones Jr. née Smith"`` reads family ``Jones``. Two shapes deliberately do NOT move, and both witnesses are names the differential corpora already carried rather than cases constructed for the release: a word the vocabulary claims as a given name keeps that reading, so ``"abd née Jones"`` still reads given ``abd``, and so does a word written as an initial, so ``"J. née Jones Smith V"`` still reads given ``J.`` -- this rule changes what POSITION decided and does not reach what a word already is. A title still decides the name behind it: ``"Dr. Smith née Jones"`` reads family ``Smith`` as it does since the title fix above, and the given-name title in ``"Sir John née Jones"`` still keeps given ``John`` with no family. A name with two name words beside the clause is untouched -- ``"Jane Smith née Jones"`` reads given ``Jane``, family ``Smith``, maiden ``Jones`` as before. If you have code that reads the lone name word beside a maiden clause out of ``given``, this is the release where it moves to ``family``. Nine of the 1,090 differential corpus names change reading: eight arrive as new diffs at every baseline and are classified with this fix, and the ninth is ``"Smith (née Jones)"``, which every released version already read family ``Smith`` -- so its diff SHRANK, at all three baselines, to the clause changing hands (closes #445) + **Deprecations** - Rename the four vocabularies whose 1.x names described the fields they feed in v1's words, so the data layer matches the ``Lexicon``: diff --git a/nameparser/_pipeline/_assign.py b/nameparser/_pipeline/_assign.py index 96f5a8ff..167c2b0c 100644 --- a/nameparser/_pipeline/_assign.py +++ b/nameparser/_pipeline/_assign.py @@ -146,6 +146,14 @@ def _name_positions(order: tuple[Role, Role, Role], family, middles..., given. One piece takes order[0]'s role; two pieces take order[0] and the other primary.""" first, second = order[0], order[1] + # rules.md#O5: "a name of one name word that nothing else has + # decided reads that word as the given name under the default + # given-first order, and as the family name under a declared + # family-first one" -- a convention, not a determination: O4 has + # no positions to compare at one word, so this line is where the + # library picks one of two equally consistent readings and picks + # it the same way every time. The rules that DO decide such a + # name (H1, N3, M4) run after this and retag. if count == 1: return [first] if first is Role.GIVEN: # GIVEN_FIRST diff --git a/nameparser/_pipeline/_post_rules.py b/nameparser/_pipeline/_post_rules.py index 6c1436be..c0c18d79 100644 --- a/nameparser/_pipeline/_post_rules.py +++ b/nameparser/_pipeline/_post_rules.py @@ -8,8 +8,8 @@ Reads: Policy.patronymic_rules, Policy.middle_as_family; Lexicon.given_name_titles. -Implements rules H1, P1, O1, O2 and O3 of docs/design/rules.md; each -is cited at its code below, and H1/P1/O1/O2's history lives in +Implements rules H1, M4, P1, O1, O2 and O3 of docs/design/rules.md; +each is cited at its code below, and H1/P1/O1/O2's history lives in docs/design/decisions.md. """ from __future__ import annotations @@ -40,6 +40,11 @@ _NAME_ROLES = (Role.GIVEN, Role.MIDDLE, Role.FAMILY) +#: M4's two carve-outs, as the tags classify recorded them: a bound +#: given-name word is vocabulary claiming the word as a given name, +#: and `initial` is the shape claim. Neither is a predicate M4 owns. +_NEVER_FLIPPED = frozenset({"vocab:bound-given", "initial"}) + def _idx(tokens: list[WorkToken], role: Role) -> list[int]: return [i for i, t in enumerate(tokens) if t.role is role] @@ -194,6 +199,46 @@ def post_rules(state: ParseState) -> ParseState: middles = _idx(tokens, Role.MIDDLE) families = _idx(tokens, Role.FAMILY) + # rules.md#M4: "a maiden name standing beside exactly one name + # word makes that word the family name, whatever suffix or + # nickname stands beside it" (#445) -- the MAIDEN role, not a + # marker: M1's configured pair produces one with no marker + # anywhere ("Smith (Jones)" under maiden_delimiters), and the + # rationale carries over unchanged, so the rule is keyed on the + # maiden name as the statement is. + # + # H1's sibling, and placed under it so the interaction the rule + # states is decidable by reading: where H1 fired, `givens` is + # empty here and this cannot fire; where H1 declined because the + # title addresses by given name, `titles` is what keeps this rule + # off the same word. A titled name is H1's at both outcomes. + # + # Sibling EXCEPT in what it counts, and that is a known gap + # rather than a boundary: this guard counts GIVEN tokens where + # H1 counts nothing (it tests which roles are unoccupied), so a + # name word another rule joined counts as several here and the + # rule declines -- `Dr. Dean of Chemistry` reads family, while + # `Dean of Chemistry née Jones` keeps given 'Dean of Chemistry'. + # rules.md#P3 says a joined part is one name word wherever + # another rule counts them, so the two disagree; widening moves + # no corpus name and is not this change's to make + # (decisions.md#M4). + # rules.md#M4: "a word the vocabulary has claimed as a given name + # keeps that reading, and so does a word written as an initial" + # -- read off the tags classify already recorded rather than a + # predicate of this rule's own, because this rule changes what + # POSITION decided and must not reach what a word IS + # (mechanisms.md#TWO-LAYER-ASSIGN). + if (not titles and len(givens) == 1 and not middles and not families + and any(t.role is Role.MAIDEN for t in tokens) + and not (_NEVER_FLIPPED & tokens[givens[0]].tags)): + _retag(tokens, givens[0], Role.FAMILY) + # recomputed for H1's reason, stated at H1: a stale index list + # is the bug shape #359 fixed + givens = _idx(tokens, Role.GIVEN) + middles = _idx(tokens, Role.MIDDLE) + families = _idx(tokens, Role.FAMILY) + # rules.md#P1: "a never-given particle standing alone where the # given name would go — or opening the name — marks the name as # surname-only: the particle run and the name words it attaches diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 7c2334f1..3db72716 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -1197,10 +1197,18 @@ def __post_init__(self) -> None: {"given": "abd", "maiden": "Jones"}, classification="fix(#411)", notes="the shortest form of the same decision: the very next " - "piece is the marker. Empty family is M2's ordinary " - "one-name-word behaviour, not a leftover of the join " - "-- 'Jane née Jones' reads the same way and always " - "has"), + "piece is the marker. It is also M4's boundary on the " + "vocabulary side, and a corpus name rather than a " + "constructed one: 'abd' is a bound given-name word, so " + "the vocabulary layer has claimed it as a GIVEN name " + "and M4 -- which changes only the positional default " + "-- does not reach it (mechanisms.md#TWO-LAYER-ASSIGN). " + "Dropping that carve-out reads family 'abd'. The empty " + "family is what M4 leaves standing here, not a " + "leftover of the join, and not M2's ordinary " + "one-name-word behaviour either -- since #445 'Smith " + "née Jones' reads family 'Smith', and this row is why " + "'abd née Jones' does not"), Case("bound_given_reserve_arabic_script", "عبد Berg née Jones", {"given": "عبد", "family": "Berg", "maiden": "Jones"}, @@ -1229,11 +1237,15 @@ def __post_init__(self) -> None: "connective_join_never_reaches_a_taken_marker"), Case("maiden_marker_ahead_of_a_conjunction", "Jane née and Jones Smith", - {"given": "Jane", "maiden": "and Jones Smith"}, - classification="fix(#412)", + {"family": "Jane", "maiden": "and Jones Smith"}, + classification="fix(#445)", notes="M2's greedy reading, with a connective among the " "words taken: the same as 'Jane née Jones Smith' with " - "'and' inside it. Until #412 closed, P3's join ran " + "'and' inside it -- which is why M4 reads it the same " + "way too, the take leaving one name word either way " + "(the family was empty here until #445; 1.4.0 read " + "first 'Jane' / middle 'née and Jones' / last 'Smith', " + "measured 2026-08-27). Until #412 closed, P3's join ran " "first and produced a marker-HEADED piece 'née and " "Jones' that the lone-piece test could not see, so the " "name read middle 'née and Jones', family 'Smith'. " @@ -1519,30 +1531,98 @@ def __post_init__(self) -> None: "Parity: 1.4.0 and 2.1.0 both read nickname 'Jones née'"), Case("marker_led_clause_with_one_name_word", "Smith (née Jones)", - {"given": "Smith", "maiden": "Jones"}, - classification="fix(#335)", + {"family": "Smith", "maiden": "Jones"}, + classification="fix(#445)", notes="N3's shape meeting M3, and the row exists because the " "two rules disagree about what a clause is. N3 reads a " "name that is only a nickname plus one name word as " "'that word is the family name' -- but a marker-led " "clause is not a nickname clause, so N3 never sees this " - "one and 'Smith' keeps the given-name reading the bare " - "spelling gives it. Recorded as an N3 Accepted line. " - "The empty family is the bare path's, not this " - "change's: 'Smith née Jones' reads given 'Smith', " - "family '' on 2.1.0 too, and 1.4.0 read first 'Smith' / " - "middle 'née' / last 'Jones' -- the marker as a " - "middle name. The parallel evidence at 1.4.0 is not " - "the fix(#410) maiden-flavor rule, which is keyed on " - "the TITLED 'Dr. Smith née Jones', but fix(#274), " - "which claims the bare one-word names 'Janey née " - "Jones' and 'abd née Jones' and declares `family` for " - "exactly the reason this row records. The given side " - "has agreed since 1.4.0, " - "the emptying is 2.x's and is tracked separately as the " - "#410 analogue. What #335 moves is only the bracketed " - "spelling: 1.4.0, 2.0.0 and 2.1.0 all read it family " - "'Smith', nickname 'née Jones' (measured 2026-08-26)"), + "one, and until #445 the word kept the given-name " + "reading the bare spelling gives it. M4 now reaches " + "both spellings from the other side: the marker " + "announces a FORMER surname, so the one name word left " + "beside it is the current one. The bracketed spelling " + "RESTORES the family EVERY released version read: " + "1.4.0, 2.0.0 and 2.1.0 all read this name family " + "'Smith', nickname 'née Jones' (measured on the wheels " + "2026-08-27), so the clause reading maiden rather than " + "nickname is #335's half and the only thing left that " + "differs -- at all three baselines alike. Be exact " + "about which spelling read `given`, because a ledger " + "took the loose wording this note used to carry and " + "narrowed one baseline where three needed it: the BARE " + "spelling is what read given 'Smith' on 2.0.0, on " + "2.1.0 and here until this rule (1.4.0 read it first " + "'Smith' / middle 'née' / last 'Jones'). The bracketed " + "spelling this row holds never read `given` on any " + "released version"), + Case("maiden_marker_makes_the_lone_name_word_the_family", + "Smith née Jones", + {"family": "Smith", "maiden": "Jones"}, + classification="fix(#445)", + notes="the rule, in its bare spelling: a maiden marker marks " + "a surname the bearer no longer uses, so it only means " + "anything beside one they do. With exactly one name " + "word left after the take, that word is the current " + "surname, and the positional convention O5 would " + "otherwise apply (a lone name word is read given) is " + "the thing M4 overrides. Read given 'Smith', family '' " + "on 2.0.0 and 2.1.0; 1.4.0 had no maiden support and " + "read first 'Smith' / middle 'née' / last 'Jones' " + "(measured 2026-08-27), so this is a new reading and " + "not a restoration -- the bracketed sibling " + "marker_led_clause_with_one_name_word is the " + "restoration"), + Case("maiden_marker_interior_makes_the_lone_name_word_the_family", + "Jane née Jones Smith", + {"family": "Jane", "maiden": "Jones Smith"}, + classification="fix(#445)", + notes="the marker standing INSIDE the name, where M2's take " + "runs to the end and swallows the rest -- so what is " + "left is again one name word, and M4 counts what is " + "left rather than where the marker stood. The widest " + "half of the rule and the row that pins it: a guard " + "that asked for the marker to be trailing would leave " + "this one given 'Jane'. A new reading, and the one " + "furthest from 1.4.0, which read first 'Jane' / middle " + "'née Jones' / last 'Smith' (measured 2026-08-27) -- " + "the real surname there is 'Smith', which 2.x reads as " + "part of the maiden name (M2's greedy take, unchanged " + "here)"), + Case("maiden_marker_lone_name_word_with_suffix", + "Smith née Jones PhD", + {"family": "Smith", "suffix": "PhD", "maiden": "Jones"}, + classification="fix(#445)", + notes="an annotation is not a name word, which is what #410 " + "established for H1 and M4 inherits: the credential " + "stands beside the name and does not make it any " + "longer, so the count that decides this reading is " + "one either way. A guard written over roles generally " + "rather than the three name roles reads this name as " + "two words and declines. 1.4.0 read first 'Smith' / " + "middle 'née' / last 'Jones' / suffix 'PhD' (measured " + "2026-08-27)"), + Case("maiden_marked_lone_initial_stays_given", + "J. née Jones Smith V", + {"given": "J.", "maiden": "Jones Smith V"}, + classification="fix(#274)", + notes="M4's boundary on the shape side, and a corpus name " + "rather than a constructed one: an initial is not a " + "family name, so the word the vocabulary layer has " + "already claimed as a shape keeps its reading " + "(mechanisms.md#TWO-LAYER-ASSIGN -- M4 changes the " + "POSITIONAL default and must not reach a word another " + "layer has claimed). Dropping the carve-out reads " + "family 'J.'. Unchanged by #445, and the maiden value " + "is M2's: the trailing 'V' is S2's suffix reading only " + "where a name word precedes it, and an initial does " + "not count, so the numeral stays maiden text (M2 " + "carries the same input as a boundary example). The " + "fix classification is #274's marker consumption, " + "which is what makes this differ from 1.4.0 (first " + "'J.' / middle 'née Jones' / last 'Smith' / suffix " + "'V', measured 2026-08-27)"), Case("marker_led_clause_in_a_quote_pair", 'Jane Smith "née Jones"', {"given": "Jane", "family": "Smith", "maiden": "Jones"}, diff --git a/tests/v2/test_ledger_guards.py b/tests/v2/test_ledger_guards.py index 5cfe5cf3..facc1a4b 100644 --- a/tests/v2/test_ledger_guards.py +++ b/tests/v2/test_ledger_guards.py @@ -940,6 +940,31 @@ class _LatinCopy(NamedTuple): # begins, and the alternation is over ANCHORS, not over words -- # there is no vocabulary here to drift from. frozenset({"^", ",\\s*"}), + # fix(#445)'s movers, one corpus name per alternative -- a list of + # names, not a copy of any wordlist, so there is no vocabulary for + # it to drift from. Two sets because the ledgers group the nine + # movers differently. At 1.4.0 seven of them move the same four + # fields and share the first set; the eighth ('John née Jones + # Smith Ma') moves those plus `suffix`, and the ninth ('Smith + # (née Jones)') keeps the rule it already had. At 2.0.0/2.1.0 the + # second set holds six that move two fields: five of the 1.4 + # seven, plus that acronym name, whose acronym is no part of the + # diff at those baselines. The two the second set drops are the + # compounds with #412 and #424, each with a rule of its own. + # + # Spelled out rather than written as the shape -- one word, then a + # marker -- because that shape also matches rules.md#M4's two + # carve-outs, 'abd née Jones' and 'J. née Jones Smith V'. A rule + # covering those two while declaring `given` would stand ready to + # explain the very given -> family move they exist to prevent, + # which is the absorption these rosters exist to catch. + frozenset({"Janey n[ée]e Jones", "Jane n[ée]e Jones J\\. V", + "Jane n[ée]e Jones Smith", "Jane n[ée]e and Jones Smith", + "John n[ée]e Jones Smith V", "Smith n[ée]e Jones", + "Smith n[ée]e Jones PhD"}), + frozenset({"Janey n[ée]e Jones", "Jane n[ée]e Jones J\\. V", + "Jane n[ée]e Jones Smith", "John n[ée]e Jones Smith Ma", + "Smith n[ée]e Jones", "Smith n[ée]e Jones PhD"}), }) def _unjustified_reach(name_regex: str, members: set[str]) -> list[str]: @@ -1320,7 +1345,7 @@ def _claim(rule: dict) -> _Claim: _CORPUS_CLAIMS: dict[str, dict[str, _Claim]] = { "expected_since_1.4.0.toml": { "fix(#335) a marker-led clause leaves the one name word its bare reading": - _Claim(1, ('family', 'given', 'maiden', 'nickname'), "c09cc7dba88b"), + _Claim(1, ('maiden', 'nickname'), "c09cc7dba88b"), "fix(#434) a multi-word maiden marker takes the maiden name": _Claim(1, ('family', 'maiden', 'middle'), "c428798fc6ef"), "fix(#434) a multi-word marker leads a bracketed clause to the maiden name": @@ -1336,7 +1361,7 @@ def _claim(rule: dict) -> _Claim: "fix(#271/#272/#298) native-script CJK: family-first order, hangul segmentation, the kana license and the dots": _Claim(108, ('family', 'given', 'middle'), "9a814f70c2dc"), "fix(#274) maiden markers consumed": - _Claim(29, ('family', 'maiden', 'middle'), "c0981c1c6557"), + _Claim(31, ('family', 'maiden', 'middle'), "67e2280be79d"), "fix(cjk-maiden-marker) maiden marker consumed, compounding with the CJK order flip": _Claim(5, ('family', 'given', 'maiden', 'middle'), "bc0e10dd7ec8"), "fix(#379) a tussenvoegsel after a family comma attaches to the family": @@ -1364,7 +1389,7 @@ def _claim(rule: dict) -> _Claim: "fix(comma-precomma-family) pre-comma run reads as family, not given": _Claim(279, ('family', 'given'), "28a62b622a48"), "fix(suffix-routing) two-token name with unambiguous trailing suffix stays suffix": - _Claim(1085, ('family', 'given', 'suffix'), "0df8e4a51a54"), + _Claim(1090, ('family', 'given', 'suffix'), "89e0b6d7f4c8"), "fix(suffix-delimiter-rendering) no-space delimiter core token kept whole": _Claim(0, ('suffix',), "e3b0c44298fc"), "ambiguous-surname-acronym data change: parenthesized (MA)/(DO) now stays nickname": @@ -1421,8 +1446,8 @@ def _claim(rule: dict) -> _Claim: _Claim(1, ('given', 'middle'), "2010cc79a34d"), "fix(#424) the particle chain stops before the trailing numeral": _Claim(1, ('family', 'suffix'), "2c99162bc9cf"), - "fix(#424) accepted: the maiden walk keeps a bare acronym": - _Claim(1, ('family', 'maiden', 'middle', 'suffix'), "f2c6cd2e3001"), + "fix(#424/#445) accepted: the maiden walk keeps a bare acronym, and the lone name word is the family": + _Claim(1, ('family', 'given', 'maiden', 'middle', 'suffix'), "f2c6cd2e3001"), "fix(#424) an unlisted abbreviation is as transparent as a listed title to the leading particle, the P4 example": _Claim(1, ('family', 'given'), "42b69cf1b320"), "fix(#424) accepted: the maiden walk keeps the numeral an initial before the marker vetoes": @@ -1435,10 +1460,12 @@ def _claim(rule: dict) -> _Claim: _Claim(1, ('family', 'middle', 'suffix'), "a564b97f7162"), "fix(#360) ste moved into the never-given particles with mc": _Claim(1, ('family', 'given'), "e62caedec864"), + "fix(#445) a maiden marker makes the lone name word the family": + _Claim(7, ('family', 'given', 'maiden', 'middle'), "3de9ef12b4a8"), }, "expected_since_2.0.0.toml": { "fix(#335) a marker-led clause leaves the one name word its bare reading": - _Claim(1, ('family', 'given', 'maiden', 'nickname'), "c09cc7dba88b"), + _Claim(1, ('maiden', 'nickname'), "c09cc7dba88b"), "fix(#434) a multi-word maiden marker takes the maiden name": _Claim(1, ('family', 'maiden', 'middle'), "c428798fc6ef"), "fix(#434) a multi-word marker leads a bracketed clause to the maiden name": @@ -1539,8 +1566,12 @@ def _claim(rule: dict) -> _Claim: _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "a564b97f7162"), "fix(#424) an unlisted abbreviation is as transparent as a listed title to the leading particle, the P4 example": _Claim(1, ('_ambiguities', 'family', 'given'), "42b69cf1b320"), - "fix(#424) the maiden walk stops before the trailing numeral": - _Claim(1, ('_ambiguities', 'maiden', 'suffix'), "cbe5bdd97317"), + "fix(#424/#445) the maiden walk stops before the trailing numeral, and the lone name word is the family": + _Claim(1, ('_ambiguities', 'family', 'given', 'maiden', 'suffix'), "cbe5bdd97317"), + "fix(#445) a maiden marker makes the lone name word the family": + _Claim(6, ('family', 'given'), "f521c94c79fc"), + "fix(#445) the lone name word beside a marker a connective join no longer absorbs": + _Claim(1, ('family', 'given', 'maiden', 'middle'), "52544a41dd62"), "fix(#424) a marker followed only by the numeral is just a word": _Claim(1, ('_ambiguities', 'family', 'maiden', 'middle', 'suffix'), "aaf53040b071"), "fix(#369) the bound given-name join takes a particle-and-bound word, so no fork is reported": @@ -1552,7 +1583,7 @@ def _claim(rule: dict) -> _Claim: }, "expected_since_2.1.0.toml": { "fix(#335) a marker-led clause leaves the one name word its bare reading": - _Claim(1, ('family', 'given', 'maiden', 'nickname'), "c09cc7dba88b"), + _Claim(1, ('maiden', 'nickname'), "c09cc7dba88b"), "fix(#434) a multi-word maiden marker takes the maiden name": _Claim(1, ('family', 'maiden', 'middle'), "c428798fc6ef"), "fix(#434) a multi-word marker leads a bracketed clause to the maiden name": @@ -1637,8 +1668,12 @@ def _claim(rule: dict) -> _Claim: _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "a564b97f7162"), "fix(#424) an unlisted abbreviation is as transparent as a listed title to the leading particle, the P4 example": _Claim(1, ('_ambiguities', 'family', 'given'), "42b69cf1b320"), - "fix(#424) the maiden walk stops before the trailing numeral": - _Claim(1, ('_ambiguities', 'maiden', 'suffix'), "cbe5bdd97317"), + "fix(#424/#445) the maiden walk stops before the trailing numeral, and the lone name word is the family": + _Claim(1, ('_ambiguities', 'family', 'given', 'maiden', 'suffix'), "cbe5bdd97317"), + "fix(#445) a maiden marker makes the lone name word the family": + _Claim(6, ('family', 'given'), "f521c94c79fc"), + "fix(#445) the lone name word beside a marker a connective join no longer absorbs": + _Claim(1, ('family', 'given', 'maiden', 'middle'), "52544a41dd62"), "fix(#424) a marker followed only by the numeral is just a word": _Claim(1, ('_ambiguities', 'family', 'maiden', 'middle', 'suffix'), "aaf53040b071"), "fix(#369) the bound given-name join takes a particle-and-bound word, so no fork is reported": @@ -1910,11 +1945,15 @@ class _Excluded(NamedTuple): # 51 -> 54 as rules.md gained the bracketed Polish examples # (#434): 'Maria Kowalska (z domu Nowak)', 'Maria Kowalska # (z domu)', and the boundary 'Anna z (domu) Nowak' M2 gained - # when the clause-straddling defect was fixed. Growth in the - # corpus, not in the exclusion -- its regex is untouched -- and - # `absorbed_by` stayed empty, so no rule reaches the protected - # shape. - _Excluded(54, "e2924f45c9d8", ()), + # when the clause-straddling defect was fixed. 54 -> 55 for + # M4's markerless clause example, 'Smith (Jones)' (#445), + # which the exclusion costs nothing: under the default facade + # the pair is a nickname one, so the name reads family + # 'Smith', nickname 'Jones' exactly as 1.4.0 read it and has + # no diff to silence. Growth in the corpus, not in the + # exclusion -- its regex is untouched -- and `absorbed_by` + # stayed empty, so no rule reaches the protected shape. + _Excluded(55, "7ad8ff289eb2", ()), } diff --git a/tests/v2/test_parser.py b/tests/v2/test_parser.py index f9ba35a8..14a9e0cf 100644 --- a/tests/v2/test_parser.py +++ b/tests/v2/test_parser.py @@ -1378,8 +1378,16 @@ def test_the_clause_free_corpus_is_not_empty() -> None: already -- the delimiter strip, then the move from word membership to the marker predicate, which a phrase entry made necessary -- and the one-liner in that filter's comment recounts - every stage: 642 names before either, 632 after the strip, 629 - once the predicate decides. So the floor is what says a future + every stage: 646 names before either, 636 after the strip, 632 + once the predicate decides. Recounted 2026-08-27 by pasting it, + and recounted AGAIN in the review round, which is the point of + shipping the one-liner: the first recount was taken before the + last of #445's rules-corpus rows existed and came out one low at + every stage. #445 adds five rows in all, of which three clear the + marker predicate ('Garcia', 'Smith', 'Smith (Jones)'), so every + stage moves by three -- and the first two figures were already + one low before any of it, a corpus row having landed without the + recount this comment asks for. So the floor is what says a future widening emptied it. Deliberately far below today's count: this asks whether the filter still selects a corpus, not what the corpus holds.""" @@ -1413,15 +1421,52 @@ def test_a_maiden_clause_changes_nothing_else(name: str) -> None: Jane née Smith' given 'Jane' -- and no longer moves, so the assertion now covers every name that reaches the marker with something to stand behind. + + One class of name moves ON PURPOSE, and is asserted MOVING rather + than skipped: rules.md#M4 makes the clause decide a name that + holds exactly one name word, because a marker announces a former + surname and only means anything beside a current one. Fourteen + of these names are in that class ('Smith', 'Smith Jr.', "'Smitty' + Jones Jr.", 'John V', 'de' ...), and the flip they assert is + exactly given -> family with every other field standing still -- + which is the whole of what #445 changed, checked over the corpus + rather than at the six rows cases.py carries. + + The `flips` predicate below is a second reading of M4's guard, and + that is a maintenance cost taken deliberately rather than the + silent-drift hazard it resembles: it fails LOUDLY in both + directions -- narrow M4 and the flip assertion fails, widen it and + the stands-still assertion does. What it buys is the carve-out + witness. Review offered the cheaper form, asserting only that + (given, family) is either unchanged or moved wholesale, with no + predicate at all; under that form a name whose word the vocabulary + claims would be free to flip, and dropping the `vocab:bound-given` + carve-out would stop failing here on 'abdul'. The corpus is the + only place that name is asked. """ base = parse(name) if not (base.given or base.middle or base.family or base.title or base.suffix): pytest.skip("nothing before the marker at all: M2 leaves it a word") + # M4's guard, read off the base parse: one GIVEN token, no other + # name word, no title (a titled name is H1's), and neither + # carve-out tag. Tokens rather than fields because the rule counts + # tokens -- a joined 'de la Vega' in `given` is one field and + # three of them. + givens = [t for t in base.tokens if t.role is Role.GIVEN] + flips = (len(givens) == 1 and not base.middle and not base.family + and not base.title + and not ({"initial", "vocab:bound-given"} & givens[0].tags)) with_clause = parse(name + " née Jones") assert with_clause.maiden == "Jones" - for field in ("title", "given", "middle", "family", "suffix", - "nickname"): + moved = ("title", "middle", "suffix", "nickname") if flips else ( + "title", "given", "middle", "family", "suffix", "nickname") + if flips: + assert (with_clause.family, with_clause.given) == (base.given, ""), ( + f"{name!r}: one name word beside a maiden clause is the " + f"family name (rules.md#M4), but it reads family " + f"{with_clause.family!r} / given {with_clause.given!r}") + for field in moved: assert getattr(with_clause, field) == getattr(base, field), ( f"{name!r}: {field} reads {getattr(base, field)!r} alone and " f"{getattr(with_clause, field)!r} with a maiden clause") diff --git a/tools/differential/compare.py b/tools/differential/compare.py index 440a470b..f065906b 100644 --- a/tools/differential/compare.py +++ b/tools/differential/compare.py @@ -356,9 +356,9 @@ def _is_latin_only(name: str) -> bool: #: decision when a corpus is added, the way the Script tables do. _CORPUS_FLOORS = { "corpus.jsonl": 480, # 486 today, from v1's banks at a pinned ref - "corpus_cjk.jsonl": 95, # 97 today, generated from the case table + "corpus_cjk.jsonl": 95, # 98 today, generated from the case table "corpus_issues.jsonl": 370, # 381 today, harvested and append-only - "corpus_rules.jsonl": 150, # 175 today, generated from rules.md + "corpus_rules.jsonl": 150, # 234 today, generated from rules.md } diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 655bbdb3..f7543f31 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -36,6 +36,7 @@ "Freiherr von Berg MA" "Freiherr von Richthofen V" "Gal·la Serra" +"Garcia" "Garcia Juan Carlos" "Hans „Erster“ und “Zweiter” Müller" "Hassan Mohamad Ali" @@ -139,9 +140,13 @@ "Sir John" "Sir abdul van der Berg" "Sir de Mesnil" +"Smith" +"Smith (Jones)" "Smith (née Jones)" "Smith Jr." "Smith Jr., Mr." +"Smith née Jones" +"Smith née Jones PhD" "Smith, Abd" "Smith, Dr." "Smith, Dr. Jr." diff --git a/tools/differential/expected_since_1.4.0.toml b/tools/differential/expected_since_1.4.0.toml index 7916b1da..b94aa73c 100644 --- a/tools/differential/expected_since_1.4.0.toml +++ b/tools/differential/expected_since_1.4.0.toml @@ -132,30 +132,57 @@ issue = "fix(#274) maiden markers consumed" # do carry marker vocabulary, so the presence guard is satisfied. # Hence the two context tests, which say what this rule has always # meant: a marker with a name word BEFORE it and one AFTER it. Reach -# 16 -> 11, and all seven names it explains are inside the eleven. -# -# Four claimed names still do not move, recorded rather than excluded: -# 'Jane Smith, nee Jones' (post-comma, M2's remaining Accepted limit), -# 'Jane van der Berg nee PhD' (only a suffix follows, so the consumer -# declines), and the connective and bound-given joins (#412). Each -# would need the regex to ask a vocabulary question -- suffix or -# connective membership -- a second alternation this rule cannot -# spend. -# -# revisit when #445 lands, and this is the half of that shape the -# bracketed fix(#335) rules do NOT cover. This rule declares `family`, -# and eight names it claims read family '' because the marker takes -# every word after it -- the bare spelling of exactly what #445 asks -# about. If #445 lands as "keep the family", their 1.4.0 diff stays a -# subset of {maiden, middle, family} and this rule keeps explaining -# them with the gate green, which is the silent absorption the -# four-field split exists to prevent, on the same field and the same -# baseline. The checklist to re-measure, all one name word before the -# marker: 'Janey née Jones' and 'abd née Jones' (the exact N3-parallel -# shape, one word after the marker too), plus 'Jane née Jones Smith', -# 'Jane née and Jones Smith', 'John née Jones Smith V', -# 'John née Jones Smith Ma', 'J. née Jones Smith V' and -# 'Jane née Jones J. V'. +# 16 -> 11 when they were added, and every name it explained then was +# inside the eleven -- figures from that measurement rather than from +# today's corpus, which has grown since. Today it explains 12 +# (measured 2026-08-27, after #445). +# +# Five claimed names still do not move, recorded rather than +# excluded: 'Jane Smith, née Jones' (post-comma, M2's remaining +# Accepted limit), 'Jane van der Berg née PhD' and 'Jane Smith née V' +# (only a suffix follows, so the consumer declines), and +# 'Jane née Jr y Jones' and 'Jane van der Berg née Jr Jones' (the +# connective and bound-given joins, #412). Each would need the regex +# to ask a vocabulary question -- suffix or connective membership -- +# a second alternation this rule cannot spend. The count said four +# until the review round re-measured it: the corpus grew and the +# sentence did not, which is this file's standing hazard and the +# reason every count in it carries the date it was taken. Measured +# 2026-08-27 against the 1.4.0 wheel, name by name over all 31 the +# regex claims. +# +# #445 landed 2026-08-27, and the prediction this note used to carry +# was wrong in a way worth keeping on the record. It reasoned that a +# "keep the family" fix would leave these diffs a subset of {maiden, +# middle, family}, so this rule would go on explaining them and would +# absorb the change in silence. The rule #445 actually chose MOVES +# the one name word rather than adding one: `given` empties as +# `family` fills, so every name that moved left this rule's fields +# and arrived UNEXPLAINED -- loud, at all three baselines. The note +# earned its keep all the same, and that is why it is corrected here +# rather than deleted: it named the right rule, the right field and +# the right eight names, and re-measuring its checklist is what +# showed its own reasoning wrong. +# +# Six of the eight moved: five by the fix(#445) rule at the end of +# this file, and 'John née Jones Smith Ma' by the fix(#424/#445) +# acronym rule above it, its diff carrying the suffix v1 read as +# well. The other two are rules.md#M4's own carve-outs, and their +# diffs are unchanged to the field, which is what makes them a +# boundary rather than an oversight. 'abd née Jones' -- whose word +# the bound-given vocabulary has claimed -- is still this rule's. +# 'J. née Jones Smith V' -- whose word is an initial -- is NOT, and +# never was: its 1.4.0 diff carries `suffix`, which these three +# fields cannot hold, so 'fix(#424) accepted: the maiden walk keeps +# the numeral an initial before the marker vetoes' is what explains +# it (asked of classify directly, 2026-08-27, rather than read off +# the regexes). The safety argument holds through that rule instead: +# it declares no `given` either, so a `given` move on either +# carve-out lands UNEXPLAINED rather than absorbed. +# +# The two other fix(#445) names, +# 'Smith née Jones' and 'Smith née Jones PhD', are corpus rows M4's +# own example lines added. name_regex = "(?i)(? maiden 'Jones Smith Ma': @@ -1084,8 +1111,19 @@ issue = "fix(#424) accepted: the maiden walk keeps a bare acronym" # it needs includes the words the marker removes). The fix(#274) rule # cannot carry it: its fields omit the suffix v1 read. A rules.md # Accepted example, first witnessed here. +# +# Renamed for #445 (2026-08-27) rather than widened in place, because +# the diff now has two causes and one rule has to explain the whole +# of it. What the walk leaves is a single name word, and rules.md#M4 +# makes that word the family: given 'John' -> family 'John', so +# `given` joins the four fields above. Attributing that to #424 alone +# would have been false, and leaving this rule at four fields would +# have made it explain nothing while the name went UNEXPLAINED -- +# which is exactly how the gate reported it before this edit. The +# name is #445's only mover whose diff is not the shape the fix(#445) +# rule below carries, the suffix v1 read being the difference. name_regex = "(?i)^john\\s+n[ée]e\\s+jones\\s+smith\\s+ma$" -fields = ["middle", "family", "suffix", "maiden"] +fields = ["given", "middle", "family", "suffix", "maiden"] [[change]] issue = "fix(#360) ste moved into the never-given particles with mc" @@ -1509,20 +1547,68 @@ issue = "fix(#335) a marker-led clause leaves the one name word its bare reading # N3 reads a name that is only a nickname plus one name word as "that # word is the family name", and a marker-led clause is not a nickname # clause, so N3 no longer reaches this shape: the word keeps the -# reading the bare "Smith née Jones" gives it, which is `given`. +# reading the bare "Smith née Jones" gives it. # # Its own rule rather than a sixth alternative in the fix(#335) rule -# above, for the reason that rule states: this diff moves FOUR fields -# where those move two, and one rule holding both would have to -# declare the union, pre-excusing a given/family regression on the -# five. The empty `family` is not this change's -- the bare spelling -# reads that way on 2.0.0 and 2.1.0 too -- but it is inside this -# diff, so the rule declares it. -# -# revisit when #445 lands: this rule declares `family`, and `family` -# is exactly the field #445 would change. Fixing it there moves this -# diff, and this rule would absorb the change in silence at all three -# baselines -- the same failure the four-field split exists to avoid, -# displaced onto a different field. Re-measure this rule then. +# above, for the reason that rule states: one rule holding both would +# have to declare the union of two different diffs and would +# pre-excuse whatever else landed inside it. +# +# The revisit note this block carried was answered on 2026-08-27, and +# the answer is the opposite of what it feared. It expected #445 to +# widen this diff and be absorbed here; #445 SHRANK it. The bare +# reading the word keeps is now `family` (rules.md#M4), and 1.4.0 +# read this name family 'Smith', nickname 'née Jones' -- so `given` +# and `family` agree with the baseline again and only the clause +# changes hands. The declaration is narrowed to the two fields that +# still move, which is what the note asked for: a `given` or `family` +# move on this name is UNEXPLAINED from here on rather than absorbed. +# Measured, not reasoned -- and the first pass got the 2.x half of +# that sentence wrong, saying those baselines read `given` when they +# read family 'Smith', nickname 'née Jones' like this one. Both are +# narrowed to two fields now (2026-08-27 review round). This name is +# the one shape in #445's class the gates do NOT report, which is +# why nothing but reading the ledger could catch it. +# +# What the narrowing buys differs by ledger, so do not read more into +# it here than it gives: at 2.0.0 and 2.1.0 a {given, family} diff on +# this name is now UNEXPLAINED, while at THIS baseline the fields-only +# fix(suffix-routing) rule below still matches every name with +# {given, family, suffix} and would take it. That catch-all is +# long-standing and out of #445's scope; the narrowing removes this +# rule from the path regardless, which is the part that was in +# scope. name_regex = "(?i)^Smith \\(n[ée]e Jones\\)$" -fields = ["given", "family", "nickname", "maiden"] +fields = ["nickname", "maiden"] + +[[change]] +issue = "fix(#445) a maiden marker makes the lone name word the family" +# rules.md#M4 -- "a maiden name standing beside exactly one name word +# makes that word the family name". A maiden name is a FORMER family +# name, which only means something beside a current one, so the word +# left standing beside it is the surname the bearer uses now. Seven +# names, one field set: 'Smith née Jones' +# read first 'Smith', middle 'née', last 'Jones' on 1.4.0 and reads +# family 'Smith', maiden 'Jones' here. +# +# Three of the four fields are #274's marker consumption -- v1 had no +# maiden support, so the marker sat in the name as a middle word and +# the last word was the family -- and the fix(#274) rule above +# carried these names until #445 added `given` to their diffs and +# they left its declaration. Only `given` is new; the rule declares +# all four because a rule explains a name's whole diff. +# +# Anchored, and spelled out one name per alternative, for the reason +# M4 itself gives. The shape regex a reader would write first -- one +# word, then a marker -- also matches M4's two carve-outs +# ('abd née Jones', 'J. née Jones Smith V'), and a rule declaring +# `given` over those two would stand ready to explain the very +# given -> family move the carve-outs exist to prevent. They stay +# with fix(#274), whose fields do not include it. +# +# 'Smith née Jones' and 'Smith née Jones PhD' are corpus rows this +# change itself added, being M4's own example lines; the other five +# were already here, and were five of the eight the fix(#274) note +# above listed as the checklist to re-measure. +name_regex = "(?i)^(?:Janey n[ée]e Jones|Jane n[ée]e Jones J\\. V|Jane n[ée]e Jones Smith|Jane n[ée]e and Jones Smith|John n[ée]e Jones Smith V|Smith n[ée]e Jones|Smith n[ée]e Jones PhD)$" +fields = ["given", "middle", "family", "maiden"] diff --git a/tools/differential/expected_since_2.0.0.toml b/tools/differential/expected_since_2.0.0.toml index d5336caf..82d7dd0d 100644 --- a/tools/differential/expected_since_2.0.0.toml +++ b/tools/differential/expected_since_2.0.0.toml @@ -625,7 +625,7 @@ name_regex = "(?i)^john\\s+van\\s+der\\s+berg\\s+ma$" fields = ["family", "suffix", "_ambiguities"] [[change]] -issue = "fix(#424) the maiden walk stops before the trailing numeral" +issue = "fix(#424/#445) the maiden walk stops before the trailing numeral, and the lone name word is the family" # 'John née Jones Smith V': rules.md#M2 -- "takes the words after it # -- up to any suffix word, or the trailing roman numeral assign # reads as the suffix (S2) -- as the maiden name". maiden 'Jones @@ -633,8 +633,18 @@ issue = "fix(#424) the maiden walk stops before the trailing numeral" # with the suffix-piece test too. No 1.4.0 twin: v1 had no maiden # support, and the fix(#274) rule carries the name there. A rules.md # example; no differential corpus name has the shape. +# +# Renamed for #445 (2026-08-27) rather than widened in place, because +# the diff has two causes now and one rule has to explain the whole +# of it. The walk leaves a single name word, and rules.md#M4 makes +# that word the family: given 'John' -> family 'John' joins the three +# fields above. Attributing that to #424 would be false, and leaving +# the declaration at three fields left this rule explaining nothing +# while the name went UNEXPLAINED -- which is how the gate reported +# it before this edit, at 2.0.0 and 2.1.0 alike. No 1.4.0 twin to +# rename: the fix(#445) rule there carries this name. name_regex = "(?i)^john\\s+n[ée]e\\s+jones\\s+smith\\s+v$" -fields = ["suffix", "maiden", "_ambiguities"] +fields = ["given", "family", "suffix", "maiden", "_ambiguities"] [[change]] issue = "fix(#424) a marker followed only by the numeral is just a word" @@ -824,7 +834,11 @@ issue = "fix(#412) a connective join no longer absorbs the maiden marker beside # # Checked against this baseline's own run: 2.0.0 reads both names the # way 2.1.0 does, so the same two arrive UNEXPLAINED here. The -# expected_since_2.1.0.toml copy carries the fuller account. +# expected_since_2.1.0.toml copy carries the fuller account. Since +# #445 (2026-08-27) this rule explains only 'Jane van der Berg née y +# Jones'; the other name is left with one name word once the marker +# takes the rest, so its diff gained `given` and it has a rule of its +# own at the end of this file, as it does at 2.1.0. name_regex = "(?i)\\bn[eé]e\\s+(y|and)\\b" fields = ["middle", "family", "maiden"] @@ -1035,20 +1049,86 @@ issue = "fix(#335) a marker-led clause leaves the one name word its bare reading # N3 reads a name that is only a nickname plus one name word as "that # word is the family name", and a marker-led clause is not a nickname # clause, so N3 no longer reaches this shape: the word keeps the -# reading the bare "Smith née Jones" gives it, which is `given`. +# reading the bare "Smith née Jones" gives it -- `given` when this +# rule was written, and `family` since #445 gave the bare spelling +# the same reading from the other side. # # Its own rule rather than a sixth alternative in the fix(#335) rule -# above, for the reason that rule states: this diff moves FOUR fields -# where those move two, and one rule holding both would have to -# declare the union, pre-excusing a given/family regression on the -# five. The empty `family` is not this change's -- the bare spelling -# reads that way on 2.0.0 and 2.1.0 too -- but it is inside this -# diff, so the rule declares it. +# above: one rule holding both would have to declare the union of two +# different diffs and would pre-excuse whatever else landed inside +# it. (The two now move the same two fields, but for different +# reasons -- that rule's names keep a family name throughout, while +# this one's gained one back from #445 -- and merging them would +# re-declare `given` over both.) # -# revisit when #445 lands: this rule declares `family`, and `family` -# is exactly the field #445 would change. Fixing it there moves this -# diff, and this rule would absorb the change in silence at all three -# baselines -- the same failure the four-field split exists to avoid, -# displaced onto a different field. Re-measure this rule then. +# The revisit note this block carried was answered on 2026-08-27 and +# answered WRONG, which the review round caught and this text +# records. The first answer said the diff was unchanged at this +# baseline "because the baseline is what read `given`". It did not: +# 2.0.0 and 2.1.0 both read 'Smith (née Jones)' as family 'Smith', +# nickname 'née Jones' (measured on the wheels, 2026-08-27) -- only +# the BARE spelling ever read `given`, and this rule is the +# bracketed one. So `given` is empty on both sides, `family` agrees +# again since #445, and the diff here shrank to {nickname, maiden} +# exactly as it did at 1.4.0. +# +# Narrowed to those two fields, which is what the revisit note asked +# for. Left at four, this rule stood over a two-field diff and +# classify's subset test would have explained a `given`/`family` +# regression on this name in silence -- the absorption the note was +# written to prevent, surviving the round that answered it. Verified +# after narrowing: the name is still explained, and a {given, family} +# diff on it is now UNEXPLAINED. name_regex = "(?i)^Smith \\(n[ée]e Jones\\)$" -fields = ["given", "family", "nickname", "maiden"] +fields = ["nickname", "maiden"] + +[[change]] +issue = "fix(#445) a maiden marker makes the lone name word the family" +# rules.md#M4 -- "a maiden name standing beside exactly one name word +# makes that word the family name". A maiden name is a FORMER family +# name, which only means something beside a current one, so the word +# left standing beside it is the surname the bearer uses now: +# 'Smith née Jones' read given 'Smith' with an +# empty family on this baseline and reads family 'Smith' here. +# +# Two fields and no more. The marker consumption these names also +# carry is this baseline's reading already, so `given` and `family` +# are the whole diff -- which is what makes them one rule, and what +# keeps out the two names whose diffs are compounds (the connective +# join below, the trailing numeral above). +# +# Anchored, and spelled out one name per alternative, for the reason +# M4 itself gives. The shape regex a reader would write first -- one +# word, then a marker -- also matches M4's two carve-outs +# ('abd née Jones', 'J. née Jones Smith V', both corpus names and +# neither moving), and a rule declaring `given` over those two would +# stand ready to explain the very given -> family move the carve-outs +# exist to prevent. +# +# 'Smith née Jones' and 'Smith née Jones PhD' are corpus rows this +# change itself added, being M4's own example lines. +# 'John née Jones Smith Ma' sits here rather than with an acronym +# rule of its own: this baseline already reads it maiden 'Jones Smith +# Ma', so the acronym is no part of the diff. At 1.4.0, where v1 read +# suffix 'Ma', it is the one name of this class that needs one. +name_regex = "(?i)^(?:Janey n[ée]e Jones|Jane n[ée]e Jones J\\. V|Jane n[ée]e Jones Smith|John n[ée]e Jones Smith Ma|Smith n[ée]e Jones|Smith n[ée]e Jones PhD)$" +fields = ["given", "family"] + +[[change]] +issue = "fix(#445) the lone name word beside a marker a connective join no longer absorbs" +# 'Jane née and Jones Smith', whose diff is a compound of #412 and +# #445 and so cannot sit in either's plain rule. #412 stopped the +# connective join from swallowing the marker, so the marker is taken +# and 'and Jones Smith' is the maiden name; what that leaves is one +# name word, and rules.md#M4 makes it the family. middle +# 'née and Jones', family 'Smith' -> family 'Jane', maiden +# 'and Jones Smith'. +# +# Its own rule rather than a widening of the fix(#412) rule above, +# which claims two names: the other one keeps a given name, and one +# rule holding both would have to declare `given` for both and +# pre-excuse its loss where losing it would be a regression. At 1.4.0 +# the fix(#445) rule carries this name with the same four fields, v1 +# having read the marker as a middle word. +name_regex = "(?i)^Jane n[ée]e and Jones Smith$" +fields = ["given", "middle", "family", "maiden"] diff --git a/tools/differential/expected_since_2.1.0.toml b/tools/differential/expected_since_2.1.0.toml index adf4880c..f1d50efa 100644 --- a/tools/differential/expected_since_2.1.0.toml +++ b/tools/differential/expected_since_2.1.0.toml @@ -318,7 +318,7 @@ name_regex = "(?i)^john\\s+van\\s+der\\s+berg\\s+ma$" fields = ["family", "suffix", "_ambiguities"] [[change]] -issue = "fix(#424) the maiden walk stops before the trailing numeral" +issue = "fix(#424/#445) the maiden walk stops before the trailing numeral, and the lone name word is the family" # 'John née Jones Smith V': rules.md#M2 -- "takes the words after it # -- up to any suffix word, or the trailing roman numeral assign # reads as the suffix (S2) -- as the maiden name". maiden 'Jones @@ -326,8 +326,18 @@ issue = "fix(#424) the maiden walk stops before the trailing numeral" # with the suffix-piece test too. No 1.4.0 twin: v1 had no maiden # support, and the fix(#274) rule carries the name there. A rules.md # example; no differential corpus name has the shape. +# +# Renamed for #445 (2026-08-27) rather than widened in place, because +# the diff has two causes now and one rule has to explain the whole +# of it. The walk leaves a single name word, and rules.md#M4 makes +# that word the family: given 'John' -> family 'John' joins the three +# fields above. Attributing that to #424 would be false, and leaving +# the declaration at three fields left this rule explaining nothing +# while the name went UNEXPLAINED -- which is how the gate reported +# it before this edit, at 2.0.0 and 2.1.0 alike. No 1.4.0 twin to +# rename: the fix(#445) rule there carries this name. name_regex = "(?i)^john\\s+n[ée]e\\s+jones\\s+smith\\s+v$" -fields = ["suffix", "maiden", "_ambiguities"] +fields = ["given", "family", "suffix", "maiden", "_ambiguities"] [[change]] issue = "fix(#424) a marker followed only by the numeral is just a word" @@ -731,6 +741,13 @@ issue = "fix(#412) a connective join no longer absorbs the maiden marker beside # # Checked against this baseline's own run, not copied: both names # arrive UNEXPLAINED here and at 2.0.0 alike, and nothing else does. +# Since #445 (2026-08-27) this rule explains only the first of the +# two: the marker in 'Jane née and Jones Smith' takes the rest of the +# name and leaves one name word, so `given` empties into `family` and +# the diff outgrew these three fields. It has a rule of its own at +# the end of this file. The names were split rather than the fields +# widened, because 'Jane van der Berg née y Jones' keeps a given name +# and adding `given` here would have pre-excused its loss. # At 1.4.0 fix(#274) already classifies them -- v1 has no maiden # field, so every marker-bearing name is a diff there. # @@ -957,20 +974,86 @@ issue = "fix(#335) a marker-led clause leaves the one name word its bare reading # N3 reads a name that is only a nickname plus one name word as "that # word is the family name", and a marker-led clause is not a nickname # clause, so N3 no longer reaches this shape: the word keeps the -# reading the bare "Smith née Jones" gives it, which is `given`. +# reading the bare "Smith née Jones" gives it -- `given` when this +# rule was written, and `family` since #445 gave the bare spelling +# the same reading from the other side. # # Its own rule rather than a seventh alternative in the fix(#335) -# rule above, for the reason that rule states: this diff moves FOUR -# fields where those move two, and one rule holding both would have to -# declare the union, pre-excusing a given/family regression on the -# six. The empty `family` is not this change's -- the bare spelling -# reads that way on 2.0.0 and 2.1.0 too -- but it is inside this -# diff, so the rule declares it. -# -# revisit when #445 lands: this rule declares `family`, and `family` -# is exactly the field #445 would change. Fixing it there moves this -# diff, and this rule would absorb the change in silence at all three -# baselines -- the same failure the four-field split exists to avoid, -# displaced onto a different field. Re-measure this rule then. +# rule above: one rule holding both would have to declare the union of +# two different diffs and would pre-excuse whatever else landed inside +# it. (The two now move the same two fields, but for different +# reasons -- that rule's names keep a family name throughout, while +# this one's gained one back from #445 -- and merging them would +# re-declare `given` over both.) +# +# The revisit note this block carried was answered on 2026-08-27 and +# answered WRONG, which the review round caught and this text +# records. The first answer said the diff was unchanged at this +# baseline "because the baseline is what read `given`". It did not: +# 2.0.0 and 2.1.0 both read 'Smith (née Jones)' as family 'Smith', +# nickname 'née Jones' (measured on the wheels, 2026-08-27) -- only +# the BARE spelling ever read `given`, and this rule is the +# bracketed one. So `given` is empty on both sides, `family` agrees +# again since #445, and the diff here shrank to {nickname, maiden} +# exactly as it did at 1.4.0. +# +# Narrowed to those two fields, which is what the revisit note asked +# for. Left at four, this rule stood over a two-field diff and +# classify's subset test would have explained a `given`/`family` +# regression on this name in silence -- the absorption the note was +# written to prevent, surviving the round that answered it. Verified +# after narrowing: the name is still explained, and a {given, family} +# diff on it is now UNEXPLAINED. name_regex = "(?i)^Smith \\(n[ée]e Jones\\)$" -fields = ["given", "family", "nickname", "maiden"] +fields = ["nickname", "maiden"] + +[[change]] +issue = "fix(#445) a maiden marker makes the lone name word the family" +# rules.md#M4 -- "a maiden name standing beside exactly one name word +# makes that word the family name". A maiden name is a FORMER family +# name, which only means something beside a current one, so the word +# left standing beside it is the surname the bearer uses now: +# 'Smith née Jones' read given 'Smith' with an +# empty family on this baseline and reads family 'Smith' here. +# +# Two fields and no more. The marker consumption these names also +# carry is this baseline's reading already, so `given` and `family` +# are the whole diff -- which is what makes them one rule, and what +# keeps out the two names whose diffs are compounds (the connective +# join below, the trailing numeral above). +# +# Anchored, and spelled out one name per alternative, for the reason +# M4 itself gives. The shape regex a reader would write first -- one +# word, then a marker -- also matches M4's two carve-outs +# ('abd née Jones', 'J. née Jones Smith V', both corpus names and +# neither moving), and a rule declaring `given` over those two would +# stand ready to explain the very given -> family move the carve-outs +# exist to prevent. +# +# 'Smith née Jones' and 'Smith née Jones PhD' are corpus rows this +# change itself added, being M4's own example lines. +# 'John née Jones Smith Ma' sits here rather than with an acronym +# rule of its own: this baseline already reads it maiden 'Jones Smith +# Ma', so the acronym is no part of the diff. At 1.4.0, where v1 read +# suffix 'Ma', it is the one name of this class that needs one. +name_regex = "(?i)^(?:Janey n[ée]e Jones|Jane n[ée]e Jones J\\. V|Jane n[ée]e Jones Smith|John n[ée]e Jones Smith Ma|Smith n[ée]e Jones|Smith n[ée]e Jones PhD)$" +fields = ["given", "family"] + +[[change]] +issue = "fix(#445) the lone name word beside a marker a connective join no longer absorbs" +# 'Jane née and Jones Smith', whose diff is a compound of #412 and +# #445 and so cannot sit in either's plain rule. #412 stopped the +# connective join from swallowing the marker, so the marker is taken +# and 'and Jones Smith' is the maiden name; what that leaves is one +# name word, and rules.md#M4 makes it the family. middle +# 'née and Jones', family 'Smith' -> family 'Jane', maiden +# 'and Jones Smith'. +# +# Its own rule rather than a widening of the fix(#412) rule above, +# which claims two names: the other one keeps a given name, and one +# rule holding both would have to declare `given` for both and +# pre-excuse its loss where losing it would be a regression. At 1.4.0 +# the fix(#445) rule carries this name with the same four fields, v1 +# having read the marker as a middle word. +name_regex = "(?i)^Jane n[ée]e and Jones Smith$" +fields = ["given", "middle", "family", "maiden"]