Skip to content

Actually exercise the surname split under drawn lexicons - #313

Open
derek73 wants to merge 5 commits into
masterfrom
test/fuzz-surname-split
Open

Actually exercise the surname split under drawn lexicons#313
derek73 wants to merge 5 commits into
masterfrom
test/fuzz-surname-split

Conversation

@derek73

@derek73 derek73 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

tests/v2/test_properties.py manufactures a surname + "민준" token so the unspaced-surname split in script_segment gets exercised under randomly drawn lexicons. It was in the pool but rarely in the name — one entry among ~30, against a name of 1–8 pieces.

Instrumented over 250 examples, before:

stage count
draws 250
lexicon + policy admit an unspaced token 13–19
…and one reaches the name 0–4
split fires 0–4

Conversion from sampled to fired was already 100% — nothing was wrong in the stage, the site scan, or the activation gate. The shortfall was sampling, so this inserts one at a drawn position when the draw admits it (drawn position, not the front: the stage takes the first activated-script token, so both placements are worth covering).

Twelve randomized runs of 250 now give 0, 2, 5, 6, 6, 7, 9, 9, 10, 12, 15, 19 — median 8, against 0–2 before.

Not a guarantee, and the comment says so: the insertion only fires when the drawn lexicon carries a hangul surname and the drawn policy activates hangul, about 14 draws in 250, so a thin run can still reach zero. What changed is that alignment now converts every time instead of one time in five.

Also corrects the comment this replaces

It claimed the surname half was "structurally inert, not luck", citing a single derandomize=True run reporting zero. That run is one sample and repeating it cannot disagree with itself — randomized runs gave 0, 1, 1, 2, 2. The structural part is real but narrower than claimed: w + "민준" on a non-hangul surname is a mixed-script token whose effective_script is None, so those candidates can never be a site whatever the policy says. Only a drawn hangul surname makes a usable one. The rest was sampling luck.

The comment now also warns to re-measure under derandomize=False rather than trusting its numbers, since repeating the committed run returns the same count forever.

Scope

The peel half still rides on sampling alone (0–5 per run). It is saturated by case rows and stage tests, so a second forced piece was not worth the distribution shift; the mechanism generalizes if that changes.

Test Plan

  • uv run --extra ja pytest -q — 2854 passed, 16 skipped, 11 xfailed
  • uv run mypy — clean, 96 files
  • uv run ruff check nameparser/ tests/ — clean
  • Before/after fire counts measured by instrumenting _split and driving the same strategies under derandomize=False

Follow-up from #311.

🤖 Generated with Claude Code

The manufactured 'surname + 민준' token was in the pool but rarely in
the name: one entry among ~30, against a name of 1-8 pieces. Measured,
the lexicon and policy agree on ~14 draws in 250 but the token reached
the name only 0-4 times -- and every time it did, the split fired. The
shortfall was sampling, not the stage, so insert one at a drawn
position when the draw admits it. Twelve randomized runs of 250 now
give 0-19 fires (median 8) where they gave 0-2.

Also corrects this comment's own claim. It called the surname half
'structurally inert, not luck' on the strength of a single
derandomize=True run reporting zero -- but that run is one sample and
repeating it cannot disagree with itself. The structural part is real
and narrower than stated: a non-hangul surname makes a mixed-script
token that can never be a site. The rest was luck.
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.46%. Comparing base (e3ddf2e) to head (8fd2eb3).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #313   +/-   ##
=======================================
  Coverage   98.46%   98.46%           
=======================================
  Files          41       41           
  Lines        2803     2803           
=======================================
  Hits         2760     2760           
  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.

@derek73 derek73 self-assigned this Aug 1, 2026
@derek73 derek73 added the tooling label Aug 1, 2026
derek73 and others added 4 commits July 31, 2026 23:34
The note said Han segmentation is "opt-in via locales.ZH, which these
policies do not draw", implying the Han entries are inert here. True of
locale PACKS, and false of what `_policies` actually generates: it
draws `segment_scripts` freely, and HAN lands in 37.7% of drawn
policies (measured over 20000 draws). An activated Han token is not
scenery either -- standing earlier in the name it takes the surname
site and declines, so "欧阳 김민준" does not split where "김민준 欧阳"
does.

Pre-existing, and two lines from where this branch has been editing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
script_segment's multi-match fork -- two vocabulary entries match the
same token, longest-first chooses, and the stage reports the reading it
passed over -- was fuzzed by nothing. The cause is structural rather
than statistical: the pool's only hangul entries were 김 and 남궁, and
no entry was a proper PREFIX of another, so `matches` could never hold
more than one length whatever the draw did.

남 is a shipped Korean surname and a prefix of 남궁, so a lexicon drawn
with both makes 남궁민준 match twice and sends the parse down the
ambiguity-emission-plus-index-remap path.

Instrumented rather than assumed, because reachable is not the same as
reached: both entries have to land in the same drawn `surnames` (0.8%
of draws), so the fork fires about once in 900 examples -- 42 over
36000 measured -- and the committed derandomize=True seed does not
reach it. A randomized run is what sees it. Said in the note so the
next reader does not conclude from one green run that the entry is
doing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`drawn.insert(...)` writes into the list `st.lists` handed back. Safe
today -- that strategy builds a fresh list per draw -- but the hazard
class is the kind a fuzzer should not carry: a strategy that ever
handed back a shared or memoized list would turn this into a bug in
the test harness, reported as a bug in the code under test. The
rebuild costs nothing and draws in the same order, so the committed
seed still counts 23 surname splits and one peel.

Also names the shape honestly in the comment above it: the pool CAN
produce that token, it just loses the draw. "cannot deliver on its own"
contradicted the paragraph four lines up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third pass at this comment, and the first two both overstated the same
half from samples too small to carry the claim. What it said and what
measurement says:

* "the conversion from sampled to fired is 100%" -- it is about four in
  five (350 of 450 aligned examples over 24 randomized runs of 250).
  The conclusion it supported was right and is kept; the number is
  gone, replaced by the decline paths, which are all the stage
  deciding: a FAMILY_COMMA opts the stage out, or an earlier
  script-written token takes the surname site -- a bare drawn surname
  ('김 김민준'), a hangul post-nominal, or a Han token under a policy
  that activated HAN. That last group is what the rewrite deleted and
  this restores; a Latin word never takes the site (zero occurrences in
  1082 aligned examples), so the note that only Latin blocks it named
  the one blocker that does not bite.
* "13-19 [per] 250" alignment, "0-4 times" in the name, "one entry
  among ~30", "0-5 per run" for the peel -- measured 5.5% of draws over
  20000, a couple per 250, a pool of median size 23, and 0-7 peels per
  randomized run. Ranges taken from a handful of runs are dropped or
  widened.
* "roughly eight times what it was" -- follows from no measurement I
  can reproduce; dropped for the fire counts themselves.

Leads instead with the one figure that cannot rot: instrument _split
under the committed derandomize=True seed and origin/master counts zero
surname splits against two peels, this tree 23 against one. One
command, same answer every time.

Adds what the change does NOT buy, which is the honest headline: this
layer asserts totality and span-exactness only, so every behavioral
mutant in the stage passes here before and after and is killed by
tests/v2/pipeline/test_script_segment.py. The one defect class it can
catch is span arithmetic in _split, and the peel already reached that
path -- writing `base + end` as `base + end + 1`, origin/master finds
it in 8 runs of 10 at a median 1561 shrink calls and 14.6 seconds,
this tree in 10 of 10 at 346 calls and 3.6 seconds.

And two costs that went unmentioned: the insertion drops the stage's
i-is-None early return under a live configuration from 1.3% of
examples to 0.1% (it survives where a drawn quote pair carries the
token into a nickname), and an insertion at the end can suppress a
peel, since the peel site is the last non-post-nominal token.

Correction to this branch's first commit message, which is pushed and
not being amended: it gave the pre-change baseline as "0-2" where the
comment said 0, 1, 1, 2, 2, 4. Neither is the range -- 36 randomized
runs of 250 give 0-9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant