Skip to content

refactor(#397): one call answers both sides of a link, and the clause's core claim is measured - #537

Merged
derek73 merged 2 commits into
masterfrom
claude/397-followup-link-bounds
Sep 22, 2026
Merged

derek73 merged 2 commits into
masterfrom
claude/397-followup-link-bounds

Conversation

@derek73

@derek73 derek73 commented Sep 22, 2026

Copy link
Copy Markdown
Owner

The short follow-up agreed at the close of #536 (#397, #461). No behavior change at any default or documented configuration.

One call answers both sides of a link

_name_word_beside(k, step, …) was called twice per generational connective, once per side. _between_name_words(k, lo, hi, …) answers both sides in one call: the left arm first, short-circuiting, then the right. step and its ternary are gone, and both callers (the join's frozen set and the maiden clause's link exception) make one call.

  • Behavior byte-identical over the feat(#397), fix(#461): the Catalan link joins, and one initials rule for every group #536 oracle: 157,188 names × 24 lexicon/policy configurations plus the facade and the no-parse paths, 3,931,700 rows, same digest.
  • One frame per generational connective saved: Josep Carod i Rovira 314 → 313, Jane Doe nee Puig i Soler 320 → 319, a clause run of 64 links 2,651 → 2,587. Reference frames 406.00 / 443.00 and every name without such a connective unchanged. Run shapes still double at ~2×.
  • Mutation: the fold exposed a pre-existing gap — the right-hand suffix and title tests survived the suite (the per-side calls had been mutated in pairs). A new LINK/PLAIN test pins them; every arm is now killed by a named test.
  • Frame figures quoted in _group.py, test_benchmark.py and decisions.md re-measured on one interpreter and updated; the landed ### P3 bullet is amended by a dated bullet, not edited.

The clause's delimiter-core claim, measured

_maiden_take's comment said a delimiter core between the marker and the clause's first word cannot pass for a name word. Measured over 802,282 parses under 13 core-bearing policies: reachable and true (25,536 of 596,392 maiden takes had such a core; the bound refuses it). Both readings are now pinned by unit tests and the comment states what happens.

One thing found and reported, not fixed: past the clause's first word, a core is an ordinary index to the neighbour arrays and can pass as the name word beside a link — Smith, John, PhD née Puig Mr. - i Soler under extra_suffix_delimiters=(" - ",) reads maiden Puig Mr. i Soler, where the same name without the core stops the clause at Puig Mr.. Unreachable at the default policy (extra_suffix_delimiters is empty), 0 corpus or case-row names; the repair threads cores through three call sites into _run_neighbours. Filed as #538; recorded in rules.md#M2 as a deviates: #538 example line and a dated decisions bullet, pinned as it reads today by a test named for it.

Review round

Three reviewers (code, tests, comments): two independent oracles found no diff and every mutation claim held. The round put the deviation on the record (#538), corrected two comment claims, added a banded pin on the absolute frame cost of a link-bearing name (fails on the pre-fold code), and renamed the property test's independent model so it is obviously not the implementation.

Verification

Suite 9653 passed / 324 skipped / 4 xfailed; mypy, ruff clean; coverage 100% on _group.py; sphinx doctest 255 / 0; five differential gates 0 unexplained (one new example line, classified at every baseline).

🤖 Generated with Claude Code

…'s core claim is measured

A connective is placed to join only where a name word stands on EACH
side of it, so neither caller ever wanted one side's answer:
`_name_word_beside(k, step, ...)` was asked once per side and the two
answers ANDed, in `_group_segment`'s `frozen` loop and again in
`_link_joins_inside_the_clause`. It is `_between_name_words(k, lo, hi,
...)` now and answers for both, so `step` and its ternary are gone and
a generational connective costs one Python frame where it cost two.
The left arm still short-circuits the right, and the neighbour arrays,
the two bounds and the two piece tests are the same tests in the same
order.

FRAMES, py3.11, through tests/v2/test_benchmark.py's own `_frames_for`
shape, b9ed142 -> 9fd8446 -> here: `Josep Carod i Rovira` 311 -> 314
-> 313, `Josep Lluis Carod i III` 377 -> 381 -> 380, `Jane Doe nee
Puig i Soler` 315 -> 320 -> 319, `John Quincy Adams i MA Prof.` 438 ->
443 -> 442, and the clause guard's own pair 1,125 -> 875 -> 859 at
sixteen links and 6,741 -> 2,651 -> 2,587 at sixty-four. Unmoved:
`tools/perf/call_count.py` (parse=406.00 facade=443.00), `John Smith`
172, `Smith, John` 203, `Juan Garcia y Lopez` 291, `John and Jane
Smith` 293, `Jane Doe nee Smith` 245, `Jane Doe nee Smith PhD` 340.
TIMING is unmoved too: `"Josep " + "i " * n + "Rovira"` and
`"Jane Doe nee Puig " + "i " * n + "Soler"` both read 1.96-2.02x per
doubling from n=200 to n=1,600, 11.36ms and 9.39ms at the top end
against 11.58ms and 9.58ms at 9fd8446.

Byte-identical over 3,931,700 parses -- 157,188 names (corpus,
cases.py, the property grids and a run-heavy generated set) under six
lexicons and four policies, plus the facade surface and the #528
no-parse paths -- all twenty-six digest rows share their sha256 and
the two dumps compare equal.

THE FOLD BOUGHT A TEST. Per side, a mutation of the suffix or the
title test hit both sides at once and the left-hand rows killed it;
folded, the two sides mutate independently and both right-hand tests
survived the whole suite, their own rows standing at the END of the
name where `hi` refuses them first.
test_a_credential_or_honorific_mid_name_on_the_right_too is the
mid-name pair that kills them, and every arm of the folded predicate
now dies by a named test.

THE CLAUSE'S DELIMITER-CORE CLAIM IS REACHABLE AND TRUE, and it is not
the whole story. Measured over corpus u cases.py u the property grids
u a 50,925-name generated set with cores, under thirteen core-bearing
policies: 25,536 of 596,392 maiden takes had a core standing between
the marker run and `lo`, where the bound refuses it. PAST `lo` a core
is not below it, is an ordinary index to `_run_neighbours` (which
steps over connectives and nothing else), and DOES pass for the name
word beside a link -- asked about a core in 51,072 of 900,023 calls,
answering differently from a core-skipping reading in 8,094 parses
over 1,278 texts, 1,824 of them moving `maiden` on 288 texts. None is
a corpus or cases.py name and none is reachable at the default policy.
Both readings are pinned by unit tests and the comment now says what
happens; the repair threads `cores` through three call sites into
`_run_neighbours` and is its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@derek73 derek73 added this to the 2.4 milestone Sep 22, 2026
@derek73 derek73 self-assigned this Sep 22, 2026
@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.78%. Comparing base (9fd8446) to head (1313f4e).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #537   +/-   ##
=======================================
  Coverage   98.78%   98.78%           
=======================================
  Files          45       45           
  Lines        3633     3636    +3     
=======================================
+ Hits         3589     3592    +3     
  Misses         44       44           

☔ 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.

… and a link's frame cost is pinned

rules.md#M2 gives the link exception "a connective standing between two
name words of the clause", and a delimiter core the caller declared
through extra_suffix_delimiters is structure rather than a name word.
Past the clause's first word one passes for that word anyway, so
`Smith, John, PhD née Puig Mr. - i Soler` under a configured ' - ' reads
maiden 'Puig Mr. i Soler' where its separator-less twin stops at
'Puig Mr.'. The preamble says a mismatch is classified, never defaulted,
so the shape lands in M2 as a `deviates: #538` example rather than as
prose: the runner asserts today's reading and the marker leaves in the
PR that repairs it. The annotation needs a policy the registry had no
entry for, so tests/v2/rules_doc.py gains
`extra_suffix_delimiters-dash`, named after the Policy field and
carrying the delimiter because the field holds a set, not a flag. A new
dated bullet under decisions.md#M2 carries the measurement and names
#538 as the open repair.

The example's string enters corpus_rules.jsonl, where the gate parses it
with the DEFAULT facade -- no delimiter declared, so #538's reading is
off that path entirely and the name moves for the 2026-09-20 link fix
instead. Classified as that at all five baselines: added to the
fix(#397) clause-link alternation at the four 2.x ledgers (suffix
'PhD i Soler' -> 'PhD', maiden 'Puig Mr. -' -> 'Puig Mr. - i Soler',
identical at each), and given its own rule at 1.4.0, where v1 had no
maiden markers and read the whole suffix-comma tail as one suffix. Four
_CORPUS_CLAIMS rows move, each by exactly one name, verified name by
name; all five gates end 0 unexplained with radar counts unchanged.

A LINK'S ABSOLUTE COST WAS UNPINNED. The clause-run guard bounds a
ratio, and one more frame per link moves both of its ends -- so
re-splitting the fold this branch just landed leaves it green. Measured:
a copy of this tree carrying `git archive 9fd8446 nameparser` in place
of its own runs 9,652 passed / 324 skipped / 4 xfailed with a single
failure, the new `test_a_link_costs_what_it_is_pinned_at`. The pin is
`_clause_run(64)` at 2,587 frames on py3.11 in a +-2% band, keyed by
interpreter like `_CALL_BASELINE`; the 16-link end is not pinned because
2% of 859 is 17.2 and the regression is 16. An unmeasured interpreter
skips rather than fails, and the reviewer's 3.12/3.13/3.14 figures are
recorded in the comment rather than in the table, having no run here
behind them.

THREE COMMENT FIXES, all re-measured. _group.py said
'PhD née - i Jones' "under a configured ' - ' declines"; it does not --
with no comma there is no tail segment, so the dash is an ordinary word
at either policy, and the text that declines is
`Smith, John, PhD née - i Jones`. _group.py and the 2026-09-21 P3 bullet
said the earlier run-of-64 pair was py3.11 and only the short names were
not; measured, b9ed142 reads 6,741 on the nose while 6048eb5 reads
2,651 against the 2,652 recorded, so the splice ran through a single
arrow. test_benchmark.py's unmeasured "one frame above that again" now
carries the re-reading. The P3 bullet also points at the two dated
entries that still spell the predicate `_name_word_beside`, so a grep
closes the trail.

Renames, both about what a name claims: the rough-edge pin is
test_a_core_beside_a_link_wrongly_passes_for_a_word_until_538 with a
docstring saying to update it when #538 lands, and
test_properties.py's independent per-side mirror is
`_name_word_on_the_side` -- its logic untouched, its name no longer the
implementation's.

No parser behavior change: `uv run python tools/perf/call_count.py`
reads parse=406.00 facade=443.00, _group.py stays at 100% coverage, and
the suite is 9,653 passed / 324 skipped / 4 xfailed against a 9,649
baseline (+1 doc example, +1 benchmark test, +2 corpus-parametrized rows
the new name adds).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@derek73
derek73 merged commit 23e52dc into master Sep 22, 2026
11 checks passed
@derek73
derek73 deleted the claude/397-followup-link-bounds branch September 22, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant