Conversation
…tial and a numeral in capitals
A capitalization_exceptions value is now a case mask -- the key's own
letters and digits, each in the case it takes -- laid over the word as
written, so one 'phd' entry repairs phd to PhD and ph.d. to Ph.D., and
repair never adds or drops a character ('john smith iii.' keeps its
period). A mask's own punctuation marks where its letters are joined
and is never written: a single letter the writer split off beside a
full stop is an initial and is capitalized where the mask keeps that
letter inside a run ('p.h.d.' gives 'P.H.D.'), while a mask that
spells the letter alone leaves it as the mask says ('h.c' keeps
'h.c.'). Lexicon raises ValueError on a value that spells anything
else, at every entry point, the v1 shim's snapshot included; that is
a decided exception to the shim's never-raise rule, recorded in
AGENTS.md, since v1 substituted such a value and repair now only
recases.
The shipped map is phd/bsc/msc. md, ii, iii and iv leave it: a SUFFIX
md is a listed acronym, and a SUFFIX roman numeral is now written in
capitals by shape (vi through x included), on a hand copy of the
pipeline's roman pattern pinned by test_regex_sync, since layering
forbids _render the import. A suffix read as a credential by its dotted
shape (shape:acronym, now defined in _types so _render can read it)
repairs as a listed acronym does. A word the parse put in a NAME role
repairs as a name word: 'iv smith' gives 'Iv Smith'.
rules.md#R4 states the invariant and pins each clause; the rules
corpus is regenerated and the ledger rows its new lines reach are
re-recorded. No role field moves; the differential gate exits 0 at
every baseline.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
R5 repairs only a name written wholly in one case, reading mixed case
as the writer's deliberate choice. The gate read every token, so a
suffix written the way a suffix is written -- 'III', 'PhD', 'Jr.' --
held a lowercase name back: 'juan garcia III' came back untouched in
every release, and v1's test for it had been a known failure since
2012 (the Google Code tracker's issue 22).
The gate text is now every token but the SUFFIX-roled ones. Titles
stay in -- not as evidence about the name, but because title repair
is still plain str.capitalize() and cannot yet be trusted to act on
one ('Dr. juan garcia' is left alone) -- and so does every name word,
nickname and maiden name ('Juan garcia III' is left alone). Force is
unchanged. The parser's own one-case reading still counts the suffix
(P3, S2), so 'john e jones III' repairs to 'John e Jones III' where
'john e jones iii' gives 'John E Jones III'; that split is decided
and recorded. The xfail retires under a name that no longer says
KNOWN_FAILURE, and rules.md#R5 states the sentence and pins both
boundaries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
'jose ortega-y-gasset' and 'JOSE ORTEGA-Y-GASSET' both repair to 'Jose Ortega-y-Gasset', where 2.2 and 2.3 gave 'Ortega-Y-Gasset': inside one hyphenated word, a part that is connective vocabulary with a worded part on each side keeps its lowercase, the hyphens joining the name around it as the spaced connective would. An edge part is ordinary name text, #458's answer kept -- 'juan e-f smith' still gives 'Juan E-F Smith' -- and the rule reads position and vocabulary, as the particle arm already does per sub-word, never re-deriving the connective-versus-initial class from shape. Only connective vocabulary is read, so the Maori 'Te Awanui-a-Rangi Black' is not reached, and a period-marked part is an initial there as it is in spaced text: 'j.-e.-p. dupont' keeps 'J.-E.-P.'. rules.md#R4 states the rule, pins its edges, and narrows the Accepted sentence that said a vocabulary word inside a longer word is never a conjunction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rules.md#R4's invariant, held over every name a caller has written -- the deduped differential corpus glob plus the case table's texts -- on both surfaces, plain and forced, under the shipped lexicon and one with the exceptions map emptied: a repaired token equals its input under str.casefold(). Compared token by token on the core and list by list on the facade, never through a rendered string. The recorded control is the parent, where the map substituted its value and the walk fails on 257 repairs; the live control puts the substitution back through the mask applier and must be seen. The hypothesis walk that now draws masks gets the same assertion rather than a second fuzzer. Under a second for the new walk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
decisions.md#R4 records the masks and why a replacement string is no longer capitalization; the check (letters and digits, the offered key, a decomposed word finding a composed key); the decided raise through the v1 shim; the given-role decision and what it reaches (iv smith -> Iv Smith, Option A); the mask's punctuation marking its joins and the split-off-initial rule, with the two drafts that were wrong; the DECLINED construction-time warning and shim translation, written in review and dropped when their premise was measured false; the invariant's Unicode boundary (ss, dotless i); the numerals and the xi parse limit; the shape acronyms; the hyphen rule with its initial guard and Maori non-reach; and the movers with a recipe that states its per-sub-word attribution. Two Excluded blocks keep md/ii/iii/iv out of the map and 'a' out of the connectives. decisions.md#R5 records the suffix-free gate, why titles stay evidence, the split from the parser's own one-case reading (now pinned in test_render), and its population; short dated bullets under the entries that said #492 stayed deferred say it landed, and the triage entry's standing marker counts are amended in place after the #489 precedent. The 3-0 list carries the shim raise. mechanisms.md's RENDER-HONORS-THE-PARSE loses a passage stale since #461. customize.rst, migrate.rst and usage.rst teach masks, the gate sentence and what a spliced field falls back to; AGENTS.md's sites that taught the dotted output are corrected; the release log amends the #459 acronym bullet in place and adds four bullets, every count a dated snapshot over the parent's corpus with its recipe named. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three reviewers read the whole branch (a silent-failure hunt, a 74-mutant test analysis and a comment analysis); this is what they found. The v1 raise for a capitalization_exceptions value that does not spell its key now carries a hint spelled for the surface the caller is standing on -- constants.capitalization_exceptions['jr'] = 'JR' -- re-raised from the shim's snapshot with the Lexicon error chained. _normpairs raises a private ValueError subclass carrying the key and the offered spelling, so the shim catches that one shape by type and every other ValueError passes through untouched; the offer itself is one helper, _offered_mask, used by both messages. The hyphen clause's initial pattern is a module constant, _DOTTED_INITIAL, registered in test_regex_sync as _INITIAL's period alternative rather than an inline literal the sync test could not see. Mask values are stored NFC-composed, so a decomposed value can no longer change the split-off-initial reading the composed spelling gives. Eight test rows kill mutants that survived the branch's own tests: a mask outranks the numeral clause; the gate leaves out every suffix token, not the trailing one, and an untagged one; non-link parts of a three-part hyphenated token get the full per-word repair; the dotless-i boundary is an assertion; two facade assigned-field rows; a leading empty part; three comma shapes of the gate. Comments that referenced a helper that never shipped, claimed the mask clause reads no vocabulary, or restated rules.md#R4 in fresh words where an excerpt exists are corrected, and decisions.md#R4 records the round. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One walk decides a mask letter's case: _letter_run_ge2 is a list comprehension and _apply_mask reads it for the word as well as the mask, so the split-off-initial test is one expression and the two casing branches are one step. A local names the undotted key the mask lookup and the acronym clause both read. Comments and docstrings state each mechanism once and cite rules.md#R4 for the rule; the shim's comment keeps the measured facts about which errors are translated away and drops the rest. The tests share one custom-lexicon helper. Byte-identical against an oracle of 141,120 recorded repairs (every corpus name, case row, rules.md example input and new test input, plain and forced, core and facade, under the default lexicon and sixteen custom maps), the exact text of every new ValueError, the shim's warning count, and 400,000 random word/mask pairs through the applier. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #539 +/- ##
==========================================
- Coverage 98.79% 98.78% -0.01%
==========================================
Files 45 45
Lines 3638 3697 +59
==========================================
+ Hits 3594 3652 +58
- Misses 44 45 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Render-side case repair:
capitalization_exceptionsvalues become letter masks, a suffix stops counting as case evidence, and a connective inside a hyphenated surname keeps its lowercase. Every change is tocapitalized()and its facade twinHumanName.capitalize(); no role field and no ambiguity report moves on any name, and the differential gate exits 0 at all five baselines (1.4.0, 2.0.0, 2.1.0, 2.2.0, 2.3.0).Closes #459
Closes #492
Closes #478
What changes
#459 — the exceptions map holds case masks. A value is its key's own letters and digits, each in the case it takes, laid over the word as the writer punctuated it: one
phd → PhDentry repairsphdtoPhD,ph.d.toPh.D.andPH.D.toPh.D.. Repair changes case and nothing else (john smith iii.keeps its period; every release dropped it). The shipped map isphd → PhD,bsc → BSc,msc → MSc;md,ii,iiiandivleave it. A suffixmdis writtenMDby the acronym clause (M.D.when the writer wrote the periods), a suffix roman numeral is written in capitals by shape (vithroughxnow giveVI…Xinstead ofVi…X), and a suffix read as a credential by its dotted shape (#516) repairs as a listed acronym (x.y.z.→X.Y.Z.). A value that does not spell its key raisesValueErrorat construction, and at the first parse for a v1Constantswith a hint spelled for that surface — the one decided exception to the shim's never-raise rule, since v1 substituted such a value and repair now only recases. A mask's own punctuation marks where its letters are joined and is never written; a single letter the writer split off beside a full stop is an initial where the mask keeps it inside a run (p.h.d.→P.H.D.,b.sc.→B.Sc.), while a caller'sh.cmask keepsh.c.. A word the parse put in a name role repairs as a name word (qc mp→Qc MP,iv smith→Iv Smith— Option A, decided).#492 — a suffix is no evidence about how the name was cased. R5's one-case gate reads every token but the suffix-roled ones, so
juan garcia III,juan garcia PhDandJUAN GARCIA Jr.repair where every release returned them untouched (v1's test for it had been a known failure since 2012). Titles, nicknames and maiden names stay evidence;Dr. juan garciaandJuan garcia IIIare left alone. The parser's own one-case reading (P3, S2) still counts the suffix, sojohn e jones IIIrepairs toJohn e Jones IIIwherejohn e jones iiigivesJohn E Jones III— a decided split, pinned.#478 — a connective interior to a hyphenated token keeps its lowercase.
jose ortega-y-gassetandJOSE ORTEGA-Y-GASSETboth giveJose Ortega-y-Gasset(2.2 and 2.3 gaveOrtega-Y-Gasset; 1.4.0 through 2.1 gave the lowercase for the lowercase spelling). Edge parts stay ordinary name text (juan e-f smithkeepsE-F, #458's answer), a period-marked single letter is an initial (j.-e.-p. dupontkeepsJ.-E.-P.), and MāoriTe Awanui-a-Rangiis the recorded non-reach.Measured
Comparator: the parent 4d0680e as an archive copy, both trees imported by path over the same corpus directory, per-sub-word attribution; recipes in
docs/design/decisions.md(R4andR5, the 2026-09-23 MEASURED bullets).Role fields moved: 0. Facade counts equal the core's on every row.
Design record
Spec approved question by question on 2026-09-23; twelve plan amendments to it, each by measurement (the render layer may not import the pipeline, so the roman pattern is a pinned hand copy; the key keeps interior periods through the fold; decomposed hangul finds a composed key; four example spellings swapped for gate-classified ones). Two designs were written in review and DECLINED when their premise measured false: a construction-time warning for a punctuated value, and a shim translation of the legacy
md → M.D./phd → Ph.D.pairs — the mask's punctuation is READ (it marks the letter runs), so "ignored" was wrong. Recorded indecisions.md#R4(2026-09-23 and 2026-09-24 bullets),#R5,#3-0-reevaluations, twoExcludedblocks, andrules.md#R4/#R5(32 example lines).mechanisms.md#RENDER-HONORS-THE-PARSEloses a passage stale since #461.Review
Each commit had a spec-compliance and a code-quality review with mutation testing in a scratch copy; fix commits were re-reviewed. Whole-branch round: silent-failure hunt, 74-mutant test analysis, comment analysis, the design-docs reviewer on
docs/design/, and a release-log/guides review against the released wheels. Defects found and fixed in review, none of them in the first draft of any commit's tests: per-letter dotted spellings mis-cased under a mask (b.s.c.→B.S.c.), a caller's ownh.cmask overridden, a 1.4.0 pickle warning about values its owner never wrote, a period-marked initial lowered inside a hyphen (j.-e.-p.), a typed exception that could not cross a process pool, a doubled multi-word warning, and three standing counts in a triage entry. Simplify pass last, against a byte-identical oracle of 141,120 recorded values plus 400,000 random mask pairs.Verification
uv run pytest -q: 9860 passed, 324 skipped, 3 xfaileduv run mypy: clean (111 files);uv run ruff check nameparser/: cleansphinx-build -b doctestand-b html: 0 failures, no warnings;python -m doctest README.rst: clean_render.py100%tools/differential/compare.pyat 1.4.0 / 2.0.0 / 2.1.0 / 2.2.0 / 2.3.0: exit 0, no UNEXPLAINED;corpus_rules.jsonlregenerated (372 → 410 names)Follow-up candidate, not filed: a decomposed (NFD) WORD, as opposed to a mask value, repairs differently from its composed spelling (
pé.x.→PÉ.X.vsPé.X.), pre-existing on master.🤖 Generated with Claude Code