Skip to content

Commit fca8bf4

Browse files
authored
Merge pull request #506 from derek73/claude/501-contest-pins
tooling(differential): the six 2.x contests get their winners, and a regex accident is narrowed away
2 parents e004ccf + f776a48 commit fca8bf4

8 files changed

Lines changed: 467 additions & 140 deletions

File tree

docs/design/decisions.md

Lines changed: 41 additions & 11 deletions
Large diffs are not rendered by default.

docs/design/mechanisms.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Problem shape. A guard needs to know what the answer WAS, so it can detect the a
7777

7878
## LEDGER-RULE-SEPARATION — file order decides, fields narrow by subset
7979

80-
Problem shape. Two differential-ledger rules claim overlapping names. Contract statement. Every ledger rule must carry a `name_regex` — since #451 `validate_rules` REJECTS a rule with `fields` and no `name_regex`, and one with neither was already rejected — so every rule sits in ONE tier, the sort is stable, and FILE ORDER decides every contest. Narrow-first is the declaration-free DEFAULT, not the contract: a wider rule can be the better classifier where it describes a compound behavior its component rule does not — `马丁·路德·金씨` divides on the nakaguro AND peels its glued honorific, so `fix(#272/#308)` describes it and `fix(cjk-glued-honorific-peel)` describes half of it — which makes `fields`-subset a proxy for specificity and the wrong one there. What IS the contract is that such a pair must be DECLARED: the earlier rule carries a `precedes_narrower` block naming the later one and saying why, and `undeclared_contests` refuses the ledger otherwise (#382). `fields` narrows a rule by subset; it does not separate rules by sorting. Narrowing by subset is not the whole contract: since #452 a rule's `fields` must EQUAL the union of the diffs it explains, and `compare.py` reports OVER-DECLARED and exits non-zero otherwise — a declared role no diff moves is not inert, it lets the rule keep claiming a name whose diff SHRINKS into the excess (decisions.md#differential-ledger). Since #468 there is a THIRD narrowing key: `orders` admits only the comparison orders it lists, the key being optional and its absence the order-blind reading every earlier rule has — a name compared under two orders can move the same roles for opposite reasons, so a rule describing an order-scoped fold would otherwise absorb that fold leaking into the default order (decisions.md#differential-ledger carries the worked case, and the legal set is borrowed from tools/differential/shapes.py rather than copied — plus one member no shape can declare, the `DEFAULT` sentinel naming the comparison run under no declared order, TOML having no null to put in an array). Exclusions take no `orders` and stay order-blind, deliberately. The ban ends the SHAPE and not the property it enabled: a required `name_regex` bounds nothing by itself, since the only width check is the sentinel probe — measured, `[a-z]` validates and reaches 970 of 1120 comparisons (2026-09-01, re-measured the same day after #486 widened the shapes corpus; it read 963 of 1113 before that). What changed is that such a rule now carries a `_CORPUS_CLAIMS` reach and digest, so its breadth is visible once at recording time rather than never (#452). The two-tier sort in `_sorted_rules` is KEPT although the ban makes it the identity on every ledger that loads (four ledgers load today, measured 2026-09-02; the open cycle's carries one rule, `fix(#462)`, and it carries a `name_regex` like every other, so the identity holds there for the same reason and not for want of rules): it is the defence for a reader that does not call `validate_rules` first — a future tool, a REPL, a test fixture — and its docstring in tools/differential/compare.py says so. How it works. Detail is owned by tools/differential/README.md. The file-order clause is measured, not theoretical: in the 1.4 ledger the comma-honorific-peel rule's fields are a strict subset of the comma-compound rule's, both carry a name_regex, and a pure reorder reattributes seven names — caught by _CROSS_RULE_WINNERS and by nothing else in the suite (#375's mutation). That pair is narrow-first and so declares nothing, and #382 settled what to do about it by declining BOTH predicates on offer. Narrowing the peel rule's own `name_regex` to the honorific-bearing shapes (#382 option 1) cannot make the pair order-independent at all, which is measured and not argued: the two rules ship an IDENTICAL `name_regex`, each rule's reach is computed from its own pattern, and the peel rule's `fields` nest inside the compound rule's — so the compound rule goes on reaching those names and goes on admitting their diffs whatever the peel rule's regex is narrowed to, and it would grow the `_HONORIFIC_SOURCES` sync roster for nothing. What holds the pair is what the arc kept: narrow-first order, with the seven names whose diff both rules admit pinned by name in `_CROSS_RULE_WINNERS`. A mechanical narrow-first sort (option 3) was refused on a false premise: it reattributes names to a rule describing half of what happens to them. So order separates this pair, by the declaration-free narrow-first default, and it is the WIDE-first pairs that must declare themselves (the rule-order arc under decisions.md#differential-ledger). What that check covers is NESTED pairs: two rules whose `fields` merely INTERSECT are decided by file order too, and sit outside it by the same reasoning that leaves EQUAL `fields` outside — neither rule is narrower, so there is nothing for a `precedes_narrower` block to name — with #498 carrying the worked case and the measured size of the class. The old #271/#272
80+
Problem shape. Two differential-ledger rules claim overlapping names. Contract statement. Every ledger rule must carry a `name_regex` — since #451 `validate_rules` REJECTS a rule with `fields` and no `name_regex`, and one with neither was already rejected — so every rule sits in ONE tier, the sort is stable, and FILE ORDER decides every contest. Narrow-first is the declaration-free DEFAULT, not the contract: a wider rule can be the better classifier where it describes a compound behavior its component rule does not — `马丁·路德·金씨` divides on the nakaguro AND peels its glued honorific, so `fix(#272/#308)` describes it and `fix(cjk-glued-honorific-peel)` describes half of it — which makes `fields`-subset a proxy for specificity and the wrong one there. What IS the contract is that such a pair must be DECLARED: the earlier rule carries a `precedes_narrower` block naming the later one and saying why, and `undeclared_contests` refuses the ledger otherwise (#382). `fields` narrows a rule by subset; it does not separate rules by sorting. Narrowing by subset is not the whole contract: since #452 a rule's `fields` must EQUAL the union of the diffs it explains, and `compare.py` reports OVER-DECLARED and exits non-zero otherwise — a declared role no diff moves is not inert, it lets the rule keep claiming a name whose diff SHRINKS into the excess (decisions.md#differential-ledger). Since #468 there is a THIRD narrowing key: `orders` admits only the comparison orders it lists, the key being optional and its absence the order-blind reading every earlier rule has — a name compared under two orders can move the same roles for opposite reasons, so a rule describing an order-scoped fold would otherwise absorb that fold leaking into the default order (decisions.md#differential-ledger carries the worked case, and the legal set is borrowed from tools/differential/shapes.py rather than copied — plus one member no shape can declare, the `DEFAULT` sentinel naming the comparison run under no declared order, TOML having no null to put in an array). Exclusions take no `orders` and stay order-blind, deliberately. The ban ends the SHAPE and not the property it enabled: a required `name_regex` bounds nothing by itself, since the only width check is the sentinel probe — measured, `[a-z]` validates and reaches 970 of 1120 comparisons (2026-09-01, re-measured the same day after #486 widened the shapes corpus; it read 963 of 1113 before that). What changed is that such a rule now carries a `_CORPUS_CLAIMS` reach and digest, so its breadth is visible once at recording time rather than never (#452). The two-tier sort in `_sorted_rules` is KEPT although the ban makes it the identity on every ledger that loads (four ledgers load today, measured 2026-09-02; the open cycle's carries one rule, `fix(#462)`, and it carries a `name_regex` like every other, so the identity holds there for the same reason and not for want of rules): it is the defence for a reader that does not call `validate_rules` first — a future tool, a REPL, a test fixture — and its docstring in tools/differential/compare.py says so. How it works. Detail is owned by tools/differential/README.md. The file-order clause is measured, not theoretical: in the 1.4 ledger the comma-honorific-peel rule's fields are a strict subset of the comma-compound rule's, both carry a name_regex, and a pure reorder reattributes seven names — caught by _CROSS_RULE_WINNERS and by nothing else in the suite (#375's mutation). That pair is narrow-first and so declares nothing, and #382 settled what to do about it by declining BOTH predicates on offer. Narrowing the peel rule's own `name_regex` to the honorific-bearing shapes (#382 option 1) cannot make the pair order-independent at all, which is measured and not argued: the two rules ship an IDENTICAL `name_regex`, each rule's reach is computed from its own pattern, and the peel rule's `fields` nest inside the compound rule's — so the compound rule goes on reaching those names and goes on admitting their diffs whatever the peel rule's regex is narrowed to, and it would grow the `_HONORIFIC_SOURCES` sync roster for nothing. What holds the pair is what the arc kept: narrow-first order, with the seven names whose diff both rules admit pinned by name in `_CROSS_RULE_WINNERS`. A mechanical narrow-first sort (option 3) was refused on a false premise: it reattributes names to a rule describing half of what happens to them. So order separates this pair, by the declaration-free narrow-first default, and it is the WIDE-first pairs that must declare themselves (the rule-order arc under decisions.md#differential-ledger). What that check covers is NESTED pairs: two rules whose `fields` merely INTERSECT are decided by file order too, and sit outside it by the same reasoning that leaves EQUAL `fields` outside — neither rule is narrower, so there is nothing for a `precedes_narrower` block to name — with #498 carrying the worked case and the measured size of the class. A REGEX ACCIDENT — a rule reaching a name through its pattern rather than by describing it — has no declaration site at all when it lands in a narrow-first or a non-nested pair, `precedes_narrower` sitting on the wide-first rule by construction, so nothing can refuse it and nothing can require a `why`; the repair is to narrow the accident away until the pair stops being a contest, which is what #501 did to the glued-peel `name_regex` after it put a FALSE label on a contract-tier name and stayed green (decisions.md, the #501 adjudication). The old #271/#272
8181
slug taboo is RETIRED (#333): the canonical-rule selector that keyed on those substrings is deliberately deleted — rule authors are free to use them in compound slugs — and the surviving rosters select on their own explicit keys (_HONORIFIC_SOURCES and _LATIN_ALTERNATION_SOURCES by named issue strings, _SPAN_BEARING_RULES by exact leading fix(...) tag). Lives in. tools/differential/compare.py, the expected_since_*.toml ledgers. Reach for it when. A ledger rule's behavior seems to depend on where it sits in the file — it does, and the reorder mutation is the test (run twice in #375; it fails _CROSS_RULE_WINNERS). History: #372 (closed) measured the then-existing fields-only rule owning 1639 of 5257 name×field pairs as filed (2026-08-10); #375/#376 then cut its classifier-of-record share sharply, and the residual pair ownership was read as the last-resort tier working as designed rather than a defect — until #451 retired the shape outright (decisions.md#differential-ledger). #372's two proposed mechanical checks were DECLINED with measurements (see decisions.md#differential-ledger), not left open.
8282

8383
## CANONICAL-VOCABULARY-AT-THE-BOUNDARY — one vocabulary at the comparison
@@ -90,7 +90,7 @@ Problem shape. A test's input depends on two config sets intersecting (a word th
9090

9191
## CROSS-RULE-OUTCOME-PINS — pin who wins the contest
9292

93-
Problem shape. Every per-rule roster measures a rule alone and the gate total is per-corpus, but WHICH rule wins a contested name is neither — and it is exactly what a reorder or a narrowing changes. Contract statement. Contested outcomes are pinned as data: a roster records which rule classifies which contested name, so a change in the winner fails the suite even when every total is unchanged. How it works. A pure file reorder in the 1.4 ledger fails _CROSS_RULE_WINNERS and nothing else in the suite — the pin is the only guard at that granularity. A shape recorded ALONE pins no winner and lives in the other roster, `compare._WATCHED_DIFFS`, so a reader looking for who wins a name does not look there (#501). Lives in. tests/v2/test_ledger_guards.py (_CROSS_RULE_WINNERS). Reach for it when. Two rules can claim the same name and you are about to change either one, or their order.
93+
Problem shape. Every per-rule roster measures a rule alone and the gate total is per-corpus, but WHICH rule wins a contested name is neither — and it is exactly what a reorder or a narrowing changes. Contract statement. Contested outcomes are pinned as data: a roster records which rule classifies which contested name, so a change in the winner fails the suite even when every total is unchanged. How it works. A pure file reorder in the 1.4 ledger fails _CROSS_RULE_WINNERS and nothing else in the suite — the pin is the only guard at that granularity. A shape recorded ALONE pins no winner and lives in the other roster, `compare._WATCHED_DIFFS`, so a reader looking for who wins a name does not look there (#501). The entry criterion is that the boundary is ARGUED ANYWHERE and not that it is argued in the guard module, because a ledger comment's argument about which rule wins a name is exactly what no guard and no run can see — the #501 adjudication is the instance, three of its six boundaries having been argued in a ledger comment and one of those three wrongly, green everywhere for a month (decisions.md, the #501 adjudication). Lives in. tests/v2/test_ledger_guards.py (_CROSS_RULE_WINNERS). Reach for it when. Two rules can claim the same name and you are about to change either one, or their order.
9494

9595
## VOCABULARY-FEEDS-STRUCTURE — a wordlist edit can move the comma decision
9696

tests/v2/test_differential.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,10 +2180,11 @@ def test_the_shape_check_reads_the_section_for_the_LEDGER_it_ran(
21802180
`_RECORDED_DIFFS['expected_since_1.4.0.toml']` survived the whole
21812181
suite: every other main() test here runs at the default 1.4.0
21822182
baseline, so the right answer and the hardcoded one are the same
2183-
dict. Harmless only while the 2.x sections stay empty, which is
2184-
open (#501) -- and the shapes are baseline-relative by
2185-
construction, so reading the wrong section is reading measurements
2186-
of a different comparison.
2183+
dict. That was harmless only while the 2.x sections stayed empty,
2184+
and since #501 filled them it is not: 'MD, PHD' is keyed in three
2185+
sections at two different shapes, so the hardcoded lookup would now
2186+
read measurements of a different comparison and say the parser
2187+
moved.
21872188
21882189
A correct row in the 2.0.0 section and a DECOY in the 1.4.0 one:
21892190
dispatched right, the run is quiet; dispatched to 1.4.0, it reports

0 commit comments

Comments
 (0)