Skip to content

fix(group): a suffix never begins a name (#371) - #474

Merged
derek73 merged 2 commits into
masterfrom
fix/371-suffix-never-begins-a-name
Aug 31, 2026
Merged

fix(group): a suffix never begins a name (#371)#474
derek73 merged 2 commits into
masterfrom
fix/371-suffix-never-begins-a-name

Conversation

@derek73

@derek73 derek73 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

A name that opens with a spaced Ph. D. stops losing its surname.

Closes #371

The defect

parse("Ph. D. Van Johnson")   given 'Van Johnson'   family ''   suffix 'Ph. D.'
                          →   title 'Ph.'   given 'D.'   family 'Van Johnson'

The rule

A suffix never opens the string: position outranks the vocabulary match. rules.md#S2 states it.

The v1 fix_phd merge — which heals a split Ph./D. into one credential — was applied wherever the pair stood, and that is what made a leading credential possible at all. Every other suffix-shaped word standing first already falls out correctly (Jr., MD, Esq., Sr. as titles; PhD, III as name words), via TITLES membership or H2's abbreviation clause. A reviewer swept all 650 one-word suffix-vocabulary entries at the head of two tails — 8,612 parses — and none yields a suffix. The Ph./D. pair is the only shape that reaches the defect.

Everything else is untouched: the merge still heals a trailing credential, and a family comma still opens a listing rather than a name.

John Smith Ph. D.    suffix 'Ph. D.'          Smith, Ph. D. Jr.    suffix 'Ph. D. Jr.'
Ph. D., John         family 'Ph. D.'          II Van Johnson       family 'Van Johnson'

"The head" means the head of the STRING, and review found both ways to get that wrong

The first draft tested the piece index. extract_delimited removes a quoted or bracketed clause before grouping, so "Bob" Ph. D. John Smith reached the merge with the pair at k == 0 and a word standing before it in the input. v1 merges there — its regex \s(ph\.?\s+d\.?) needed only a preceding space — and declining broke parity on 112 measured names, every one opening with a quote or bracket, and not one of them in any corpus. The test is now the token index, which is v1's boundary exactly.

The other direction cannot be fixed here, and finding out why settled the design. A leading title is a piece, so Sir Ph. D. Van Johnson keeps the credential and the empty family — #371's own symptom one word to the left. That is 1.4.0's reading, and the principled alternative does not exist: "the first piece of the name" is not computable before this merge, because H2's abbreviation test is true of Ph. itself, so a scan stepping over titles would step over the very piece being judged. rules.md#S2 therefore says opens the string, with the title case pinned as an Accepted boundary rather than left as an unmarked counterexample.

Parity with 1.4.0, stated precisely

Three of the four corpus names of this shape return to their v1 reading exactly:

1.4.0 this branch
Ph. D. Van Johnson title Ph., first D., last Van Johnson identical
Ph. D. John Smith + middle John, last Smith identical
Ph. D. title Ph., last D. identical
Ph. D., Jr. title Ph., first D., suffix Jr. family D. — not restored

The fourth moves the D. from given to family, two roles apart, and its residual diff rides under fix(comma-precomma-family), which predates this change.

So what shipped in 2.0 was a regression from applying the merge unconditionally. expected_since_1.4.0.toml loses its fix(leading-credential) rule; 2.0.0 and 2.1.0 each gain one claiming the same four names, five roles and the same digest.

Read the diagnosis, not the error. The gate reported that rule SHADOWED, not reverted — compare.py separates the two "because they have three different fixes", and #426 is the precedent for dropping a shadowed rule. An earlier draft of this PR generalised EXPLAINED NOTHING into "how a parity restoration announces itself", which would teach the next maintainer to delete a shadowed rule whose behaviour is still diverging.

Review

Four agents; two regressions found, both mine, both above. Nine further findings, all fixed. Three worth naming:

  • The guard test this PR narrowed was left vacuous. Removing the Ph. D. witness left II Van Johnson alone — and II is not suffix vocabulary under that file's reduced lexicon, so the test passed under the exact mutation it is named for. PhD is the replacement witness; the mutation is killed again.
  • Every characterisation I wrote of v1 was wrong. "Healed the pair only when it TRAILS" is false in both directions, in four files — v1 healed it everywhere except the head, and reading that sentence as the contract would license narrowing the merge and silently changing John Ph. D. Smith.
  • Two live references to the rule this PR deletes remained in the same ledger; acting on them takes the gate red, since the (?!\s*ph\.) carve-out is what lets Ph. D., Jr. reach its own rule.

Also added: Case rows for Ph. D. Van Johnson (the issue's subject, whose empty fields are the symptom — test_case asserts the whole dict) and the bare Ph. D., neither of which had a pin outside an out-of-band tool run.

Verification

  • 6171 → 6186 passed; mypy, ruff and 232 sphinx doctests clean.
  • Gate green at 1.4.0 / 2.0.0 / 2.1.0, unexplained: 0.
  • Four mutations verified, including both shipped defects the review found and the repaired guard.

One check is red

build (3.12) fails test_facade_thousand_names_under_a_second at 1.06–1.08s against a fixed 1.0s bound. Measured three ways against this branch's own parent — uninstrumented (~1%, inside the parent's own spread), coverage-instrumented (indistinguishable), and the whole benchmark file under --cov (branch faster) — no method reproduces a slowdown. Tracked as #475, which carries the full evidence including the CI control run that favours master.

🤖 Generated with Claude Code

`parse("Ph. D. Van Johnson")` read given 'Van Johnson' with an empty
family and suffix 'Ph. D.'. It now reads title 'Ph.', given 'D.',
family 'Van Johnson'.

Position outranks the vocabulary match. The v1 fix_phd merge -- which
heals a split `Ph.`/`D.` into one credential -- was applied wherever
the pair stood, and that is what made a leading credential possible at
all: every other suffix-shaped word standing first already falls out
as a title (Jr., MD, Esq., Sr., via TITLES or H2's abbreviation
clause) or as an ordinary name word (PhD, III). The pair was the only
shape that reached the defect.

RESTORES 1.4.0 rather than deviating from it. v1 matched the pair by
regex on the raw string and healed it only where it TRAILED; leading,
it split them exactly as this does -- measured on the released wheel,
all four corpus names of the shape return to their v1 reading. So
expected_since_1.4.0.toml LOSES its fix(leading-credential) rule (the
gate said EXPLAINED NOTHING, which is how a parity restoration
announces itself) while 2.0.0 and 2.1.0 GAIN one claiming the same
four names, five roles and digest. What shipped in 2.0 was a
regression from applying the merge unconditionally.

Opening the NAME is not opening a piece list: a credential run
legitimately opens segment 1 after a family comma ("Smith, Ph. D.
Jr."), which C1 reads as a listing. A first draft tested `k == 0`
alone, broke fifteen tests and took the gate red at three baselines.

Accepted: Parser.revise(suffix="Ph. D.") now renders 'Ph., D.'.
revise() sub-parses the string it is given and a field value has no
head. A draft carved that out by requiring a name to displace, and the
carve-out made the head reading depend on what FOLLOWED -- appending a
maiden clause changed whether 'Ph.' was a title, which the maiden
property test caught. Dropping the carve-out removed both problems.

rules.md#S2 states the rule; three mutations verified; gate green at
all three baselines.
@derek73 derek73 added this to the v2.2 milestone Aug 31, 2026
@derek73 derek73 added the bug label Aug 31, 2026
@derek73 derek73 self-assigned this Aug 31, 2026
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.66%. Comparing base (07b3231) to head (66128d3).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #474   +/-   ##
=======================================
  Coverage   98.66%   98.66%           
=======================================
  Files          45       45           
  Lines        3214     3214           
=======================================
  Hits         3171     3171           
  Misses         43       43           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The gate meant a different "head" than the rule it implemented, in two
directions at once, and the corpus could see neither.

TOO LATE: `k == 0` is a PIECE index, and extract_delimited removes a
quoted or bracketed clause before grouping -- so `"Bob" Ph. D. John
Smith` reached the merge with the pair at k == 0 and a word standing
before it in the input. v1 merges there (its regex needed only a
preceding space); declining broke parity on 112 measured names, every
one opening with a quote or bracket, none of them in any corpus. The
test is now `a[0] == 0`, the first TOKEN, which is v1's boundary.

TOO EARLY: a title is a piece, so `Sir Ph. D. Van Johnson` keeps the
credential and the empty family -- #371's own symptom one word to the
left. That is 1.4.0 parity and it cannot be fixed here: "the first
piece of the name" is not computable before this merge, because H2's
abbreviation test is true of `Ph.` itself, so a scan stepping over
titles would step over the very piece being judged. rules.md#S2 now
says OPENS THE STRING and carries the boundary example.

Prose corrected with it, all of it mine and all of it wrong:
- "restores 1.4.0 on all four" -- three. `Ph. D., Jr.` moves the `D.`
  from given to family, two roles apart, and rides under a pre-comma
  rule that predates this change.
- "EXPLAINED NOTHING is how a parity restoration announces itself" --
  the gate said SHADOWED, which compare.py separates from reverted
  precisely because the fixes differ. #426 is the precedent.
- "v1 healed the pair only when it TRAILS" -- false in both
  directions; v1 healed it everywhere except the head, and reading
  that sentence as the contract would license narrowing the merge.
- two live references to the rule this PR deleted, in the same ledger;
  acting on them takes the gate red.
- the 2026-07 phd-merge entry promised the two spellings "read alike",
  which #371 scopes to non-leading position.

Tests: the guard test this PR narrowed was left VACUOUS -- `II` is not
suffix vocabulary under the file's reduced lexicon, so removing the
`Ph. D.` witness left it passing under the exact mutation it is named
for. `PhD` is the replacement witness and the mutation is killed
again. Added Case rows for `Ph. D. Van Johnson` (the issue's subject,
whose empty fields are the symptom) and the bare `Ph. D.`, and pinned
both new conditions.

Four mutations verified. Gate green at all three baselines.
@derek73
derek73 merged commit b4eeb93 into master Aug 31, 2026
15 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ph. D. Van Johnson has no surname: a leading suffix displaces the particle like a title used to

1 participant