Skip to content

Commit 5f5bb5e

Browse files
derek73claude
andcommitted
Classify the #272 kana fixes in the differential harness
The CJK rule's character class gains the two kana blocks, and its label names both issues: #272's kana license and nakaguro separator move name pieces between the same first/middle/last fields on the same native-script input, so this is one diff shape rather than two, and splitting the rule by issue would buy no tightness. The sync pin widens with it, and stops scoping itself by issue. That scoping rested on a premise #272 falsified -- the kana members were classification-only, keyed by no default -- and comparing against the whole _SCRIPT_RANGES table is the stronger promise anyway: a new script now fails this test until someone decides in writing whether the rule should cover it, which the added set assertion says out loud. Both corpora still contain no CJK name at all, so the rule classifies nothing in a real run; the harness passes 654 names with 18 classified diffs and none unexplained. That blind spot is #295's subject, cited in the rule's comment, and it is why the behavioral guarantee lives in the case table rather than here. corpus_issues.jsonl is deliberately not regenerated: a dry run adds three Latin strings from #296 and, as #295 predicts, no CJK -- unrelated tracker content that belongs in its own reviewable act. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 37cf758 commit 5f5bb5e

2 files changed

Lines changed: 52 additions & 30 deletions

File tree

tests/v2/test_regex_sync.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -164,49 +164,56 @@ def test_comma_char_matches_the_pipeline_comma_set() -> None:
164164

165165

166166
def test_differential_cjk_rule_matches_the_script_ranges() -> None:
167-
"""The #271 rule in tools/differential/expected_changes.toml hand-
168-
copies the CJK spans from _vocab._SCRIPT_RANGES into a character
167+
"""The CJK rule in tools/differential/expected_changes.toml hand-
168+
copies the script spans from _vocab._SCRIPT_RANGES into a character
169169
class. A TOML file cannot import the constant, so this is the one
170170
copy with no possible alternative -- and the one whose divergence
171171
is quietest, because the harness is run by hand rather than in CI.
172172
173173
Both failure directions matter, which is why this compares sets
174174
rather than checking coverage. A span MISSING from the class turns
175-
an intended #271 change into an UNEXPLAINED diff (a release
175+
an intended #271/#272 change into an UNEXPLAINED diff (a release
176176
blocker for the wrong reason); a span that should not be there
177177
silently classifies a real regression as intended, which is the
178178
failure the whole harness exists to prevent.
179179
180-
Han's astral block is out of scope on both sides. The rule omits
181-
it deliberately -- no corpus name reaches it, see the comment
182-
there -- so the comparison runs over the BMP spans only, and a new
183-
BMP script added to _SCRIPT_RANGES for the SAME #271 behavior
184-
(family-first order / hangul segmentation) still fails here until
185-
the rule covers it.
186-
187-
#272 added HIRAGANA/KATAKANA to _SCRIPT_RANGES for an unrelated
188-
reason (kana classification for effective_script's license, not
189-
the order-flip or segmentation #271's diff rule explains -- no
190-
default keys on either kana member), so the comparison is scoped
191-
to the #271 scripts by name rather than every table entry; a
192-
future script added for #271's own reason still must extend both
193-
sides, same as before.
180+
Every table entry is in scope. The rule covered HAN and HANGUL
181+
alone while the kana members existed only for classification, but
182+
#272 gave HIRAGANA a default order entry and made the kana blocks
183+
part of the same first/middle/last diff shape the rule explains,
184+
so scoping it by issue no longer draws a real line. Comparing
185+
against the whole table is also the stronger promise: a script
186+
added to _SCRIPT_RANGES for ANY reason fails here until someone
187+
decides, in writing, whether the rule should cover it.
188+
189+
Han's astral block is the single exception, out of scope on both
190+
sides. The rule omits it deliberately -- no corpus name reaches
191+
it, see the comment there -- so the comparison runs over the BMP
192+
spans only.
194193
"""
195194
toml_path = (Path(__file__).parents[2] / "tools" / "differential"
196195
/ "expected_changes.toml")
197196
rules = tomllib.loads(toml_path.read_text())["change"]
198-
matched = [r for r in rules if "#271" in r["issue"]]
197+
matched = [r for r in rules
198+
if "#271" in r["issue"] or "#272" in r["issue"]]
199199
assert len(matched) == 1, (
200-
f"expected exactly one #271 rule in {toml_path.name}, "
200+
f"expected exactly one CJK rule in {toml_path.name}, "
201201
f"found {len(matched)}")
202+
# A new table entry must force an explicit decision rather than
203+
# quietly widening (or failing to widen) the rule above.
204+
assert set(_vocab._SCRIPT_RANGES) == {
205+
Script.HAN, Script.HANGUL, Script.HIRAGANA, Script.KATAKANA}, (
206+
"a Script joined _SCRIPT_RANGES: decide whether the "
207+
f"differential rule in {toml_path.name} should cover it, then "
208+
"update this assertion")
202209
declared = {
203210
(int(lo, 16), int(hi, 16))
204211
for lo, hi in re.findall(r"\\u([0-9A-Fa-f]{4})-\\u([0-9A-Fa-f]{4})",
205212
matched[0]["name_regex"])}
206213
expected = {span
207-
for script in (Script.HAN, Script.HANGUL)
208-
for span in _vocab._SCRIPT_RANGES[script]
214+
for spans in _vocab._SCRIPT_RANGES.values()
215+
for span in spans
209216
if span[1] <= 0xFFFF}
210217
assert declared == expected, (
211-
f"{toml_path.name}'s #271 name_regex declares {sorted(declared)}; "
218+
f"{toml_path.name}'s CJK name_regex declares {sorted(declared)}; "
212219
f"_SCRIPT_RANGES' BMP spans are {sorted(expected)}")

tools/differential/expected_changes.toml

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,40 @@
77
# ahead of `fields`-only ones before matching.
88

99
[[change]]
10-
issue = "fix(#271) native-script CJK: family-first order + hangul segmentation"
10+
issue = "fix(#271/#272) native-script CJK: family-first order, hangul segmentation, the kana license and the nakaguro"
1111
# '毛 泽东', '김민준': script_orders flips first/last for a name written
1212
# wholly in Han or Hangul, and the Korean surnames that now ship as
1313
# default vocabulary additionally split an unspaced hangul token into
14-
# last + first. Seeded from the fix(#271) rows in tests/v2/cases.py.
14+
# last + first. '高橋 みなみ', 'マイケル・ジャクソン': #272 extends the
15+
# same order table to kana-bearing Japanese names and makes the
16+
# katakana middle dot a token separator, which moves pieces between the
17+
# same three fields. Seeded from the fix(#271) and fix(#272) rows in
18+
# tests/v2/cases.py.
19+
#
20+
# One rule, not two, because it is one diff shape: both issues move
21+
# name pieces between first/middle/last on native-script CJK input, and
22+
# splitting the class by issue would need a rule per script with no
23+
# gain in tightness (the fields list is already the narrow half).
1524
#
1625
# Scoped by script, which is exactly the scope of the behavior --
17-
# neither change can touch a name with no CJK character in it. Han's
26+
# nothing here can touch a name with no CJK character in it. Han's
1827
# astral block (U+20000-U+323AF) is deliberately left out of the class:
1928
# no name in either corpus reaches it, and a rule should be no wider
2029
# than the diffs it has to explain. Extend it -- and the sync pin in
2130
# tests/v2/test_regex_sync.py, which fails if the two disagree -- if
22-
# one ever does.
31+
# one ever does. The kana spans are BMP-only for a different reason:
32+
# supplementary-plane kana is hentaigana and other archaic forms no
33+
# modern name uses, so _SCRIPT_RANGES does not list it either.
2334
#
2435
# Expected to match nothing against the current corpora, which contain
25-
# no CJK at all: build_issues_corpus.py requires an internal space, and
26-
# unspaced names are the shape this classifies. Kept for the reason the
27-
# suffix-delimiter rule below is kept -- ready the moment a name lands.
28-
name_regex = "[\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uAC00-\\uD7A3]"
36+
# no CJK at all -- build_issues_corpus.py requires an internal space,
37+
# and unspaced names are the shape this classifies. That blind spot is
38+
# tracked as #272's and #271's shared gap in issue #295, which also
39+
# records the provenance decision a fix needs; until it is closed the
40+
# behavioral guarantee lives in the fix(#271)/fix(#272) rows of
41+
# tests/v2/cases.py, and this rule is kept ready for the moment a CJK
42+
# name lands in a corpus.
43+
name_regex = "[\\u3040-\\u309F\\u30A0-\\u30FF\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uAC00-\\uD7A3]"
2944
fields = ["first", "middle", "last"]
3045

3146
[[change]]

0 commit comments

Comments
 (0)