core/docs/decisions/ADR-0184-distinct-unit-product-rule.md
Shay e195a229c9
feat(adr-0184): distinct-unit product rule — sealed reader wrong 13→8 (#486)
* docs(adr-0184): scope the distinct-unit product rule — cut the product-of-all over-commit

The 47-refusal coverage diagnostic surfaced that the headline 3/47/0 (serving
recognizer) hides the sealed comprehension reader's real state: resolve_pooled over
the 50 real train_sample cases is 2 correct / 13 WRONG / 35 refused. The confuser
probe's wrong=0 was a misleading proxy — all 13 real wrongs are the whole-text
product-of-all, the unique complete candidate, committed unopposed.

Scopes the first lever, decided by MEASURING candidate refusal rules against the real
metric (correct up, wrong down on train_sample):

  baseline                         2 / 13 / 35
  distinct-unit product (chosen)   2 /  8 / 40   <- cuts 5, zero coverage loss
  product spans >1 clause          1 /  4 / 45   <- destroys correct 0003
  drop all products                0 /  2 / 48

The distinct-unit rule: multiply/divide may compose DISTINCT units but a multiply
step whose operand repeats a non-empty unit already in the product (apples x apples,
cards x cards) is unit-incoherence -> refuse (unit^2 is never the answer). Empty-unit
operands exempt (0003 multiplies a blank-unit 0.75). Dimensional, not lexical
(ADR-0165-safe); refines verify.py clause 3 shared by self_verifies + classify.

Honest scope: 13->8, NOT 0. The remaining 8 are distinct-unit products in the wrong
shape (rate problems) = cue precision (ADR-0177 CP-2b), the next lever, NOT to be
faked with a per-case rule. Establishes the real scoreboard (resolve_pooled over
train_sample) and notes the ratification bridge (ADR-0175 Phase 5) as the separate
dependency for any of this to reach the serving headline.

Spec only; serving 3/47/0 untouched (verify is not on the serving path).

* feat(adr-0184): distinct-unit product rule — sealed reader real-GSM8K wrong 13->8

Cuts the over-eager product-of-all on real GSM8K. The sealed comprehension reader
(resolve_pooled over train_sample) was 2 correct / 13 WRONG / 35 refused; all 13 are
the whole-text product-of-all committed unopposed (0042->2.4M, 0048->19200,
0001->14400). This is the first lever measured against the REAL metric (resolve_pooled
over train_sample), not the curated confuser count.

Mechanism (verify._is_repeated_unit_product + classify_derivation downgrade):
a pure multiplicative chain whose operands repeat a non-empty unit forms unit^2
(apples x apples, cards x cards) -- never the answer; it is the product-of-all
multiplying independent groups. Such a product is classified `exempt`
(commit-INELIGIBLE), NOT removed. Empty units exempt (0003 multiplies blank-unit
0.75); divide exempt (feet/feet = legitimate count). Dimensional, not lexical
(ADR-0165-safe).

Implementation finding (folded into ADR §3.1): the naive version put the predicate
in the shared _base_reasons gate, which DROPPED the product and regressed the
confuser probe 1->3 -- the disguised-polarity 0001/0003 refuse only because the
coins x coins product DISAGREES with the coins + coins accumulation reading; dropping
it unmasked the additive reading as a unique wrong commit (80/30). The fix is the
downgrade: keep it as a commit-ineligible `exempt` candidate so it still forces the
disagreement. Pinned by test_downgrade_not_removal_preserves_disagreement_refusal.

Evidence (sealed lane; chat/ does not import verify -> serving frozen):
- resolve_pooled over train_sample: 2 correct / 8 wrong / 40 refused (was 2/13/35);
  the 5 repeated-unit products (0001/0017/0042/0045/0048) now refuse, 0003/0021 kept.
- confuser probe: wrong unchanged (no 0001/0003 regression), positives still solve.
- serving train_sample 3/47/0 and practice (accumulation + search) 3/47/0
  byte-identical; self_verifies/_base_reasons unchanged so search lanes are untouched.
- 171 derivation/pool/verify tests + 40 architectural invariants green.

Honest scope: 13->8, NOT 0. The remaining 8 (0011/0016/0018/0019/0025/0028/0032/0047)
are distinct-unit products in the wrong shape (rate problems) = cue precision
(ADR-0177 CP-2b), the next lever -- not to be faked with a per-case rule. Carries the
corrected ADR-0184 (supersedes the spec-only #484).
2026-05-30 08:35:35 -07:00

9.1 KiB
Raw Permalink Blame History

ADR-0184 — Distinct-unit product rule: cut the product-of-all over-commit (the first lever measured against real GSM8K)

Status: Accepted / Implemented. Implementation refined the mechanism: the rule is a commit-ineligibility downgrade in classify_derivation (§3), not the shared unit-clause failure §3 originally proposed — putting it in the shared gate regressed the disagreement refusals ADR-0182 relies on (see §3.1, the finding). Follow-on to ADR-0175 (the self-verification gate), ADR-0182 (pooling), and ADR-0177 (cue precision — the next lever this one hands off to).

One line. A multiplicative product may compose quantities of distinct dimensions but must not multiply two operands of the same non-empty unit (apples × apples, cards × cards × cards → a meaningless squared unit). This single dimensional rule cuts the sealed reader's real-GSM8K wrong count 13 → 8 with zero coverage loss, measured on train_sample.


1. Why this exists — the headline 3/47/0 was hiding the real reader

3/47/0 is the serving recognizer (conservative; refuses what it can't read). Run the sealed comprehension reader (resolve_pooled — everything ADR-0182 built) over the same 50 real cases and the picture is very different:

serving (recognizer):        3 correct / 47 refused /  0 wrong   ← the headline
sealed reader (resolve_pooled): 2 correct / 35 refused / 13 WRONG ← what we are building

The confuser probe's wrong=0 is real on its 30 curated cases but was a misleading proxy for real-GSM8K safety: the same reader commits 13 wrong answers on real problems. All 13 are the whole-text product-of-all — every step multiply, every one the unique complete candidate, so it commits unopposed:

case committed gold operand units
0042 2,400,000 30 bags × apples × bags × apples × apples
0048 19,200 4 cards × weeks × cards × cards × cards
0001 14,400 990 _ × hours × your × hours × days
0017 7,000,000 800 _ × _ × days × days
0018 60 16 minutes × goals × hours

The product-of-all self-verifies because multiplication is unit-permissive (clause 3 of the gate lets multiply compose across any units) and it uses every quantity (completeness ✓). The gate cannot tell a rate-chain product from a coincidental one — the "self-verification is necessary but not sufficient" finding (ADR-0175). This ADR removes the dimensionally impossible subset of those products; the rest hands off to cue precision (§6).

2. The measured lever (decided by the real metric, not a guess)

Each candidate refusal rule, evaluated on the sealed reader over train_sample (the metric that matters — correct↑wrong↓):

rule correct wrong refused
baseline (today) 2 13 35
product repeats a non-empty unit (this ADR) 2 8 40
product spans >1 clause 1 4 45
drop all products 0 2 48

The distinct-unit rule is the only option that cuts wrongs with zero coverage loss. "Span >1 clause" cuts more but destroys 0003 (a correct 3-sentence rate-chain 48 boxes × 24 erasers/box × $0.75); "drop all products" loses both correct answers. Coverage is never bought at the cost of a correct answer.

3. The rule (as implemented)

The dimensional predicate (_is_repeated_unit_product): a pure multiplicative chain (every step multiply) whose operands — including the start — repeat a non-empty unit forms unit² (apples × apples, cards × cards), which is almost never the answer to a word problem asking for that unit. Empty-unit operands are exempt (an unknown unit cannot be shown to collide — and the correct 0003 multiplies a blank-unit 0.75). Divide is exempt — same-unit division (feet / feet) is a legitimate dimensionless count. Dimensional, not lexical (ADR-0165-safe).

Such a product is classified exempt (commit-ineligible) in classify_derivation, not failed in the shared _base_reasons gate. It stays in the pool — it may force a disagreement refusal — but can never resolve as the answer. A unique repeated-unit product is therefore exempt-only → refuse (the 0042-class cut); a distinct-unit product stays complete → commit (0003/0021).

3.1 Finding — why a shared unit-clause failure was wrong

The first implementation put the predicate in the shared unit-consistency clause, so a repeated-unit product failed (was dropped from the pool entirely). That cut the 5 train_sample wrongs but regressed the confuser probe 1 → 3: the disguised-polarity cases 0001/0003 refuse only because the coins × coins product disagrees with the coins + coins accumulation reading. Dropping the product left the (wrong) additive reading unique → it committed (0001 → 80, 0003 → 30). The fix is the downgrade: keep the product as a commit-ineligible exempt candidate so it still forces the disagreement, but can never commit. (This is the same commit- ineligibility concept ADR-0182 introduced.) Pinned by test_downgrade_not_removal_preserves_disagreement_refusal.

Why this keeps the correct products

case product units repeated non-empty unit? verdict
0003 ✓ boxes × erasers × '' no commit (kept)
0021 ✓ pounds × reps × sets no commit (kept)
0042 ✗ bags × apples × bags × apples yes (bags, apples) refuse (cut)
0048 ✗ cards × weeks × cards × cards yes (cards) refuse (cut)

A genuine rate-chain composes distinct dimensions by construction; a product that revisits a dimension is multiplying independent groups (4 bags×20 + 6 bags×25, mis-read as 4×20×6×25) — never a real quantity.

4. wrong=0 obligations (must discharge before merge)

  1. train_sample (real, the metric): sealed-reader wrong 13 → 8, correct held at 2 (0003/0021). Failing-under-violation: a test that the 5 named cases (0001/0017/0042/0045/0048) refuse, and 0003/0021 still commit.
  2. Confuser probe: wrong must not regress and positives still solve. The §3.1 finding makes this load-bearing: the naive shared-clause version regressed it 1 → 3; the downgrade holds it at baseline (1 on main pre-#481; 0 once anchor-skip lands). 0001/0003 must stay refused, not commit 80/30.
  3. Practice search lane: search_runner commits products; confirm wrong=0 and correct not reduced (the rule may only add refusals there).
  4. Serving frozen: verify is not on the serving path (chat/_score_one_candidate_graph), so 3/47/0 stays byte-identical. Lane-SHA 8/8.
  5. Scope of the rule: applies to multiply (and divide) steps only; add/ subtract unchanged. A rare legitimate same-unit product (area ft × ft) will refuse — accepted (refuse-preferring; vanishingly rare in GSM8K).

5. Where it lives

generate/derivation/verify.py: _is_repeated_unit_product (the dimensional predicate) consulted by classify_derivation, which downgrades an otherwise- complete repeated-unit product to exempt. self_verifies / _base_reasons are unchanged — so the search lanes (practice) and serving stay byte-identical, and the change is confined to the pool's commit-eligibility (where the over-commit lives). Sealed lane only. tests/test_adr_0184_distinct_unit_product.py pins the predicate, the downgrade, the 0042-class refusal, and the §3.1 downgrade-not-removal regression guard.

6. What this does NOT fix — the honest hand-off to cue precision

wrong drops to 8, not 0. The remaining 8 (0011, 0016, 0018, 0019, 0025, 0028, 0032, 0047) are products over distinct units that are still the wrong shape — rate problems like 0018 (2 goals / 15 min × 120 min, mis-read 15 × 2 × 2). Telling a correct rate-chain (distinct units bound by per/each) from a coincidental distinct-unit product is exactly the cue-precision problem (ADR-0177 CP-2b) — which multiply a cue licenses. That is the next lever and must not be faked with a per-case rule here (the overfitting trap, feedback-synthetic-corpus-overfitting-trap).

This ADR is a safe, general down-payment (13→8, zero coverage loss), not the finish. It also establishes the real scoreboard: every future coverage lever is measured by resolve_pooled over train_sample (correct↑wrong↓ toward ≥10, 0) — never by the curated confuser count alone.

7. Dependency for any of this to reach the headline

Even at wrong=0 the sealed reader cannot move serving 3/47/0 without a ratification bridge (ADR-0175 Phase 5): a reviewed path that promotes the sealed reader's reliable readings into the serving path. Cutting the 13→8→0 wrongs is the precondition (a wrong-prone reader can never be ratified); the bridge is the separate, later step that makes the headline finally move.