Skip to content

Stop is_initial matching CJK characters (#320) - #321

Merged
derek73 merged 9 commits into
masterfrom
fix/initial-script-repertoire
Aug 2, 2026
Merged

Stop is_initial matching CJK characters (#320)#321
derek73 merged 9 commits into
masterfrom
fix/initial-script-repertoire

Conversation

@derek73

@derek73 derek73 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closes #320. Blocks #319, which should go next on the corrected classification this establishes.

The bug

_vocab.is_initial used v1's ^(\w\.|[A-Z])$. Python's \w is Unicode-aware, so 씨. matched. is_suffix_strict applies is_initial as a veto (V. in John V. Smith is a middle initial, not roman five) — so a veto written for Latin was being asked of scripts it was never about, and period-written CJK honorifics failed the strict suffix test.

The cost ran past the honorific itself. Measured against 1.4.0:

input 1.4.0 2.x before this PR after
김민준, 씨. first 씨. / last 김민준 identical to 1.4.0 family 김민준, suffix 씨.
田中さん, 様. first 様. / last 田中さん identical to 1.4.0 family 田中, suffix さん, 様.

Those two rows were at parity with 1.4.0 despite #307, #308 and #312 all being shipped. Those issues intended this behavior and did not deliver it — the veto stood in the way and 2.x quietly behaved like 1.x. Seven honorifics are affected: 様 殿 氏 군 님 씨 양.

The mechanism is the peel's site, not the comma structure: _is_post_nominal asks is_suffix_strict, which returned False for 様., so the scan-back stopped at it, took it as the site, found no listed tail there, and abandoned. Structure is FAMILY_COMMA before and after.

The fix

Split the shape test from the repertoire test rather than narrowing the character class.

_INITIAL stays v1-verbatim, so all three pinned copies — including public config.REGEXES["initial"] — are byte-unchanged and test_regex_sync's three-way relationship holds. _policy._NO_INITIALS names the four scripts whose characters cannot be an initial; is_initial_shaped is the shape half alone and is_initial ANDs the two.

Both halves have callers, and telling them apart is the point. The veto and the initial tag want is_initial. _assign's roman-numeral fork wants is_initial_shaped — it asks whether the preceding piece looks like part of an initial run, a question about layout. An earlier revision of this PR had it read the narrowed tag, which cost John 씨. V its family name outright; see the review section below.

Narrowing to [A-Za-z], which the issue originally proposed, would have been wrong in the other direction: it flips Й. from tag initial to tag conjunction — the regression the 2.1.0 Ukrainian entry claims to prevent — and strips the public initial tag from every Cyrillic, Greek, Arabic and Hebrew initial. Neither moves field output on a short name, so both pass a green suite. tests/v2/pipeline/test_classify.py now asserts the tag.

_NO_INITIALS is enumerated rather than derived from _SCRIPT_RANGES. The criterion is orthographic convention — does the writing tradition abbreviate a given name to one character plus a period — not what a character is; stating it phonologically would misfile Devanagari, an abugida whose initials are real. And enum membership settles nothing about abbreviation: Script.KATAKANA is in it so the classifier can name what it deliberately declines, and keys no default behavior at all. A gate fails until any new member is classified, verified with a temporary Script.THAI.

Verification

  • Suite 2905 passed / 17 skipped / 11 xfailed; mypy and ruff clean; Sphinx -W clean
  • Differential: 742 names, unexplained: 0, expected_changes.toml untouched
  • Seven new cases.py rows, each classified from measured 1.4.0 values rather than predicted
  • Latin verified exhaustively: every corpus name parsed with and without the fix — only CJK moves, zero Latin-only names affected
  • Mutation testing: 13 mutants, zero survivors

Review pass

Four reviewers went over the whole branch after the first five commits; a3c31f6, 08e61f6, 2e9bee4 and 82f9815 are the result. Two defects that per-commit review could not see:

  • John 씨. V lost its family name — the roman-numeral fork described above. Blast radius measured at 336 of 8,960 generated names, all one shape.
  • The Script gate could be silenced without answering — wrong codepoint ranges plus a table row left it green while is_initial('ก.') still returned True. It now derives a character from _SCRIPT_RANGES and asserts behavior.

Also four more pins (including the spaced 田中さん 様., which a docstring cited as the motivating failure while no test asserted it, and the lenient_comma_suffixes=False path, which had no case row at all), a seventh doc place the six-place sweep missed (_types.py's hand-maintained twin of docs/modules.rst), and three corrected prose claims.

Also here

tools/differential/README.md documents a trap this branch hit: inserting python into the worker command bypasses its PEP 723 pin, so 2.x answers while labelled 1.4.0. The corrupted output is the expected values, so it reads as confirmation and would yield a parity classification — the opposite of the truth. PYTHONSAFEPATH=1 does not rescue it; the route is the project .venv's editable install.

Scope

The release note is scoped to the ASCII period. Fullwidth and ideographic full stops, halfwidth katakana and NFD hangul miss the vocabulary by different mechanisms and stay open as #322. Also filed from this review: #323 (양. 지훈 splits the given name) and #324 (case-table bookkeeping).

🤖 Generated with Claude Code

derek73 and others added 5 commits August 1, 2026 23:34
An initial is a single letter standing in for a name. v1's regex used
\w, which is Unicode-aware, so a Han ideograph or hangul syllable plus
a period matched -- and is_suffix_strict applies is_initial as a veto,
which pushed period-written CJK honorifics ('씨.', '様.') out of the
suffix vocabulary entirely.

Split the shape test from the repertoire test rather than narrowing
the character class. _INITIAL stays v1-verbatim, so its three pinned
copies (config, _vocab, _render) stay byte-identical; _NO_INITIALS
names the scripts whose characters cannot be an initial and is_initial
composes the two. Narrowing to [A-Za-z] would have been wrong in the
other direction -- it flips 'Й.' from initial to conjunction, the
regression the 2.1.0 Ukrainian entry claims to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding a Script member already trips a chain of gates: test_policy's
test_every_script_member_has_a_range_entry demands a codepoint range,
and satisfying that trips test_regex_sync's two differential-rule
gates. None of them asks whether the script has initials, which is a
separate question from whether it determines an order or a
segmentation -- and #317 (Thai) is a live candidate that would
otherwise inherit the CJK answer silently.

Every gate in that chain is satisfied by editing _SCRIPT_RANGES, the
toml's character classes, or the four-member roster literal
test_regex_sync spells out -- none of which requires answering the
initials question. This one compares a hand-written classification
table against set(Script), so it fires at the first step and stays
failing through the rest.

Verified by adding a temporary Script.THAI member -- 2 failed (this
gate and test_policy); adding a range entry too -- 3 failed (this gate
and both regex_sync gates).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Cyrillic assertion is the load-bearing one: it asserts the TAG,
because the naive narrowing changes 'Й.' from initial to conjunction
without moving field output on a short name, which is how it passed a
full green suite unnoticed. The collision is live, not hypothetical --
й ships as a default conjunction (Ukrainian, 60943ed for #267) -- so
the test reads й from the module's _LEX rather than from a lexicon
built in the test body, which would keep passing if the shipped entry
were dropped.

Two of the three case notes state a different mechanism than the one
this change was expected to have. Measured rather than assumed:

- '김민준 씨.' does not turn on segmentation, which divides 김민준
  identically either way. effective_script('씨.') is None -- the
  trailing period defeats the wholly-one-script test -- so with '씨.'
  still a name piece the script_orders lookup declined for the whole
  name and the pieces fell back to name_order.

- '田中さん, 様.' is FAMILY_COMMA before and after; SUFFIX_COMMA needs
  more than one word ahead of the comma. Both runs were always in the
  #312 peel's reach. What moved is is_suffix_strict's answer: the peel
  scan-back stopped at '様.' as the site rather than stepping over it.

Each Korean row names the period-free sibling whose fields it must
match and before this did not (ko_honorific_after_comma and
ko_honorific_ssi); the Japanese row names
ja_honorific_glued_before_an_initial, the intended veto it is the
misfire of.

corpus_cjk.jsonl is regenerated because it is derived from the case
table (test_cjk_corpus_matches_the_case_table pins it).

Case classifications are UNDETERMINED pending the differential run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured, not guessed. The differential harness reports 739 names, 94
intentional diffs, 0 unexplained, exit 0 -- and every Latin row is
byte-identical to the pre-#320 run, as the design predicted: _INITIAL
is untouched and no Latin token carries a CJK character. The only three
rows the run gained are the three names Task 3 added to the CJK corpus,
each already claimed by an existing expected_changes.toml rule
(comma-family, suffix-routing, cjk-comma-compound), so the toml needed
no edit.

All three rows are fix(#320). What 1.4.0 returns, via the pinned worker:

  '김민준, 씨.'  -> first '씨.'  / last 김민준
  '김민준 씨.'   -> first 김민준  / last '씨.'
  '田中さん, 様.' -> first '様.'  / last 田中さん

For the two comma rows that is exactly what 2.0 produced with Tasks 1-3
reverted, so both sat at parity with 1.4.0 until this change; the veto
is what moves them. The spaceless-comma row 김민준 씨. is the one
judgement call: it already differed from 1.4.0 beforehand, as given 김 /
middle 민준 / family '씨.', because hangul segmentation divides the name
either way. But it did not differ as the fields the row now asserts --
the suffix and the family-first reading both arrive with the veto's
removal -- so it is classified to #320 rather than to the segmenter,
matching how ko_honorific_ssi is classified to #307 without naming the
same segmentation it also depends on.

Each row's notes now record what 1.4.0 did with the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The initial veto stopped being a claim about shape alone, and several
places had been written as though it still was.

AGENTS.md's suffix_not_acronyms/is_an_initial bullet is now marked
Latin-only. It enumerated a collision that no longer happens for the
seven single-character CJK members of suffix_not_acronyms -- 씨, 様, 氏,
군, 양, 님 and 殿. The last of those was missing from the working list
this pass started from; enumerating six of seven in a bullet that
exists to name a set is the failure being fixed, so it was checked
against SUFFIX_NOT_ACRONYMS rather than transcribed. _NO_INITIALS is
cited as what it is -- the four scripts that do NOT have initials --
rather than glossed backwards.

The two "kept in sync by hand" comments now say what stays in sync.
_vocab's records that "verbatim" is a promise about the PATTERN, not
the predicate: since #320, _INITIAL.fullmatch(text) and is_initial(text)
are different questions and '씨.' answers them differently, so the bare
pattern is not the thing to ask. The copy stays as verbatim as it ever
was -- config's REGEXES["initial"] still differs by the empty
alternative's `?`, exactly as the line above it and test_regex_sync
already said, so this is not and never was a byte-identical copy.

_render's records that its copy is deliberately NOT composed with the
repertoire test -- layering forbids the import, and its only call site
is v1's conjunction carve-out, which this pattern reaches only once
`normalized in lex.conjunctions` holds. Verified rather than assumed:
the default lexicon carries no CJK conjunction or particle, and neither
does JA, ZH, RU or TR_AZ. That is a property of shipped data and not an
invariant, conjunctions being configurable public API, so the comment
also records why the divergence stays harmless if a user breaks it:
CJK is caseless, so word.lower() and word.capitalize() agree.

_classify's contract line said "Policy is not consulted", which since
#320 is true of the Policy dataclass but not of the _policy module --
is_initial reads its _NO_INITIALS constant. Narrowed to Policy FIELDS,
which is the point the line was making.

docs/modules.rst's public `initial` tag gains the repertoire
qualifier. It described an initial-shaped word; the tag now also
requires a script that has initials.

The release-log entry says what an upgrader sees. Every example in it
was run -- the seven honorifics, the "田中さん, 様." peel and its
periodless twin, and the three alphabets that keep their initials
(А. С. Пушкин, م. الفارسي, Ա. Խաչատրյան) -- and the field claims are
per-spelling, the vetoed honorific landing in the family name in
"김민준 씨." and the given name only after a comma. It quotes v1's regex
as REGEXES["initial"] actually spells it, `?` included, since that is
the public name a reader will look up. It also records the finding from
the differential: measured against 1.4.0, "김민준, 씨." and "田中さん, 様."
were returning exactly what 1.x returns, so #307/#308/#312 shipped with
a hole wherever the honorific carried a period.

The differential README gains the trap this branch fell into twice.
compare.py invokes the worker by script path; inserting `python` before
it makes python the command and the script an argument, so uv never
reads the PEP 723 metadata and the 1.4 pin is never installed. With
nothing to satisfy, uv runs it in the project .venv, where the working
tree is installed editable -- 2.x answering every query under a 1.4.0
label. The README says so and says what does NOT explain it, because
the wrong story points at a mitigation that fails: sys.path[0] is the
script's own directory, which holds no nameparser, so this is not the
CWD trap AGENTS.md documents three sections up and PYTHONSAFEPATH=1
does not rescue it (measured: safe path on, still 2.0.0). Only an
absolute script path from outside the project fails loudly. It is worse
than an ordinary mistake because the corrupted output is the 2.x
expected values: every diff vanishes, the run comes out as parity, and
the reader draws the opposite of the truth. The fix is to stop trusting
a version number nobody made the worker report. Plus the zsh note:
`compare.py | tail` leaves $? as tail's status and PIPESTATUS undefined
(zsh's is the 1-indexed pipestatus), so a failing run reads as passing.

Prose only; no behavior change. Suite 2894 passed / 16 skipped /
11 xfailed, ruff clean, mypy clean over 95 files, sphinx-build -W clean.

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

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.47%. Comparing base (c8056aa) to head (82f9815).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #321   +/-   ##
=======================================
  Coverage   98.46%   98.47%           
=======================================
  Files          41       41           
  Lines        2807     2811    +4     
=======================================
+ Hits         2764     2768    +4     
  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 3 commits August 2, 2026 10:49
Two findings from the branch review, both about the same seam between
"initial-shaped" and "is an initial".

assign's roman-numeral fork asks whether the piece before a trailing
single roman letter was an initial -- i.e. whether we are mid-run and
the letter is a name part rather than roman five. It asked through the
"initial" TAG, which #320 narrowed from initial-SHAPED to actually an
initial. The fork only ever needed the shape, and the narrowing cost
'John 씨. V' its family name outright (given John, family ''), while
'John 김. V' turned a middle initial and a family name into a family
name and a suffix. Blast radius was exactly that: last piece a bare
roman numeral, preceding piece one CJK character plus a period.

_vocab now names the two questions separately -- is_initial_shaped is
v1's is_an_initial verbatim, is_initial is that ANDed with the
repertoire test -- and assign calls the shape half on the token it used
to read tags from. One shared definition rather than a hand copy: the
repo has precedent for copied patterns (_assign._PERIOD_ABBREV) but
each one buys a test_regex_sync obligation. All five probed inputs
('John 씨. V', 'John 김. V', 'John 田. X', 'John 김. V, PhD',
'John 김. 김. V') are back to their 1.4.0/master fields, and the #320
honorific fixes are untouched.

The narrowed tag is NOT inert after this, which the review expected it
to be. Measured over all three differential corpora plus every case-
table row, with _classify's tag restored to the shape test: six field
diffs, all of them #320's own fixes. The route is _group's
_is_suffix_piece ('vocab:suffix' in tags and 'initial' not in tags) --
but as _assign imports it, in the trailing-suffix peel. Both call sites
INSIDE _group (_is_rootname, and _is_suffix_piece reached through it)
move nothing.

Second finding: test_every_script_is_classified_for_initials compared
the table to Script and its False rows to _NO_INITIALS, and never to
is_initial. A Script.THAI declared with a private-use range, put in
_NO_INITIALS with a False row, passed the whole test while
is_initial('ก.') still returned True -- the declaration contradicted
and nothing noticed. It also made the test the one on this branch on
the wrong side of the no-constant-content rule. It now derives a
representative character from that script's own _SCRIPT_RANGES entry
and asserts is_initial on it, matching the row. Shape-admitted rather
than simply the first codepoint, because several ranges open on an
unassigned or punctuation character (HIRAGANA's 0x3040, KATAKANA's
0x30A0) that \w rejects for the shape's reason, which would make the
repertoire assertion vacuous; a script with no initial-shaped character
anywhere in its spans fails outright. Reproduced: under the scenario
above the test now fails with "no character in _SCRIPT_RANGES[thai] is
initial-SHAPED".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fix moves 24 of 52 probed input shapes and the branch pinned three.
Four more rows, all measured before they were written, and all
classified against 1.4.0 measured through the pinned worker (which
reported 1.4.0, per the README's warning about the invocation that
silently answers 2.x).

- ja_honorific_with_a_period_no_comma, '田中さん 様.'. The SPACED form
  is the example is_initial's own docstring cites, and it was nowhere
  in the suite -- the existing row pins the COMMA form, which reaches
  the peel by flattening two runs where this one has a single run, so
  that row can stay green through a change that breaks this one.
  1.4.0: first 田中さん, last '様.' -- what 2.0 gave until #320.

- ko_honorific_period_under_strict_comma_suffixes, '김민준, 씨.' under
  Policy(lenient_comma_suffixes=False). The table had no
  lenient_comma_suffixes row at all, and this is the knob's own shape:
  a post-comma suffix that is initial-SHAPED. '씨.' was in that class
  by shape and the knob decided it; it is out of the class now and the
  honorific parses identically under both settings, which is what the
  row holds -- _script_segment names this setting as the one that
  strands 씨 in a neighbouring shape, and the knob's documentation
  scopes it to 'I' and 'V'. The knob has no v1 spelling, so the facade
  runner skips the row and the classification compares against 1.4.0's
  single reading (first '씨.', last 김민준).

- ko_honorific_yang_trails / ko_honorific_yang_written_with_a_period,
  '김민준 양' and '김민준 양.'. 양 is what suffixes.py singles out as
  the shipped vocabulary's risk class -- a top-tier surname admitted on
  the strength of trailing position alone -- and only its LEADING
  reading was pinned. Both spellings now agree, so a future tightening
  of the 양/군 policy cannot move one without the other. 군 gets no
  pair: it parses identically and carries no surname reading, so it
  would pin nothing these two do not.

corpus_cjk.jsonl regenerates from the case table (test_regex_sync pins
it). The differential run stays at unexplained: 0 -- the three new CJK
names classify against existing rules, 田中さん 様. and 김민준 양. under
fix(suffix-routing) and 김민준 양 under fix(cjk-honorific-suffix); no
rule was added or widened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six prose corrections from the review pass; no behavior change.

_types.py's STABLE_TAGS comment is the hand-maintained twin of
docs/modules.rst's block, which the branch updated and this one did
not -- it still promised the "initial" tag for any initial-SHAPED
word, which since #320 is false for CJK. Nothing pins the two copies
against each other, so the comment now says so.

is_initial's docstring named the wrong mechanism: measured, '씨.'
carried vocab:suffix and vocab:suffix-word before the fix as after,
and is_suffix_lenient took it either way -- the veto rejected
is_suffix_strict alone, via _is_suffix_strict_n. cases.py already had
this right, so the docstring was contradicting the row it explains.

_NO_INITIALS' rationale argued from two things that do not hold. It
justified membership phonologically ("a syllable, not a letter"), but
Devanagari is an abugida and Arabic an abjad and the branch's own
tests assert both keep their initials -- so the stated criterion would
have put Devanagari IN. The operative question is orthographic: does
the tradition abbreviate a given name to one character plus a period?
That matters concretely for Thai (#317), an abugida the old wording
would misfile. And the enum does NOT admit only scripts that determine
a convention -- KATAKANA is in it so the classifier can name what it
declines, and neither DEFAULT_SCRIPT_ORDERS nor segment_scripts'
default mentions it. The conclusion (do not derive _NO_INITIALS from
the enum) survives on the better ground that membership is granted on
assorted grounds.

The release note claimed the fix for "a period", true only of U+002E:
_normalize strips the ASCII period alone, so 씨. (U+FF0E), 씨。
(U+3002) and 씨。 (U+FF61) miss the vocabulary lookup before the veto is
reached, and all three parse identically before and after this branch.
Scoped, with the still-open _normalize gap named.

Also: test_classify's fixture comment and its docstring made opposite
claims about whether й shipping is load-bearing (the docstring was
right -- _LEX is local); that docstring named four scripts where the
sibling test asserts six; cases.py said "fields" where only the field
ASSIGNMENT matches; test_vocab called a digit "the" edge of \w when _
is equally inside it; and AGENTS.md's suffix_not_acronyms bullet now
says which of the two predicates the tension is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73

derek73 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Review pass — three commits added

Four reviewers went over the whole branch (code, tests, comments, silent failures). Mutation testing: 13 mutants, zero survivors, including the segments[:N] class that survived on an earlier branch. But the whole-branch pass found two defects that per-commit review structurally could not see.

a3c31f6 — the roman-numeral fork lost a family name

John 씨. V    1.4.0 and master:  given John | family V
              this branch:       given John | family ''

_assign.py:205 asks "was the preceding piece an initial?" to decide whether a trailing bare V/I/X is a name part or roman five — and it asked through the initial tag, which this PR narrowed from initial-shaped to actually an initial. The guard only ever needed the shape. Blast radius measured at 336 of 8,960 generated names, all one shape: trailing bare roman numeral preceded by one CJK character plus a period.

Fixed by applying this PR's own thesis one file further — is_initial_shaped (v1-verbatim shape) is now split from is_initial (shape AND repertoire), and the fork asks the former. All affected inputs match master again.

Worth recording: the initial tag narrowing is not inert after that fix. _assign binds _group._is_suffix_piece at import with four call sites of its own, separate from _group's internal uses, and that route is what delivers the #320 field fixes.

a3c31f6 — the Script gate could be silenced without answering

Add Script.THAI with wrong codepoint ranges, put it in _NO_INITIALS, add the table row: all green, while is_initial('ก.') still returned True. The gate compared table→_NO_INITIALSScript and never to is_initial. It now derives a representative character from _SCRIPT_RANGES and asserts the behavior, which also moves it off the wrong side of the project's no-constant-content rule.

08e61f6 — four pins

The fix moves 24 of 52 probed shapes; the PR pinned 3. Added the spaced 田中さん 様. (the example is_initial's own docstring cites, previously unpinned — the existing row is the comma form, a different peel branch), the lenient_comma_suffixes=False path (which had no case row at all), and 김민준 양 / 김민준 양. — 양 is a top-tier Korean surname and the shipped-vocabulary risk class, so pinning that the period form agrees with its twin is what stops a future 양/군 tightening moving one silently.

2e9bee4 — three of my claims were wrong

  • A seventh doc place. _types.py:84 is the hand-maintained twin of docs/modules.rst:52. The six-place sweep updated the doc and missed the source.
  • The is_initial docstring named the wrong mechanism — the veto rejected is_suffix_strict, not the vocabulary; 씨. kept vocab:suffix throughout, and is_suffix_lenient took it either way.
  • The _NO_INITIALS rationale was falsified twice. "The enum admits scripts that determine a convention" — Script.KATAKANA keys no default behavior and says so in its own docstring. And the phonological criterion ("letters, not syllables") does not discriminate: Devanagari is an abugida, Arabic an abjad, and both keep their initials. The operative criterion is orthographic convention — does the tradition abbreviate a given name to one character plus a period. This mattered because Thai, the motivating future case, is an abugida too.

Release-note scope corrected. "Fix a period after a CJK honorific" was true only for ASCII U+002E; 씨. (U+FF0E), 씨。 (U+3002) and 씨。 (U+FF61) miss the vocabulary lookup by a different mechanism and remain open. The note now says so rather than implying otherwise.

Gates: 2905 passed / 17 skipped / 11 xfailed, mypy and ruff clean, Sphinx -W clean, differential unexplained: 0, expected_changes.toml and public config.REGEXES["initial"] still untouched.

@derek73

derek73 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Follow-ups from this review are filed and out of scope here: #322 (compatibility spellings miss the CJK vocabulary — fullwidth/ideographic stops, halfwidth kana, NFD hangul), #323 (양. 지훈 splits the given name), #324 (case-table bookkeeping).

The 'John 씨. V' incident was written out three times: is_initial_shaped's
docstring, assign's call site, and a3c31f6's message. All accurate, and
collectively noise.

Keep the copy at the call site -- that is where someone debugging the
input actually lands -- and leave the commit message as the full record.
The docstring's job is helping a caller pick between the two predicates,
so it keeps the choosing rule and the discriminating example and points
at the fork rather than retelling it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73 derek73 added this to the v2.1 milestone Aug 2, 2026
@derek73
derek73 merged commit 9de5d2c into master Aug 2, 2026
11 checks passed
@derek73
derek73 deleted the fix/initial-script-repertoire branch August 2, 2026 19:00
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.

A trailing period stops a CJK honorific being recognized ("김민준, 씨." → given "씨.")

1 participant