Skip to content

Commit e4d13a9

Browse files
derek73claude
andcommitted
Sort differential rules most-specific-first, dropping the ordering rule
expected_changes.toml's rules were tried in file order, so a rule narrowed by name_regex had to be written above every rule narrowed only by fields -- an invariant the file could only defend in prose, and one it had already lost once (the fix(#271) CJK rule was dead on arrival at the end of the file, its diffs claimed and mislabelled by fields-only fix(suffix-routing)). compare.py now sorts the rules into two tiers before matching, stably, so rules within a tier keep the order they were written in. That makes the ordering rule structural, and the three places that documented it by hand -- the toml header, the #271 rule's own "FIRST on purpose" paragraph, and the README's ordering section -- collapse to one line each. No exit-code change: sorting cannot change WHICH rules match, only which of several matching rules wins the label. The run is unchanged against the current corpora (654 names, 18 intentional diffs, 0 unexplained, exit 0) -- with one fields-only rule in the file and the one specific rule that competes with it already written above it, the sort is a no-op today. Verified it is not a no-op in general: with the #271 rule moved to the end of the file, '毛泽东' classifies as suffix-routing unsorted and as fix(#271) sorted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a102c86 commit e4d13a9

3 files changed

Lines changed: 11 additions & 20 deletions

File tree

tools/differential/README.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,10 @@ matches only if the observed diff fields are a subset of this list).
116116
Keep both as tight as the actual diff allows -- a loose rule can mask
117117
a real regression.
118118

119-
Rules are tried in file order and the first match wins, so a rule
120-
narrowed by `name_regex` has to sit **above** any rule narrowed only
121-
by `fields`. The `fields`-only rules are broad by construction: a
122-
`fields = ["first", "last", "suffix"]` rule claims every diff confined
123-
to those three columns, whatever the name looked like. Adding the
124-
`fix(#271)` CJK rule at the end of the file was enough to make it dead
125-
on arrival -- `fix(suffix-routing)` claimed all of its diffs first, and
126-
labelled them as something else.
119+
Rules are sorted most-specific-first before matching -- a `name_regex`
120+
rule outranks a `fields`-only one (which is broad by construction)
121+
wherever both match -- so file order does not decide which rule claims
122+
a diff.
127123

128124
Some entries in the seed list are for behavior families that this
129125
particular corpus (pre-M12 v1 test strings) happens not to contain any

tools/differential/compare.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ def main() -> int:
6262
rules = tomllib.loads(
6363
(HERE / "expected_changes.toml").read_text()).get("change", [])
6464
validate_rules(rules)
65+
# Most-specific-first: a name_regex rule outranks a fields-only rule
66+
# wherever both match, so file order stops being load-bearing. The
67+
# sort is stable, so rules within a tier keep the order they were
68+
# written in.
69+
rules.sort(key=lambda r: not isinstance(r.get("name_regex"), str))
6570
# A glob that matches nothing must not read as "everything passed".
6671
# Comparing zero names would print 0 unexplained and exit 0 -- the
6772
# harness's own stated nightmare (see validate_rules), and a

tools/differential/expected_changes.toml

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
# S5). Rules are seeded from docs/superpowers/plans/notes-m12-diffs.md
44
# and the `classification="fix(...)"` rows in tests/v2/cases.py; keep
55
# each entry's `name_regex`/`fields` as tight as the diff allows.
6-
# ORDER MATTERS: classify() takes the FIRST matching rule, so a rule
7-
# narrowed by `name_regex` must sit ahead of any rule narrowed only by
8-
# `fields`, or the broad one silently claims its diffs under the wrong
9-
# label. See the #271 entry immediately below for a case that did.
6+
# File order is not load-bearing: compare.py sorts `name_regex` rules
7+
# ahead of `fields`-only ones before matching.
108

119
[[change]]
1210
issue = "fix(#271) native-script CJK: family-first order + hangul segmentation"
@@ -23,14 +21,6 @@ issue = "fix(#271) native-script CJK: family-first order + hangul segmentation"
2321
# tests/v2/test_regex_sync.py, which fails if the two disagree -- if
2422
# one ever does.
2523
#
26-
# FIRST on purpose, and this position is load-bearing. classify()
27-
# returns the first matching rule, and 'fix(suffix-routing)' below
28-
# declares no name_regex at all -- so with this rule anywhere after it,
29-
# every CJK diff was classified as a suffix-routing change instead
30-
# (verified: all four names of a synthetic CJK corpus landed there).
31-
# The label would have been wrong and this rule dead. A rule narrowed
32-
# by name_regex belongs ahead of every rule narrowed only by `fields`.
33-
#
3424
# Expected to match nothing against the current corpora, which contain
3525
# no CJK at all: build_issues_corpus.py requires an internal space, and
3626
# unspaced names are the shape this classifies. Kept for the reason the

0 commit comments

Comments
 (0)