fix(render): initials() honors the folded-word order the family field applies (#408) - #465
Merged
Conversation
O3's fold and P6's attachment tag rather than move a token, and
`_types._text_for` partitions on that tag for every role it renders --
so `parse("der, y van")` reads family 'van der'. `initials()` walked
`tokens_for(role)` in written order and never asked, so a view and the
field beside it gave two orders for one parse: initials 'y. d. v.'.
Mirrors `_text_for`'s partition, per role rather than scoped to
FAMILY. Both producers re-role to FAMILY, so the GIVEN and MIDDLE arms
are unreachable today; they are there so the two views cannot diverge
again, the same uniformity `_post_rules`' UNJOINED_TAG loop takes.
Measured over the 1094-name deduped corpus, 6564 parses across three
name_order values x middle_as_family off/on: 660 move. At the default
order it is 1 without middle_as_family and 71 with it, and the change
is strictly parity-restoring -- 54 of those 71 return to exactly what
the released 1.4.0 wheel gives and none moves away from it. The
facade's initials had been folding all along, through its own *_list
views, so the core view was out of step with the field, the facade and
v1 at once, with nothing in the suite touching it.
`initials()` is not a compared surface, so the gate cannot see this:
run at all three baselines on the pre-change tree and on this one, the
output is identical to the byte. No ledger rule was added or needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All five were measured on the branch, and three were errors in my own
measurements rather than in the prose reporting them.
1. The release-log bullet opened on `HumanName("der, y van")`, which
never moved -- the facade folds through its own `*_list` views and
moves for 0 of the 1094 corpus names. The value that moved is
`parse(...)`, the 2.0-API view, which is also the bullet's own
argument four sentences later. Corrected, and the facade's
invariance is now stated as measured rather than asserted.
2. The 17 names matching 1.4.0 neither before nor after split 15/2,
not 14/3: `der, y van` parses BYTE-IDENTICALLY to 1.4.0 under
`middle_as_family`, and I had carried its field diff over from the
other policy. It also disagrees for a different reason than the
14 -- 1.4.0 contributes nothing for an all-particle family where
2.x contributes its words, which is R3's own decided clause, not
an unexplained residue. `남궁민수 지훈` is the hangul surname split
(W1), not #379/#404 or #432.
3. `test_initials_folds_in_every_role_it_renders` asserted the MIDDLE
arm and pinned only GIVEN: the hand-built name had ONE middle
token, and a one-element partition is the identity. Two tokens per
role now, and each arm is separately mutation-checked -- skipping
the partition for GIVEN, for MIDDLE, or for FAMILY each fails.
4. R3's `history:` still pointed at `decisions.md#R2`, so the entry
arguing its new order clause was unreachable from the rule. It
points at `#R3` now, and the entry carries the back-pointer.
5. "One name moves at the default policy" carried no denominator. The
population is one: exactly one corpus name has a contributing
folded token standing behind a contributing unfolded one. Stated
at both sites, with the 71-of-72 figure for the folded policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… enough The entry named one mutation; finding 3 showed it left the MIDDLE arm unpinned. Records the four that run now, and the general shape -- a test pinning a partition needs two elements in every group it claims, or the claim rides on an identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e out in ParsedName.initials' docstring IS the reference modules.html renders, so a docs sweep that stops at the .rst files misses half the page (AGENTS.md's release checklist, step 0). The order clause was in _render.initials, which autodoc does not render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The #408 test's one-token-per-role fixture made its MIDDLE arm's mutation a no-op, and the FAMILY-scoped mutation that did fail read as proof the arms were pinned. Recorded as a Verification shape, since the failure arrives through the fixture rather than the predicate and the existing entries all address the predicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pointer The comment referred to 'the mutation below', which lives in decisions.md rather than in this file. Cites mechanisms.md#TWO-ELEMENT-GROUPS under the excerpt discipline instead -- and the citation test caught the first attempt, which had paraphrased the em dash away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…E, not one per role Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test named for every role it renders carried two GIVEN and two MIDDLE and no FAMILY, so skipping the partition for FAMILY passed it -- caught only by its siblings and by R3's example line, neither of which is about per-role application. Verified by mutation before and after: the skip-FAMILY mutation now fails this test and did not. Same miss as the first draft's, one role apart. A zero-element group is the identity as surely as a one-element group and reads even less like a gap, which is how it survived the round that had just named the one-element form. decisions.md and mechanisms.md#TWO-ELEMENT-GROUPS both described the fixture wrongly and are corrected; the mechanism's only worked example was certifying as fixed the gap that survived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ase bullet Two findings from the branch review. The partition BUILDS a group, and that group needs the two-element treatment as much as the role group does. The only fixture with a two-element folded run was 'Hassan, Mohamad Ahmad Ali', whose run is 'Ahmad Ali' -- both initial to 'A.', so reversing the folded half alone passed every test in the file. Verified before and after: the reversal mutation now fails test_initials_order_folded_words_ first_like_the_family_field and previously failed nothing. Third occurrence of the arity shape on this branch, one level down each time: a one-element role group, then a zero-element one, now a two-element group whose members the assertion cannot tell apart. mechanisms.md#TWO-ELEMENT-GROUPS says count the elements; this one says count the ones the assertion can DISTINGUISH. Separately, the #385/#402 bullet six lines above the #408 bullet, in the same Unreleased section, still told readers the two APIs order initials differently and that #408 was open. Every clause of it was false as shipped: the values agree at 'y. v. d.', the 87 constructed inputs are 0, and #408 closes below it. No count survives the fix, so none is carried forward. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_render.initials, its sibling test and mechanisms.md#FOLDED_TAG all say the pipeline puts the tag on FAMILY tokens alone -- which is why the GIVEN and MIDDLE arms of the partition are uniformity rather than reachable behavior. Measured true (0 of 1094 corpus names carry it elsewhere) and tested nowhere, so a rule that ever folded into another part would falsify all three silently and turn initials()'s 'would otherwise reopen #408 there' into a live gap with nothing to fail. Verified by mutation: re-roling one producer's fold to MIDDLE fails this guard. It also asserts its own floor, so it cannot pass by exercising nothing. Three comment overclaims fixed with it, all the reach-versus-rule shape this branch keeps producing: 'the one name the DEFAULT policy moves' is the one CORPUS name (constructed ones move too -- 'de la, y van', 'der, e van'); skip-FAMILY was caught by the siblings AND R3's example line, not the siblings alone; and draft 1 carried both arity misses, not one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five findings from the docs review of this branch. Each was
re-verified at HEAD before editing; all five still held.
decisions.md#R3's recompute recipe measured the implementation
against itself. "Compare initials() against a folded-first partition
of tokens_for(role)" is what initials() now IS, so on the shipped
tree the recipe yields 0 differing parses, not 660 -- the
inert-measurement shape inside the sentence written to prevent it,
and the only recompute path the repo offered for the release log's
counts too. The recipe now names the comparator that produces the
number (the pre-change rendering: the same contributing tokens left
in WRITTEN order) and states that the skip filter comes first,
because dropping it counts every skipped word as a difference. All
four numbers measured 2026-08-30: 660 of 6564 with the filter and
written order, reproducing 1 / 71, 1 / 294, 1 / 292; 0 for the old
wording; 1546 for written order without the filter; 1018 for the
folded partition without it.
mechanisms.md#RENDER-HONORS-THE-PARSE called the fix "strictly
parity-RESTORING", which overstates in two directions. 588 of the
660 moving parses are under the two family-first orders, which v1
has no equivalent for, so nothing there is restored or broken --
decisions.md#R3 already says this. And the entry's own lead example
is a counterexample: parse("der, y van") gives y. d. v. before,
y. v. d. after, and 1.4.0 gives y. (measured on the released wheel).
The 54-of-71-and-none-away claim is kept and scoped to the
population it was measured over.
rules.md#R3's new order clause rested on a false premise -- "initials
abbreviate a field" -- which the rule's own first sentence and first
example line contradict: the family FIELD of "Dr. Juan Q. Xavier de
la Vega III" is de la Vega and its initials are J. Q. X. V. #461 is
open and is exactly that membership question, so a normative "the two
never disagree" would be the authority for forcing initials() to
match family_base -- the reading measured and backed out on the
previous branch. The clause now rests on order alone.
mechanisms.md#FOLDED_TAG claimed the consumer list "has failed there
twice in opposite directions". The second never shipped: the revise
strip arrived with Parser.revise in the same commit as the test that
pins it, and no issue, decision entry or test names a released defect
of that shape. Now one shipped failure and one hazard. Its "Lives in."
roster also gave that site as a bare method; it is
nameparser/_parser.py:187, and the roster is what a reader walks when
adding a view.
docs/release_log.rst's #408 bullet carried counts from neither source
AGENTS.md sanctions -- the bullet itself says the gate cannot see
initials(), and R3's example line witnesses the order without
counting. No count is dropped: each carries argument the bullet needs.
Instead the bullet now says where each comes from -- what moves is
recomputed by decisions.md#R3's now-working recipe, while the 1.4.0
comparisons and the facade sweep are dated snapshots nothing in the
repo re-derives.
Verified: 6129 passed / 156 skipped / 9 xfailed; mypy, ruff, sphinx
doctests and the README doctest clean; corpus rebuild 1094 / 241 with
no diff; gate 229 / 194 / 102 with unexplained: 0 at 1.4.0 / 2.0.0 /
2.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rule named two sources for a quantified release bullet: the differential gate's classified summary, or verification against a rules.md example. Neither can reach a claim about a render view. The gate compares the seven role fields and _ambiguities, so a change to initials() or capitalized() leaves its output identical to the byte, and an example line witnesses one output without counting anything. #408's bullet needed counts of exactly that kind, so it named a third source in prose -- a recompute recipe kept with decisions.md#R3 -- with no rule admitting one. Admit it, and carry the failure that recipe hit: its first draft compared initials() against a folded-first partition, which is what initials() now IS, so it reproduced 0 against a claimed 660 while being the only stated provenance for the number. Naming the comparator, and running the recipe as written before shipping the bullet, are what stop that. Cross-version numbers stay dated snapshots under Counting claims.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #465 +/- ##
=======================================
Coverage 98.65% 98.65%
=======================================
Files 45 45
Lines 3193 3194 +1
=======================================
+ Hits 3150 3151 +1
Misses 43 43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
initials()read a name in a different order than the fields of the same name.Closes #408
The defect
Two rules fold words into the family and render them before it — O3's
middle_as_family, and P6's tussenvoegsel attachment after a family comma._types._text_forhonors that: it collectsFOLDED_TAGcarriers and returnsfolded + parts._render.initials()walkedtokens_for(role)in written order and never applied the reorder, so the two views disagreed about one parse:mechanisms.md#RENDER-HONORS-THE-PARSEalready named this as an instance of its second shape — a view and a field disagreeing about the same parse — so the design was settled before implementation and is not re-argued here.It restores v1 parity rather than deviating from it
This is the opposite of what the issue assumed, and it is the finding that made the change cheap.
v1 had this feature as
CONSTANTS.middle_name_as_last, which_config_shimmaps tomiddle_as_family. Measured on the released 1.4.0 wheel, at the default order:middle_as_familyOf the 17 that match neither, 14 differ only in v1's per-
*_list-element initials granularity (V G.againstV. G.) and all 14 come into 1.4.0's letter order after the change, none before. Two differ because the 2.x parse itself differs —Smith, John V.is #432 and남궁민수 지훈is W1. The fifteenth isder, y van, where 1.4.0 contributes nothing for an all-particle family, which is R3's own decided clause.v1 has no general
name_order, so the family-first orders have no v1 reference in either direction.Reach
660 of 6564 parses move — 1094 corpus names × three
name_ordervalues ×middle_as_familyoff/on:The population matters more than the count. The shape an order change can be observed in is a contributing folded token standing after a contributing unfolded one of the same role. At the default order the corpus holds exactly 1 such name without the option and 72 with it — so the movers are 1 of 1 and 71 of 72, a near-total hit rate over a corpus carrying one instance of P6's half, not a reach near zero.
The differential gate cannot see any of this.
compare.pycompares the seven role fields plus_ambiguities;initialsappears nowhere in it. Gate output is byte-identical before and after, including all 67 / 58 / 51 per-heading counts. No ledger rule was added and none was needed — the same blindnessdecisions.md#R4records forcapitalized().Review
Four agents. Nine Important findings, no Critical, all fixed. Three are worth naming because they are the same failure shape at three depths.
Fixture arity, three times over. A partition over a group with fewer than two elements is the identity, so an under-populated fixture asserts the behavior and pins nothing.
Ahmad Ali— two elements that both initial toA., so reversing the folded half alone passed all 428 relevant tests.Recorded as
mechanisms.md#TWO-ELEMENT-GROUPS: count the elements, and count the ones the assertion can distinguish.A recompute recipe that measured nothing.
decisions.md#R3said to compareinitials()"against a folded-first partition oftokens_for(role)" — which is whatinitials()now is. On the shipped tree that yields 0 differing parses; the 660 comes from comparing against the pre-change written-order rendering. The inert-measurement shape inside the sentence written to prevent it, on a count the gate cannot check. The recipe now names the comparator and reproduces 660.A claim asserted in three places and tested nowhere.
_render.initials, its sibling test andmechanisms.md#FOLDED_TAGall say the pipeline putsFOLDED_TAGon FAMILY tokens alone — the entire justification for the GIVEN and MIDDLE arms being uniformity rather than reachable behavior. True (0 of 1094 names carry it elsewhere) and unguarded, so a rule that ever folded into another part would falsify all three silently. Now pinned, and verified by mutating a producer to fold into MIDDLE.Also fixed: a
2.2.0 - Unreleasedbullet six lines above this one still told readers the two APIs order initials differently and that #408 was open, with values false as shipped.Verification
corpus_rules.jsonlunchanged at 241 — R3's new example line reuses an input already present.decisions.md#R3reproduces, each failing at its own role's assertion.The rule this needed, amended
AGENTS.mdadmitted two provenance sources for a quantified release bullet — the gate's classified summary, or verification against a rules.md example. Neither can reach a render view: the gate's output is byte-identical across this change, and an example line witnesses an output without counting anything. This bullet named a third source in prose with no rule admitting one, so the rule now admits it — a recompute recipe kept with the design entry the bullet cites — and carries the failure that recipe hit, since naming the comparator and running the recipe as written are what stop it recurring. Cross-version numbers stay dated snapshots under Counting claims.🤖 Generated with Claude Code