You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/design/mechanisms.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,7 +77,7 @@ Problem shape. A guard needs to know what the answer WAS, so it can detect the a
77
77
78
78
## LEDGER-RULE-SEPARATION — file order decides, fields narrow by subset
79
79
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
81
81
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.
82
82
83
83
## 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
90
90
91
91
## CROSS-RULE-OUTCOME-PINS — pin who wins the contest
92
92
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.
94
94
95
95
## VOCABULARY-FEEDS-STRUCTURE — a wordlist edit can move the comma decision
0 commit comments