You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(group,docs): the cleanup round, and the pin the guard was missing
/simplify, four angles. Reuse and efficiency returned no changes; the
other two found real work.
The one that matters: the diff's central conjunct was UNPINNED. Dropping
`in_entry and` from the tag test left the whole suite green -- including
the guard added for exactly this -- while diverging on 1,152 of 65,725
inputs. That guard pins the sticky entry_open update, not the tag
condition. The distinguishing input needs a suffix piece FIRST and then
TWO titles, so the second title continues an entry it does not belong to:
"Smith, MD Rev. Dr." gives title_list ['Rev.','Dr.'] and the mutant gives
['Rev. Dr.'], with the title STRING identical either way. Added, and
verified to fail against the mutation copy.
The invariant this change rests on -- one comma segment is one suffix
entry -- was stated in NO rule. C1 governs routing; R1 governed word
ORDER and never mentioned separation; the only statement was a code
comment. That is a large part of why two stages disagreed about it for
years, so R1 gains the sentence and two examples, both names already in
the corpus (a third, "Smith, MD, PhD", would have moved for a #325
routing reason and been misfiled under #429).
Also from the review:
- Two case rows removed. family_comma_three_credential_entries is a
strict kill-subset of the title-led row AND its note claimed to pin a
latch that family_comma_title_between_credentials actually pins -- all
three of its pieces are in the entry, so the latch never has to survive
a gap. family_comma_title_run_does_not_join kills nothing, by its own
note; test_facade.py holds the same input with assertions that can see
the collapse.
- `family_comma` is redundant by invariant (segment() emits at most one
segment for NO_COMMA; SUFFIX_COMMA short-circuits on `tail`). Kept for
locality, now documented as unpinnable rather than left to look like
missing coverage.
- I reintroduced #191 in my own new comment while this same PR corrects
it elsewhere. Fixed here and at the two remaining delimiter sites.
test_group.py:128 keeps #191 -- it is genuinely about the prefixes.
- The predicate moved below _is_suffix_piece, which it calls, restoring
the _is_leading_title/_leading_titles pair it had split.
- The "whatever role it holds" claim is qualified: no multi-token TITLE
piece witnesses that branch in 38,892 generated inputs.
- The docstring's regression narration trimmed to the contract sentence
plus a pointer; the concrete account lives at the call site.
Efficiency recorded rather than optimised: _segment_holds_no_name runs
twice per family-comma parse, measured pure recomputation (408 of 506
comma-bearing literals reach both sites, 0 disagreements). Worth 1.2-2.2%
of a family-comma parse and 0% of every other. Caching needs a derived
ParseState field the sites would not otherwise share, so decisions.md
carries the number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/design/decisions.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -442,6 +442,8 @@ Excluded (MAIDEN_MARKERS, per nameparser/config/maiden_markers.py):
442
442
SCOPE, stated as the condition actually reads rather than as the first draft described it: any family comma whose following segment holds no name word, NOT only a one-word family. `John Smith, Jr. III` renders suffix 'Jr. III' (1.4.0's reading), and a title-led segment is in too — `Smith, Dr. MD PhD` renders 'MD PhD'. The differential cannot witness the widening: of 1069 corpus names, 303 carry a structure comma, the predicate is true for 36, only 9 of those have two or more pieces in the segment, and none of the 9 combines a multi-word pre-comma name with two suffix pieces. 0 unexplained is therefore evidence about the corpus, not about the class — the case table carries it instead.
443
443
THE REGRESSION THE REVIEW FOUND, and the reason this entry is long: the first draft let ANY piece open an entry, as the tail block always had. That is safe on a tail segment, where assign routes every piece to SUFFIX — which is what `tail` means — and wrong off it, where a title piece routes to TITLE. Two failures, one cause. The `joined` tag is role-BLIND and the facade heals it for every role, so a title opening an entry collapsed `title_list` from ['Rev.', 'Dr.'] to ['Rev. Dr.'] on `Smith, Rev. Dr.`; and where the pre-comma name left a suffix, the tag glued the next suffix BACKWARD across the writer's own comma — `Smith Jr., Mr. Jr.` rendered suffix 'Jr. Jr.', the exact inverse of the bug being fixed. Neither is visible to the differential, which compares strings, nor to the case table as it stood, which asserts the title STRING (space-joined either way). The rule that replaces it separates two joins that had been one: WITHIN a piece the tag renders a merged piece as one unit, whatever role it holds; BETWEEN pieces it continues an entry, and only a piece that renders into the same run may do that. The entry is sticky across a piece that is not in it, so an interleaved title does not split the run it sits in (`Smith, MD Dr. PhD` renders 'MD PhD'); a delimiter core still closes it, which is the one thing that separates two entries.
444
444
Scoped deliberately, and the scoping is the other decision: `tail` still gates delimiter-core dropping, which is v1 `expand_suffix_delimiter` parity (#206, commit 021823e — three code comments say #191, which is the German/Dutch vocabulary PR, and are corrected here) and a TAIL rule. The core set is threaded to the block as `seg_cores` so the widened condition cannot drop cores where `tail` says not to. Worth knowing before anyone tries to pin that: for every realistic delimiter the two spellings are indistinguishable by construction, because the core must first pass the very predicate that gates the block — the only reachable class is a policy whose delimiter core is itself suffix or title vocabulary (`Policy(extra_suffix_delimiters={" jr "})`, `Smith, MD jr`), which the case table does not enshrine.
445
+
Cost, measured rather than optimised away: `_segment_holds_no_name` now runs TWICE per family-comma parse, once here and once in assign, on the same pieces — verified pure recomputation, not two questions (408 of 506 comma-bearing test literals reach both sites, 0 disagreements, and off-tail `seg_cores` is empty so `kept` never filters, which is why assign sees exactly the pieces group tested). Removing the second call is worth 0.6–1.4 µs, 1.2–2.2% of a family-comma parse and 0% of every other parse (NO_COMMA and SUFFIX_COMMA make no call at all). Caching it needs a derived ParseState field the two sites would otherwise not share, so the number is recorded instead. The order is cheapest-first per 7009fcf: two bool loads and an int compare gate the call.
446
+
R1 gained the separator sentence with this change. That the invariant it states — one comma segment is one suffix entry — was written down in no rule is a large part of why two stages disagreed about it for years: C1 governs routing, R1 governed word ORDER and never mentioned separation, and the only statement of the rule was a code comment.
445
447
Left alone, measured and pre-existing: the NO-COMMA path comma-joins a space-separated run too (`John Smith MD PhD` renders 'MD, PhD' at 1.4.0, 2.0.0, 2.1.0 and master alike). The fix requires FAMILY_COMMA and does not reach it; that path has no writer-drawn boundary and is a separate question, recorded here and NOT yet filed. It is load-bearing for one thing: `str()` of a fixed parse is a no-comma string, so re-parsing it re-inserts the comma — round-tripping is not stable on these shapes, and an earlier draft of the release bullet claimed it was.
446
448
The name `Smith, MD PhD` entered the corpora as this fix's C1 example — a FIRING one, carrying no `· boundary` annotation, which marks the non-firing example a rule must have — so its ledger entries arrive with it. A fourth written for the 1.4.0 ledger EXPLAINED NOTHING and was dropped: `fix(comma-family) lone post-comma piece routes to suffix/title, not first` already claims the name there, its fields covering all three that move. That is #373's dormant-rule diagnosis doing its job, and #426 is the precedent for dropping rather than keeping a shadowed rule.
0 commit comments