镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit 24691d0

Browse files
derek73claude
andcommitted
Address the PR review: one numeral report site, case-aware clause (#718)
Comment review: fix labels that contradict 1.4.0 ('John III' is no parity row, 'John III, Sir' is one, 'abdul Doe nee Smith V' is #274's), and comments this change made stale (assign's deferral note and module docstring, group's 'abdul V' reserve example, _vocab's claim that X is suffix vocabulary). Test review (mutation of each emitter in a copy): the particle chain's numeral report only chose a sentence -- assign reports the same kind from the final role -- so it is gone, as is the maiden clause's stand-down, which could never fire. The clause's report on a numeral it gives up now asks the case, so 'Jane Doe nee Smith vi' is silent as every other path is. New rows pin the chain-taken numeral, its already-an-initial stand-down, 'John VI', family-first 'John V', the lowercase pair 'John x'/'john x' and the maiden-clause case contrast; five rows restoring 1.4.0 field for field are relabelled parity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ed64d54 commit 24691d0

11 files changed

Lines changed: 135 additions & 60 deletions

File tree

‎docs/design/decisions/S10-trailing-roman-numeral.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
- 2026-10-18 (Derek), #718 — THE DECLINED NUMERAL REPORTS TOO, BOTH DIRECTIONS AS S4'S FORK DOES. The count's first form reported only the generation, so `John V` read family V in silence while `Jack Ma` reports keeping its family; Derek: report both, as `Jack Ma` does. A declined numeral is a pick of the walk, and its branches are taken in three places, so three sites report it (mechanisms.md#AMBIGUITY-AT-THE-DECISION-SITE): assign where it stays a lone name word (`John V` — "read as a family name rather than a generational suffix"), the particle chain where it joins the run (`Freiherr von Richthofen V`), and a maiden clause that keeps it as its last word (`John née Jones Smith V`, `abdul nee Smith V`). Each wording is its own, since the acronym pick's ("both a post-nominal and an ordinary name") misdescribes a numeral.
1010
ONE STAND-DOWN, FOUND BY INV10 rather than designed. In a one-case name the connective `i` reaches the fork as a letter classify has already reported as `conjunction-or-initial`, read as an initial, and the new report put a second report on the same token: 138 rows of the connective grid (measured on this branch with the `reported_as_initial` check out of assign's numeral report, a dated snapshot) in tests/v2/test_properties.py (`AND ROVIRA I`, `HENRY I`) tripped INV10's kind pair. The two reports did not contradict each other — both read the letter as a name word — but they named one reading twice, so the numeral's report stands down where the initial reading is already reported, the shape of rules/S7's "a member its own capitals already made the credential reports nothing new". The guard was left untouched: its kind pair still means "two readings" everywhere else, and narrowing it to suit this change would have retaken its recorded control. `HENRY I` therefore reports `conjunction-or-initial` alone, and moved in test_assemble's contrast list from the withdrawn side to the side where the fork resolved to a name word.
1111
- 2026-10-18, #718 review — THE COUNT IS NOT ASKED IN THE GIVEN PART AFTER A FAMILY COMMA. The code review's grid (family-comma and empty-head shapes behind a title-particle, about 558,000 parses) found 2,880 parses where the first form moved a numeral from the suffix into the given or family name with no report: `Smith, Freiherr von Berg V` read given 'von Berg V', `, Freiherr von V` family 'von V', `Smith, St van V` given 'van V', where `Smith, von V` and `Smith, Freiherr John V` kept suffix V. The cause was not a choice: assign's walk over that part never counts the numeral (rules/S4: the given part's trailing slot "is read without it (S5)"), but the particle chain's bound there asks `tail_reading`, which the count had reached, so the chain took a word assign would have read as the suffix. The bound now reads with the count off (`trailing_run_start`, `count_numeral=False`), and all four read as they did on master (2.2.0 onward, since #424; 1.4.0 read the numeral into the name). The class is the one the AGENTS.md gotcha on group-side predictors of the given part names, met from the other side: the predictor did not change, the peel it asks did. The S4 analogue, `Smith, Freiherr von Berg Ma` → given 'von Berg Ma', is master's reading and is left alone here.
12+
- 2026-10-18, #718's PR review — TWO OF THE THREE REPORT SITES ARE ONE, AND THE CLAUSE ASKS THE CASE. The test-coverage review mutated each new emitter out in a copy of the tree. Removing the particle chain's numeral report moved no field and no kind — assign's pick loop reported the same `suffix-or-name`, worded from the final role ('read as a family name') — so the chain's branch was only choosing a sentence, and it is gone: a declined numeral stays one of the read's picks, and assign alone reports it, the decision having been the read's and not the chain's. The maiden clause's stand-down on a letter already reported as an initial could not fire at all (classify reads no connective inside a clause), so that call is gone too. And the clause's report on a numeral its take gave up did not ask the case: `Jane Doe nee Smith vi` and `x` reported where every other path had gone silent, the drift mechanisms.md#ONE-PREDICATE-PER-QUESTION names; the take now asks `numeral_settled_by_case` as assign does, `VI` still reporting beside them. Five rows the first draft labelled `fix(#718)` read field for field as 1.4.0 does and are `parity`, as are the two Freiherr rows added here; a restored 1.4.0 reading is parity by the table's own definition (rules/S4's `bare_surname_is_not_a_credential` is the precedent).

‎docs/design/rules/S10-trailing-roman-numeral.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ S10. Rationale: a roman numeral ending a name is its generation,
5151
"john v" → family=""
5252
"john smith v" → ambiguities=()
5353
"john i" → family="i"
54+
"john i" → ambiguities=("conjunction-or-initial",)
55+
Accepted: inside a maiden marker's clause the connective is not
56+
read (M2), so a one-case `i` there is the suffix word the
57+
vocabulary lists, as `v` is.
58+
"john nee smith i" → suffix="i"
5459
Accepted: a numeral with another suffix word behind it is not
5560
the last word of the name and is read as any word there is, so a
5661
one-letter numeral before a credential is the family name.

‎nameparser/_pipeline/_assign.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
particle out of that position) -- whatever role name_order assigns --
6060
and, since #613, at P6's attachment after a family comma where the run
6161
holds an ambiguous particle ("Beethoven, Ludwig van").
62-
Emits SUFFIX_OR_NAME at these sites: the trailing roman numeral, each
62+
Emits SUFFIX_OR_NAME at these sites: the trailing roman numeral, taken
63+
or (since #718) declined for want of words to spare, each
6364
ambiguous acronym the trailing peel had to resolve, the bare-suffix
6465
carve-out where an input that is nothing but post-nominal vocabulary
6566
gets its first word made into the name (H4's suffix half, #491),
@@ -371,8 +372,9 @@ def _assign_main(seg_idx: int, state: ParseState,
371372
# rather than overwrites. Deferred to after assignment because what
372373
# a pick is declined as depends on the role it took, and which role
373374
# "not peeled" means depends on name_order; the field itself is
374-
# worded by assemble (#626). (The roman-numeral fork needs no such
375-
# deferral and is reported here.)
375+
# worded by assemble (#626). (The roman-numeral fork's TAKEN branch
376+
# needs no such deferral and is reported here; its declined branch
377+
# is a pick, #718, and is reported with the others.)
376378
#
377379
# Read once in group, after P3's connective joins and ahead of the
378380
# particle chain, on a main segment of three or more pieces whose

‎nameparser/_pipeline/_group.py‎

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@
7070
Peel, TailRead, chain_lead, chain_run_end, is_conj_piece, is_leading_title, is_prefix_piece,
7171
is_suffix_piece, is_title_piece, join_connectives, joined_tags,
7272
leading_titles, merge_particle_runs, merge_pieces, peel_walk,
73-
read_trailing_run, reported_as_initial, tail_reading,
74-
trailing_candidates,
73+
read_trailing_run, tail_reading, trailing_candidates,
7574
trailing_run_start, trailing_start,
7675
trailing_start_past_titles,
7776
)
@@ -82,7 +81,7 @@
8281
import nameparser._pipeline._comma as _comma
8382
from nameparser._pipeline._vocab import D, PH
8483
from nameparser._pipeline._vocab import (
85-
comma_follows_marker, delimiter_cores,
84+
comma_follows_marker, delimiter_cores, numeral_settled_by_case,
8685
)
8786
from nameparser._types import AmbiguityKind, Role
8887

@@ -333,9 +332,14 @@ def _maiden_take(pieces: Sequence[Sequence[int]],
333332
# -- the take consumed the run, so the forks the reading called
334333
# on it are reported here, where assign will never see them
335334
consumed = {i for j in range(end, len(pieces)) for i in pieces[j]}
336-
for piece in (*vpeel.picks, *vpeel.absorbed,
337-
*(() if vpeel.numeral is None
338-
else (vpeel.numeral,))):
335+
# rules/S10: "a numeral written in lower case in a name of mixed
336+
# case is the generation and reports nothing" -- here as at
337+
# assign's numeral report (#718's review)
338+
numeral = (() if vpeel.numeral is None
339+
or numeral_settled_by_case(
340+
tokens[vpeel.numeral[0]].text, one_case)
341+
else (vpeel.numeral,))
342+
for piece in (*vpeel.picks, *vpeel.absorbed, *numeral):
339343
if piece[0] in consumed:
340344
ambiguities.append(PendingAmbiguity(
341345
AmbiguityKind.SUFFIX_OR_NAME,
@@ -352,8 +356,7 @@ def _maiden_take(pieces: Sequence[Sequence[int]],
352356
f"{word.text!r} ending the maiden name is also a "
353357
f"post-nominal; the maiden marker's clause keeps it rather "
354358
f"than reading it as one", tuple(last)))
355-
elif (tuple(last) in picked
356-
and not reported_as_initial(ambiguities, last[0])):
359+
elif tuple(last) in picked:
357360
# rules/S10: "either reading is reported" -- a numeral the read
358361
# declined for want of words to spare, which the clause keeps
359362
ambiguities.append(PendingAmbiguity(
@@ -1115,27 +1118,12 @@ def chain(tail: int) -> None:
11151118
# the given role the fold then moves ('Freiherr von
11161119
# Berg Ma' was told 'given name').
11171120
last = pieces[j - 1]
1118-
# ...and a numeral S2's read declined (rules/S10, #718),
1119-
# a pick with no credential tag: the read's picks are
1120-
# where it is recorded, and `tuple` is C-level, so a
1121-
# chained name still pays no frame
1122-
numeral = (j > k + 1 and len(last) == 1
1123-
and read is not None
1124-
and tuple(last) in read.peel.picks
1125-
and tokens[last[0]].tags.isdisjoint(
1126-
_AMBIGUOUS_CREDENTIAL_TAGS))
1127-
if numeral:
1128-
if not reported_as_initial(ambiguities, last[0]):
1129-
ambiguities.append(PendingAmbiguity(
1130-
AmbiguityKind.SUFFIX_OR_NAME,
1131-
f"{tokens[last[0]].text!r} is a roman "
1132-
f"numeral, but the name has no other word to "
1133-
f"spare for it; the particle chain took it "
1134-
f"into the name rather than reading it as a "
1135-
f"generational suffix",
1136-
tuple(last)))
1137-
took.add(tuple(last))
1138-
elif (j > k + 1 and len(last) == 1
1121+
# A numeral S2's read declined (rules/S10, #718) is not
1122+
# reported here: it stays one of the read's picks, and
1123+
# assign words it from the role it ends in ('Freiherr
1124+
# von Richthofen V', 'a family name'), the chain taking
1125+
# nothing the read had not already given up
1126+
if (j > k + 1 and len(last) == 1
11391127
and not tokens[last[0]].tags.isdisjoint(
11401128
_AMBIGUOUS_CREDENTIAL_TAGS)
11411129
and not is_prefix_piece(last, ptags[j - 1],
@@ -1275,9 +1263,10 @@ def chain(tail: int) -> None:
12751263
# before #614: the join tried on the pieces as it would
12761264
# leave them, the reading assign runs over each (the
12771265
# ONE function, _pieces.tail_reading), the suffixes
1278-
# compared as the peeled pieces themselves -- 'abdul V'
1279-
# peels the V unjoined and nothing joined, 'abdul Smith
1280-
# Ma' peels the acronym unjoined and keeps it joined.
1266+
# compared as the peeled pieces themselves -- 'abdul
1267+
# Smith V' peels the V unjoined and nothing joined (no
1268+
# word to spare there, rules/S10), 'abdul Smith Ma'
1269+
# peels the acronym unjoined and keeps it joined.
12811270
if read is not None:
12821271
same_suffixes = True
12831272
chained = False

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,9 +1364,10 @@ def particle_tail(seg: Sequence[Sequence[int]],
13641364

13651365

13661366
# rules/S10: "a letter already reported as read as an initial reports
1367-
# nothing new" -- the one question the numeral fork's three declined-
1368-
# pick emitters (assign's, the particle chain's and the maiden
1369-
# clause's, #718) all ask, so it has one answer
1367+
# nothing new" -- asked by assign's report of the numeral fork's
1368+
# declined pick (#718). The maiden clause's report on the numeral it
1369+
# keeps cannot meet the case: classify reads no connective inside a
1370+
# clause, so a clause word never carries that report
13701371
def reported_as_initial(ambiguities: Iterable[PendingAmbiguity],
13711372
index: int) -> bool:
13721373
"""Whether classify's connective-or-initial fork already reported

‎nameparser/_pipeline/_vocab.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,16 @@ def is_initial_shaped(text: str) -> bool:
254254

255255
# rules/S10: "A roman numeral ending a name written without a comma
256256
# reads as the generation" -- which the initial veto would otherwise
257-
# take: V, I and X are suffix vocabulary AND bare capitals, and a bare
258-
# capital inside a name is a middle initial. Whether a one-letter
257+
# take: V and I are suffix vocabulary, X a numeral by shape alone, and
258+
# all three are bare capitals, which inside a name are middle initials. Whether a one-letter
259259
# numeral has the words to spare is the caller's count (`peel_trailing`).
260260
def is_trailing_numeral_suffix(text: str, preceding: str) -> bool:
261261
"""assign's roman-numeral fork, shared with group's bound-given
262262
reserve (#401): a FINAL single-token piece that is a roman numeral
263263
reads as the suffix when the piece before it does not look like
264-
part of an initial run. `preceding` is that piece's first token;
264+
part of an initial run -- for a one-letter numeral a necessary
265+
condition only, the caller's words-to-spare count deciding
266+
(rules/S10, #718). `preceding` is that piece's first token;
265267
the callers establish that `text` is last and that a name piece
266268
precedes it.
267269

0 commit comments

Comments
 (0)