Skip to content

间隔号-divided transcriptions keep source order - #305

Open
derek73 wants to merge 8 commits into
masterfrom
v2.1/interpunct
Open

间隔号-divided transcriptions keep source order#305
derek73 wants to merge 8 commits into
masterfrom
v2.1/interpunct

Conversation

@derek73

@derek73 derek73 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #298, codepoint-scoped per the design amendment recorded in the issue (comment below).

U+00B7 — the Chinese 间隔号 — becomes the parser's first context-sensitive separator: it divides only between characters of a classified script, so the Catalan punt volat interior to Gal·la is untouched. A name it divides is a transcription — 威廉·莎士比亚 is William Shakespeare — and reads in source order (given 威廉, family 莎士比亚) with segmentation gated off (马丁·路德·金 under the zh pack keeps 马丁 whole even though is a listed surname). The dot's offsets ride ParseState.interpunct_offsets, recorded by tokenize exactly as comma offsets are (main region only, so a dot inside a nickname doesn't mark the outer name).

The Japanese nakaguro is deliberately NOT a transcription marker. 高橋・一郎 is roster formatting (姓・名) and keeps its #272 family-first reading, byte-identical — the two dots carry opposite conventions in exactly this position, so each is read by its own typography's standard: no language detection, the codepoint itself settles it. The cost is pinned as a chosen limitation: 威廉・莎士比亚 (a transcription typed with the Japanese dot) reads by the dot it was typed with, like the spaced form.

Guards: five case rows (including the limitation row and a Gal·la parity row), the CJK corpus regenerated (35 names), U+00B7 joining U+FF65 as the differential rule's second sanctioned extra with the sync test extended, and the rule slug now owning #298.

Test plan

  • Full suite 2700 passed / 14 skipped / 11 xfailed; mypy, ruff, sphinx html + doctest (207/0) clean
  • Differential harness vs pinned 1.4: 689 names, 48 intentional, 0 unexplained
  • Per-commit gate verification in an isolated worktree (every commit suite-green in sequence)
  • Spec acceptance criteria 1–7 verified live, including str(HumanName("威廉·莎士比亚")) == "威廉 莎士比亚" and string_format="{first}·{last}" reinstating the dot

🤖 Generated with Claude Code

derek73 and others added 6 commits July 30, 2026 01:52
Each dot is read by its own typography's standard. The Japanese
nakaguro (U+30FB/U+FF65) is roster/divider typography whose pieces the
script license already reads correctly (#272: 高橋・一郎 family-first,
マイケル・ジャクソン positional) -- it stays an unconditional pure
separator that records nothing. Only the Chinese 间隔号 U+00B7 carries
the transcription meaning (威廉·莎士比亚 keeps source order), so only
it records, as interpunct_offsets mirroring comma_offsets: main-region
only, gated by the same flag that keeps nickname commas out of
comma_offsets (record_commas renamed record_offsets).

U+00B7 cannot separate unconditionally -- it is also the Catalan punt
volat, interior to legitimate names (Gal·la) -- so it is the first
context-sensitive separator: it divides only between classified-script
neighbors (region-local flanks; an edge B7 stays token text), handled
in _tokenize_region where the index exists. The consumers (order and
segmentation suppression) arrive in the following commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A B7-divided name is a transcription: the dot plays for Han exactly
the role pure katakana plays in the kana license -- orthography naming
the convention -- so the family-first license (Policy.script_orders)
yields to the positional name_order. The check sits before the
script_orders lookup in _effective_order because the suppression
outranks the license.

Codepoint-scoped per spec decision 5 (2026-07-30 design doc): only
U+00B7 records interpunct_offsets. The Japanese nakaguro (U+30FB) is
roster typography whose divided pieces the license already reads
correctly, so #272's family-first reading of 高橋・一郎 is byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The transcription doctrine's second consumer, after _effective_order:
a B7-divided name's pieces are foreign-name syllable groups, not
surname+given, so the segmentation stage has no surname site to find
-- neither the vocabulary nor a configured segmenter applies.

Reachable today: script_segment takes the FIRST script token of the
name segment with no single-token gate, so 马丁·路德·金 (Martin
Luther King) would split 马 + 丁 under a zh vocabulary listing 马.
The gate sits directly beside the FAMILY_COMMA one -- both are
the-writer-already-decided opt-outs that run before either split
mechanism is consulted. Codepoint-scoped like the rest of #298: the
Japanese nakaguro records no offsets and therefore gates nothing.

The gate is state-global, matching the order twin: a marker anywhere
in the name suppresses segmentation of every token, so an un-dotted
hangul token beside a dotted Han one stays whole too ("김민준
马丁·路德" no longer splits 김 + 민준) -- a name carrying the
transcription marker reads as a transcription listing throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…298)

Five case rows land in tests/v2/cases.py, in the table's canonical
given/family/middle spelling:

- zh_interpunct_transcription_source_order: 威廉·莎士比亚 divides on
  the B7 and keeps source order (fix(#298)).
- zh_interpunct_nakaguro_typed_stays_roster: the SAME transcription
  typed with the Japanese nakaguro reads as a 姓・名 roster pair,
  family-first (fix(#272)). A limitation row, chosen not accidental:
  the design is codepoint-scoped (spec 2026-07-30 decision 5), so
  cross-convention input reads by the convention of the codepoint it
  was typed with -- only the Chinese B7 marks a transcription.
- ja_interpunct_b7_katakana: sloppy-IME B7 between katakana divides
  like the nakaguro; order was already positional (fix(#298)).
- latin_punt_volat_is_name_interior: Gal·la Marcet, the flank guard's
  reason -- Latin-flanked B7 is interior, never a divider (parity).
- zh_interpunct_suppresses_segmentation: 马丁·路德·金 under zh -- the
  dot gates segmentation off, 马丁 stays whole (fix(#298)).

The differential CJK rule's character class gains U+00B7, the second
sanctioned extra beside U+FF65: both dots change parses without being
classified by any Script (B7 doubles as Catalan's punt volat).
test_differential_cjk_rule_matches_the_script_ranges pins the pair as
sanctioned_extras; the compound rule's lookahead copies classified
spans only and is untouched.

corpus_cjk.jsonl regenerates 31 -> 35: the four CJK-bearing rows enter
(Gal·la Marcet correctly does not -- no classified codepoint). Harness
hand-run: 689 names, 48 intentional diffs, zero unexplained; the CJK
rule classifies 27 including all the B7 names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/release_log.rst gains the 2.1.0 Behavior Changes bullet for
U+00B7: the 间隔号 is a token separator between characters of a
classified script, the name it divides keeps its source order and is
never segmented, the Catalan punt volat is untouched, the nakaguro is
deliberately not a transcription marker, and rendering stays
space-joined with a custom string_format reinstating the dot. Placed
directly after the katakana-dot bullet, whose closing sentence hands
off to #298.

docs/usage.rst gains a paragraph in the East Asian section's Chinese
flow — the 间隔号 convention as background, then the separator
behavior and the never-segmented reading, with the nakaguro
distinguished — and the Boundaries limits paragraph states the two
limitations: the spaced form (威廉 莎士比亚) and the nakaguro-typed
transcription (威廉・莎士比亚) both keep family-first, because only
the Chinese dot marks a transcription. No new doctest blocks; the
case rows carry the examples.

docs/migrate.rst completes the "one exception" sentence: two dots are
now decided in tokenization rather than by policy — the katakana dot
unconditionally, the interpunct only between classified-script
characters. The nakaguro doctest and its explanation are unchanged.

docs/concepts.rst extends the first/last decider list: the interpunct
walks the script_orders entry back, marking a transcription that keeps
its source order.

docs/customize.rst needs no edit: its tokenization-not-policy sentence
is scoped to the nakaguro and stays true, and under script_orders={}
everything is positional already, so the dot adds nothing switchable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The differential rule now explains #298's B7 diffs, so its issue slug
says so -- compare.py output labels a B7 diff with the issue that
caused it. The sync test's substring selection (#271/#272) still
matches this one rule uniquely. And the corpus generator's docstring
spoke of #298 as future work it has since harvested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.45%. Comparing base (0e49f61) to head (c4ab1ca).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #305      +/-   ##
==========================================
+ Coverage   98.43%   98.45%   +0.01%     
==========================================
  Files          41       41              
  Lines        2750     2776      +26     
==========================================
+ Hits         2707     2733      +26     
  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 and others added 2 commits July 30, 2026 11:13
The B7 flank guard read text[i-1]/text[i+1] raw, so a policy-stripped
invisible adjacent to the dot defeated it: parse("威廉‏·莎士比亚")
left the literal dot in the family field, lost the order suppression,
and the polluted token defeated the script license too. That input is
the real-world case strip_bidi exists for -- RTL documents quoting
Chinese transcriptions get U+200E/200F inserted at script transitions,
in text visually identical to the clean form.

The guard now walks each flank past characters the strip policy
removes (bidi when strip_bidi, emoji when strip_emoji) before
classifying: a character that vanishes from the token stream cannot
occupy a flank position either. Whitespace, commas, and the name-dot
separators remain guard-defeating -- a dot beside a space is not
between characters -- and the region-local bounds are unchanged, so
the pinned edge behavior stays.

Also pins the guard's edge coverage the surviving mutants demanded:
the trailing-dot upper bound (whose loss is an IndexError, not a
wrong answer), the minimal one-char flanks, the one-sided flank
(the AND, and that no spurious offset records), and hangul flanks
(마틴·루터·킹 -- Korean writes interpunct transcriptions too).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The B7 sanctioned-extra span was a pure loss, removed: the flank
guard guarantees every name the dot can change contains a classified
character, so B7 names already matched the CJK rule through their
flanks -- the span's only actual effect was letting the rule claim
first/middle/last diffs on punt-volat Latin names (Gal·la Marcet),
pre-excusing a regression on exactly the input class the guard
protects. The sync test goes back to one sanctioned extra, and its
overlap loop now checks whole spans instead of discarding each
span's high end.

Three case rows close review-found gaps: the Korean transcription
마이클·잭슨 (마 is a listed census surname, and the spaced form
really does mis-split 마|이클 today -- the dot is what rescues it),
the suffix-comma composition 威廉·莎士比亚, PhD, and the half-flanked
王·Smith parity row. The suffix-comma row's corpus entry surfaced a
genuine compound diff -- comma routing (title to suffix) union the
CJK flip (first/last) -- that neither single-change rule may claim,
each excluding the other's fields on purpose; the new
fix(cjk-comma-compound) rule takes exactly that union, gated by the
same two-lookahead shape as the delimiter compound, and the compound
sync pin is parametrized over both slugs so neither span copy goes
unpinned.

Docs: usage.rst and migrate.rst state the per-flank judgment (a
hangul character beside a Han one qualifies), the state-global
breadth (a dot anywhere reads the whole name as a transcription
listing), and the half-flanked outcome (the undivided dot remains
part of the word); AGENTS.md's doctrine paragraph counts five
defaults and names the 间隔号 marker among them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@derek73 derek73 self-assigned this Jul 30, 2026
@derek73 derek73 added this to the v2.1 milestone Jul 30, 2026
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.

Transcribed foreign names in Chinese (威廉·莎士比亚) parse reversed or not at all

1 participant