Skip to content

Commit 4eff183

Browse files
derek73claude
andcommitted
Tighten the compound rule to names the order flip can reach
Review found the delimiter-only regex claiming more than its comment said: 「」『』 sit in CJK Symbols and Punctuation, OUTSIDE every classified span, so 'John 「Jack」 Kennedy' matched and a bare first/last regression on a Latin name classified as intended. The rule now requires a classified codepoint alongside the delimiter (both lookaheads), drops the unearned middle field (all three claimed diffs touch exactly first/last/nickname), and its comment owns the one absorption subset-matching cannot avoid -- a nickname-only diff on a nakaguro name -- instead of claiming 'cannot absorb'. The second lookahead is the toml's second hand copy of the script spans, so it gets the same sync pin as the first, which also pins the delimiter set and records the #271/#272-substring taboo where a rule author will look. Plus the smaller review items: the provenance comment now credits the parity rows (the corpus members that pin NON-diffs), the positional note within the name_regex tier is stated, test_regex_sync's module docstring counts its outside-the-package tests correctly, and the README comma splice is mended. Noted, not changed: the Latin probes the review used do not fall to UNEXPLAINED without the compound rule -- the fields-only fix(suffix-routing) rule absorbs any bare first/last diff on any name, and always has. Pre-existing fields-tier looseness, outside this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ef7e116 commit 4eff183

3 files changed

Lines changed: 69 additions & 20 deletions

File tree

tests/v2/test_regex_sync.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
Layering is the usual reason for a copy but not the only one, so this
1313
module's scope is the PROMISE rather than that one pair of packages:
1414
the comma-set pin below reads _pipeline._state instead of config, and
15-
the last test reaches outside the package altogether, to a TOML file
16-
that could not import a Python constant if it wanted to.
15+
the last three tests reach outside the package altogether -- two to a
16+
TOML file that could not import a Python constant if it wanted to,
17+
one to a generated corpus whose generator can, and must stay run.
1718
"""
1819
import importlib.util
1920
import json
@@ -236,6 +237,42 @@ def test_differential_cjk_rule_matches_the_script_ranges() -> None:
236237
f"_SCRIPT_RANGES' BMP spans are {sorted(expected)}")
237238

238239

240+
def test_differential_compound_rule_matches_the_script_ranges() -> None:
241+
"""The fix(cjk-delimited-nickname) rule (#295) carries the SECOND
242+
hand copy of the script spans in the toml: its require-a-classified-
243+
codepoint lookahead exists so the delimiter set alone cannot claim a
244+
Latin name's first/last regression ('John 「Jack」 Kennedy' -- the
245+
corner brackets sit outside every classified span). Pin that copy to
246+
the table exactly as the canonical CJK rule's is, and pin the
247+
delimiter set itself, so widening either (to ASCII quotes, say) is
248+
an explicit decision here rather than a silent absorption change.
249+
250+
Selection note for future rule authors: the canonical-CJK-rule pin
251+
above selects by the literal '#271'/'#272' substrings and asserts
252+
uniqueness -- this rule's slug avoids them on purpose, and any new
253+
rule's must too.
254+
"""
255+
toml_path = (Path(__file__).parents[2] / "tools" / "differential"
256+
/ "expected_changes.toml")
257+
rules = tomllib.loads(toml_path.read_text())["change"]
258+
matched = [r for r in rules if "cjk-delimited-nickname" in r["issue"]]
259+
assert len(matched) == 1
260+
regex = matched[0]["name_regex"]
261+
assert "[「」『』・・]" in regex, (
262+
"the compound rule's delimiter set changed; decide deliberately")
263+
declared = {
264+
(int(lo, 16), int(hi, 16))
265+
for lo, hi in re.findall(r"\\u([0-9A-Fa-f]{4})-\\u([0-9A-Fa-f]{4})",
266+
regex)}
267+
expected = {span
268+
for spans in _policy._SCRIPT_RANGES.values()
269+
for span in spans
270+
if span[1] <= 0xFFFF} | {(0xFF65, 0xFF65)}
271+
assert declared == expected, (
272+
f"compound rule's codepoint lookahead declares {sorted(declared)}; "
273+
f"_SCRIPT_RANGES' BMP spans are {sorted(expected)}")
274+
275+
239276
def test_cjk_corpus_matches_the_case_table() -> None:
240277
"""corpus_cjk.jsonl is GENERATED, not curated (#295): every
241278
distinct case-table text bearing a codepoint the script table

tools/differential/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ that stops being run. Pass `--corpus PATH` (repeatable) to narrow it.
5151
| `corpus_cjk.jsonl` | the CJK-bearing rows of `tests/v2/cases.py`, via `build_cjk_corpus.py` (#295) | anything the case table itself missed — it re-witnesses reviewed expectations at the 1.4 boundary rather than discovering new shapes |
5252

5353
They are deliberately separate rather than merged: `corpus.jsonl` is
54-
reproducible forever from an immutable git ref, the issue tracker is
55-
mutable, so regenerating `corpus_issues.jsonl` is an explicit,
56-
reviewable act that can only add names — and the CJK corpus exists
57-
because BOTH of those are structurally blind to unspaced CJK (v1's
54+
reproducible forever from an immutable git ref, while the issue
55+
tracker is mutable, so regenerating `corpus_issues.jsonl` is an
56+
explicit, reviewable act that can only add names — and the CJK corpus
57+
exists because BOTH of those are structurally blind to unspaced CJK (v1's
5858
banks never tested it; `build_issues_corpus.py` requires an internal
5959
space, which unspaced names never have). It regenerates from the case
6060
table, and `tests/v2/test_regex_sync.py` pins the checked-in file

tools/differential/expected_changes.toml

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ issue = "fix(#271/#272) native-script CJK: family-first order, hangul segmentati
4545
# span.
4646
#
4747
# corpus_cjk.jsonl (#295) exists so this rule fires in every real
48-
# run: it derives from the fix(#271)/fix(#272)/fix(shime-mark) rows of
49-
# tests/v2/cases.py (build_cjk_corpus.py; the 〆 span rides in on the
50-
# same HAN entry), closing the blind spot the other two corpora have
51-
# by construction -- v1's banks had no reason to test CJK, and
52-
# build_issues_corpus.py requires an internal space, which unspaced
53-
# names never have.
48+
# run: build_cjk_corpus.py harvests every CJK-bearing text in
49+
# tests/v2/cases.py -- the fix(#271)/fix(#272)/fix(shime-mark) rows
50+
# AND the parity rows, whose value here is pinning NON-diffs (マイケル
51+
# must not change against 1.4) -- closing the blind spot the other
52+
# two corpora have by construction: v1's banks had no reason to test
53+
# CJK, and build_issues_corpus.py requires an internal space, which
54+
# unspaced names never have.
5455
name_regex = "[\\u3005-\\u3006\\u3040-\\u309F\\u30A0-\\u30FF\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uAC00-\\uD7A3\\uFF65-\\uFF65]"
5556
fields = ["first", "middle", "last"]
5657

@@ -131,14 +132,25 @@ issue = "fix(cjk-delimited-nickname) delimiter recognition compounds with the CJ
131132
# first/last. Neither single-change rule may claim the union (the
132133
# delimiter rule's fields exclude first/last, the CJK rule's exclude
133134
# nickname -- each on purpose, so a lone regression in the other's
134-
# fields stays loud). Scoped to the CJK-typographic delimiters and
135-
# the nakaguro only: a name containing 「」『』 or ・ is CJK-adjacent by
136-
# construction, so this cannot absorb a Latin delimiter diff. The
137-
# issue slug deliberately avoids the literal #271/#272 strings --
138-
# test_regex_sync's differential pin selects THE CJK rule by those
139-
# substrings and asserts it is unique.
140-
name_regex = "[「」『』・・]"
141-
fields = ["first", "middle", "last", "nickname"]
135+
# fields stays loud). Both lookaheads are required: the delimiters
136+
# alone would match 'John 「Jack」 Kennedy' -- the brackets sit in CJK
137+
# Symbols and Punctuation, OUTSIDE every classified span, so a Latin
138+
# name can carry them -- and would then absorb a bare first/last
139+
# regression on it. Requiring a classified codepoint too confines the
140+
# rule to names the order flip can actually reach. The second
141+
# lookahead's class is the same hand copy of _SCRIPT_RANGES the rule
142+
# above carries, pinned by the same sync test. One absorption is
143+
# inherent and accepted: a nickname-ONLY diff on a nakaguro name
144+
# ('マイケル・ジャクソン') classifies here, because subset-matching
145+
# cannot REQUIRE a field to have changed. Positional note: this rule
146+
# overlaps the CJK rule above and the delimiter rule below; within
147+
# the name_regex tier file order decides which label a diff reports
148+
# under, so it sits after the tighter single-change rules on purpose.
149+
# The issue slug deliberately avoids the literal #271/#272 strings --
150+
# test_regex_sync's differential pin selects the canonical CJK rule
151+
# by those substrings and asserts it is unique.
152+
name_regex = "(?s)(?=.*[「」『』・・])(?=.*[\\u3005-\\u3006\\u3040-\\u309F\\u30A0-\\u30FF\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFAFF\\uAC00-\\uD7A3\\uFF65-\\uFF65])"
153+
fields = ["first", "last", "nickname"]
142154

143155
[[change]]
144156
issue = "feat(#269) non-Latin titles/conjunctions recognized"

0 commit comments

Comments
 (0)