The lookback review of the four-PR stack (EX-6/pooling/prior-state/anchor-skip) found the ADR-0182 spec under-predicted and under-scoped 0016. Records actuals without editing the original spec (retained for provenance): - Status -> Accepted/Implemented (PRs #476, #480, #481). - §8 Realized results: confuser wrong spec-predicted 5->4->3, ACTUAL 5->2->1->0. Pooling also caught disguised-polarity (bonus); 0016 needed anchor-skip PLUS intra-clause accumulation (a conjunction split the spec framed as anchor-skip alone) and yielded the bonus 0017 *solve*; the prior-state guard (#480) is a distinct question-time lever sharing resolve_pooled. - Folds in the lookback findings: solid items, the closed test-coverage gap (anchor-skip refuse branches, #481 4205605), no live wrong=0 hazard (multi-actor product-commit predates anchor-skip), and the open 0010 multi-referent spurious (a graduation question, not a wrong). Serving 3/47/0 byte-identical throughout. Doc only.
274 lines
16 KiB
Markdown
274 lines
16 KiB
Markdown
# ADR-0182 — Cross-composer disagreement pooling: refuse distractor-quantity confusers without a reactive cue rule
|
||
|
||
**Status:** Accepted / Implemented (PRs #476, #480, #481). See
|
||
[§8 Realized results](#8-realized-results-2026-05-29) for actuals vs the
|
||
predictions below — the implementation **exceeded** the spec and required one
|
||
mechanism the spec under-scoped (intra-clause reading). The spec text below §7 is
|
||
retained unedited for provenance. Follow-on to
|
||
[ADR-0163-F2](./ADR-0163-F2-confuser-corpus-spec.md) (the confuser probe),
|
||
[ADR-0175](./ADR-0175-calibrated-attempt-and-eliminate-learning.md) (the
|
||
self-verification gate), and [ADR-0177](./ADR-0177-cue-precision-learning.md)
|
||
(cue precision). Sits *before* CP-2b in the dependency chain: it lets the
|
||
**disagreement rule** refuse a class of confusers that cue precision alone cannot
|
||
yet, using only structure the composers already produce.
|
||
|
||
> **One line.** Make the wrong=0 *disagreement* rule do the refusing: when a
|
||
> distractor-quantity problem admits both a blunt product reading and a competing
|
||
> additive reading, pool the self-verifying candidates **across composers** and let
|
||
> their disagreement refuse — instead of trying (and failing) to write a cue rule
|
||
> that tells a real multiplier from a distractor.
|
||
|
||
---
|
||
|
||
## 1. Why this exists (the failure it fixes)
|
||
|
||
The confuser probe's two **distractor-quantity** cases misfire — they are 2 of the
|
||
5 remaining `wrong` after [ADR-0163-F2 EX-6](./ADR-0179-extraction-richness.md)
|
||
(hyphen-bonded units) drove the pseudo-accumulation pair to refuse:
|
||
|
||
| case | text | committed | gold | via |
|
||
|---|---|---|---|---|
|
||
| `confuser-v1-0014` | "Kate has 20 pencils. She studies **for** 3 hours and then buys 5 more pencils. How many pencils?" | `20×3×5 = 300` | 25 | `search_chain` |
|
||
| `confuser-v1-0016` | "A train travels at 60 miles **per** hour **for** 2 hours. Tom has 8 tickets and buys 4 more tickets. How many tickets?" | `60×2×8×4 = 3840` | 12 | `search_chain` |
|
||
|
||
Both come from `search_chain`'s **product-of-all-quantities**, licensed by a present
|
||
multiplicative cue and forced to consume every quantity by the completeness clause.
|
||
|
||
### The microscope finding: no tight rule separates these from the legitimate cases
|
||
|
||
The distractor products are **structurally identical** to the legitimate
|
||
multiplicative products that `train_sample` requires to keep committing
|
||
(`correct` must never drop):
|
||
|
||
| case | text fragment | product | cue | operand units | required |
|
||
|---|---|---|---|---|---|
|
||
| `confuser-0014` (distractor) | "studies **for** 3 hours … buys 5 more pencils" | 20×3×5 | `for` | pencils × hours × pencils | **refuse** |
|
||
| `train-0021` (correct) | "bench presses 15 pounds **for** 10 reps … 3 sets" | 15×10×3 = **450** ✓ | `for` | pounds × reps × sets | **commit** |
|
||
| `confuser-0016` (distractor) | "60 miles **per** hour … 4 more tickets" | 60×2×8×4 | `for`/`per` | miles × hours × tickets | **refuse** |
|
||
| `train-0003` (correct) | "24 erasers in **each** box … 48 boxes" | 48×24×… = **864** ✓ | `each` | boxes × erasers | **commit** |
|
||
|
||
Every tight lexeme/gate rule was tested and breaks a protected case:
|
||
|
||
- *"refuse `for`-licensed cross-unit products"* → breaks **0021** (same cue, correct).
|
||
- *"refuse products mixing target-unit + foreign-unit operands"* → breaks **0003**
|
||
(`boxes` is foreign to the target `erasers`, yet the product is correct — the
|
||
canonical legitimate product has the **exact** distractor shape).
|
||
|
||
The only real difference is semantic: `"for 10 reps"` is a multiplicative binder;
|
||
`"for 3 hours"` is a durational adjunct. Distinguishing them by cue is precisely the
|
||
**cue-precision** problem that [ADR-0177](./ADR-0177-cue-precision-learning.md)
|
||
measured as *not yet solvable* ("no cue is reliable yet — every pattern floors at
|
||
~0.0"). Writing a rule to refuse 0014/0016 specifically would be a **reactive
|
||
surface patch** — the overfitting trap recorded in
|
||
`feedback-synthetic-corpus-overfitting-trap`.
|
||
|
||
So the lever is **not** "tell a good cue from a bad one." It is "make the engine
|
||
notice it has two incompatible readings and refuse the ambiguity" — which is the
|
||
wrong=0 disagreement rule it already owns.
|
||
|
||
## 2. Why naive pooling is not enough (the completeness obstacle)
|
||
|
||
The disagreement rule (`select_self_verified`) refuses when ≥2 **self-verifying**
|
||
derivations disagree. The obvious idea — run accumulation *and* the product search
|
||
and pool their candidates — does **not** work as-is, because the distractor problem
|
||
has only **one** self-verifying reading:
|
||
|
||
- The additive reading of 0014 (`20 + 5 = 25`, the actual structure) **leaves
|
||
`3 hours` unused**.
|
||
- The **completeness clause** (ADR-0175: "a trustworthy derivation must account for
|
||
every quantity the problem states") therefore **rejects** the additive reading —
|
||
it does not self-verify, so it never enters the pool.
|
||
- The product reading consumes every quantity, so it *is* complete, *is* the unique
|
||
self-verifying candidate, and commits `300`.
|
||
|
||
This is the deep cause: **for a distractor problem, completeness guarantees that the
|
||
only self-verifying reading is the one that wrongly consumes the distractor.** The
|
||
clause that protects multi-step problems is the same clause that mandates the
|
||
distractor misfire. Pooling must come *with* a principled, narrow completeness
|
||
relaxation, or it changes nothing.
|
||
|
||
## 3. The mechanism
|
||
|
||
Two coupled changes, both refuse-preferring, both gated by wrong=0 obligations.
|
||
|
||
### 3a. Isolated-foreign completeness exemption (read-only, commit-ineligible)
|
||
|
||
Relax completeness for a quantity that is an **isolated foreign unit** *relative to a
|
||
reading*: its unit is non-empty and equals **no *used* operand's unit** in that
|
||
reading. Such a quantity is a *candidate distractor* — it stands alone in a
|
||
dimension the reading does not touch. (The signal is the reading's own used-operand
|
||
units, which is always available — see §7 Q2: the question's asked-for unit is *not*
|
||
currently extracted, so the exemption must not depend on it.)
|
||
|
||
Crucially, a derivation that uses the exemption is **commit-ineligible**: it may
|
||
enter the candidate pool to *create disagreement*, but it may **never resolve as the
|
||
sole answer**. Committing still requires *full* completeness (every quantity used).
|
||
The exemption only ever buys a **refusal**, never an answer.
|
||
|
||
This is the load-bearing safety property: the completeness guarantee for *commits*
|
||
is untouched, so the multi-step-incomplete attempts ADR-0175 added it to catch (the
|
||
9→2 practice fix) still cannot commit. The exemption widens only what can *refuse*.
|
||
|
||
### 3b. Cross-composer candidate pooling
|
||
|
||
`select_self_verified` is invoked once over the **union** of candidates from all
|
||
composers (accumulation, in-clause multiplicative, target-guided chain) for a single
|
||
problem, rather than each composer resolving in isolation and the runner taking the
|
||
first non-`None`. Uniqueness/disagreement then operates across readings of
|
||
*different shapes*, which is where the real ambiguity lives.
|
||
|
||
Distractor-aware accumulation (3a's enabling reading): in a change clause that
|
||
carries >1 quantity, instead of refusing outright (current GB-3b.1 behavior),
|
||
accumulation may drop an **isolated-foreign** quantity and read the remaining
|
||
single same-unit change — producing the competing additive candidate. That
|
||
candidate is commit-ineligible (it left the foreign quantity unused), so it can only
|
||
force disagreement.
|
||
|
||
### Worked outcomes
|
||
|
||
| case | complete (commit-eligible) reading | exempt (refuse-only) reading | pool verdict |
|
||
|---|---|---|---|
|
||
| `0014` | product `300` | additive `25` (`3 hours` exempt) | disagree → **refuse** ✓ |
|
||
| `0016` | product `3840` | additive `12` — **requires anchor-skip** (see below) | disagree → **refuse** *(if anchor-skip)* |
|
||
| `0021` | product `450` | *none* (single clause, no anchor/change; no isolated-foreign) | unique → **commit** ✓ |
|
||
| `0003` | product `864` | *none* (no additive cue; no isolated-foreign) | unique → **commit** ✓ |
|
||
|
||
The legitimate products survive because they have **no competing reading** to
|
||
disagree with — exactly the property that distinguishes a real product from a
|
||
distractor, expressed structurally rather than by cue.
|
||
|
||
**0016 is the harder sub-case (honest accounting).** Its distractor occupies the
|
||
*anchor-position* clause ("A train travels 60 miles … for 2 hours" — two
|
||
foreign quantities), so the competing additive reading `8 + 4 = 12` is generated
|
||
only if **anchor selection** also skips an all-foreign leading clause, not just
|
||
foreign quantities inside a change clause. 0014's distractor sits in a change
|
||
clause, so it falls out of 3b directly. The implementation should treat anchor-skip
|
||
as a distinct, separately-tested step: **0014 is the guaranteed win (wrong 5→4); 0016
|
||
lands only when anchor-skip is built and validated (wrong →3).** Claiming both
|
||
before 0016's anchor-skip is proven would be the kind of overclaim the lookback
|
||
discipline exists to catch.
|
||
|
||
## 4. wrong=0 analysis (the obligations this spec must discharge before code)
|
||
|
||
1. **Commit path unchanged.** A derivation may resolve only under *full*
|
||
completeness. The isolated-foreign exemption is strictly commit-ineligible.
|
||
*Obligation:* a failing-under-violation test — flip the exemption to
|
||
commit-eligible and a known multi-step-incomplete case must commit a wrong
|
||
answer.
|
||
2. **Exemption is narrow.** "Isolated foreign" = unit non-empty ∧ not equal to any
|
||
*used* operand's unit in the reading. A quantity sharing a unit with any used
|
||
operand is **never** exempt (it is real signal); an empty-unit quantity is never
|
||
exempt (it cannot be shown foreign). *Obligation:* a test where a same-unit
|
||
"distractor" is *not* exempted and the reading stays complete-or-refused.
|
||
3. **Pooling cannot manufacture a new sole committer.** Pooling adds candidates;
|
||
the hazard is that a newly-pooled *complete* reading becomes a unique committer
|
||
on a case that previously refused. *Obligation:* re-run the full `train_sample`
|
||
and practice lanes; `wrong` must stay 0 and `correct` must not drop. Any new
|
||
commit must be inspected and gold-confirmed.
|
||
4. **Disagreement, not preference.** When complete and exempt readings disagree,
|
||
the result is refusal — never "prefer the additive reading." The engine does not
|
||
*know* 25 is right; it knows it has two incompatible readings.
|
||
|
||
## 5. Validation plan (the lanes that prove it)
|
||
|
||
- **Confuser probe:** `wrong` 5 → **4** (0014 refuses) from 3b alone; → **3** once
|
||
anchor-skip lands 0016. pseudo-accumulation stays at 0 wrong (EX-6); genuine
|
||
positives still solve; the pair-tell on 0014 (currently a tell) clears.
|
||
- **`train_sample` (capability):** `3/47/0` byte-identical — no new commit, no drop.
|
||
- **practice accumulation/search:** `wrong = 0` held; `correct` not reduced.
|
||
- **smoke + lane-SHA freeze:** green; serving untouched (sealed lane only).
|
||
- **Tightened baseline:** `_BASELINE_WRONG` 5 → 3 in
|
||
`tests/test_adr_0163_f2_confusers.py`, plus a `test_distractor_quantity_refuses`
|
||
obligation that fails loudly if pooling or the exemption regresses.
|
||
|
||
## 6. Scope boundaries (what this is *not*)
|
||
|
||
- **Not** a cue-precision fix. It does not learn which cues license multiplication;
|
||
it sidesteps that by refusing the ambiguity. CP-2b
|
||
([ADR-0177](./ADR-0177-cue-precision-learning.md)) remains the path to *solving*
|
||
(rather than refusing) distractor cases later.
|
||
- **Not** a completeness weakening for commits. Commits still require full
|
||
completeness; only the *refusal* surface widens.
|
||
- **Not** a general distractor *detector*. "Isolated foreign unit" is a deliberately
|
||
narrow, conservative structural signal; many distractors (same-unit ones) are out
|
||
of scope and must continue to refuse via other means.
|
||
- **Sealed.** `chat/` does not import the derivation composers; serving `3/47/0`
|
||
cannot move. This is practice/confuser-lane capability only, until a Phase-5
|
||
ratification.
|
||
|
||
## 7. Open questions for review
|
||
|
||
1. **Where does pooling live?** A new `generate/derivation/pool.py` that owns the
|
||
union + single `select_self_verified` call, with the confuser runner and practice
|
||
runners delegating to it (so the "first non-`None`" ordering disappears and the
|
||
two lanes cannot drift) — vs. threading a flag through each composer. The former
|
||
is cleaner and removes the implicit composer-priority ordering; preferred.
|
||
2. **No asked-for-unit signal exists yet.** Verified during scoping: MS-1
|
||
`extract_target` exposes `Target.units` as the **union of body unit-shapes**
|
||
(`('pencils','hours')` for 0014, `('miles','hours','tickets')` for 0016) — *not*
|
||
the unit the question asks for. Isolating the asked-for unit ("pencils" from "How
|
||
many pencils") would need a question-head-noun parse that does not exist (and is
|
||
out of scope here). The exemption is therefore defined **relative to a reading's
|
||
used-operand units** (always available), not an asked unit. If a question-unit
|
||
parse later lands, it becomes an *additional* tightening, never a prerequisite.
|
||
3. **Interaction with the deferred slash-fraction leak**
|
||
([ADR-0179 EX hazard pin](./ADR-0179-extraction-richness.md)). A fraction-operand
|
||
PR will change which quantities exist; sequence pooling and fraction support so
|
||
their completeness interactions are validated together, not pairwise-blind.
|
||
|
||
## 8. Realized results (2026-05-29)
|
||
|
||
Implemented across three PRs on the sealed lane; serving `train_sample` `3/47/0` and
|
||
the practice lane stayed **byte-identical throughout** (the pool is reached only by
|
||
the confuser runner — `chat/`, serving, and practice call `compose_accumulation`/the
|
||
serving scorer, never `accumulation_candidates`). The four-PR stack (with EX-6 #473)
|
||
got a lookback review whose findings are folded in below.
|
||
|
||
### Confuser `wrong`: spec predicted `5 → 4 → 3`; actual `5 → 2 → 1 → 0`
|
||
|
||
| PR | mechanism | predicted | **actual** |
|
||
|---|---|---|---|
|
||
| #476 | cross-composer pooling + commit-ineligible exemption | 0014 only (`5→4`) | **`5→2`** — 0014 **and** both disguised-polarity (0001/0003) refused |
|
||
| #480 | prior-state question guard (`asks_prior_state`) | *not in this spec* | **`2→1`** — temporal-scope 0020 refuses |
|
||
| #481 | anchor-skip **+ intra-clause** accumulation | 0016 (`→3`) | **`1→0`** — 0016 refuses **and** clean twin 0017 *solves* 12 |
|
||
|
||
`8 solved / 21 refused / 0 wrong / 1 spurious`; pair-tells `4→0`.
|
||
|
||
### Where reality diverged from the spec (honest drift)
|
||
|
||
- **Pooling exceeded its predicted scope.** The disagreement rule also caught
|
||
**disguised-polarity** (0001/0003): the spurious `buys X for N coins` product
|
||
disagrees with the accumulation reading → refuse. The spec scoped pooling to
|
||
distractor-quantity only.
|
||
- **0016 needed more than anchor-skip.** §3b framed 0016 as "anchor selection skips
|
||
an all-foreign leading clause." In fact its Tom sentence packs **state + change in
|
||
one sentence** ("Tom has 8 tickets **and** buys 4 more tickets"), so it also needed
|
||
**intra-clause accumulation** (a conjunction-level split, *local* to
|
||
`accumulation_candidates` — the global GB-1 segmenter is untouched). This was
|
||
validated as a **real** pattern (`train-0010` "had 20 … then lost 12";
|
||
`practice-0121`), not a 0016-only contrivance — and it produced the bonus 0017
|
||
*solve*, the comprehension-vs-surface-match discrimination the corpus exists to test.
|
||
- **The prior-state guard (#480) is a distinct lever**, not the pooling mechanism —
|
||
question-time reading rather than candidate disagreement. Recorded here because it
|
||
landed in the same arc and shares the `resolve_pooled` entry point.
|
||
|
||
### Lookback findings (four-PR stack, folded in)
|
||
|
||
- **Solid:** serving frozen by construction + empirically; commit-ineligibility
|
||
proven by `test_exempt_only_never_commits`; clean cross-PR composition;
|
||
deterministic; `+712/−31` LOC honest (additive substrate).
|
||
- **Gap (fixed):** the anchor-skip refuse branches (referent guard, polarity-cue
|
||
requirement) were untested — *asserted but not proven*. Closed with non-vacuous
|
||
failing-under-violation tests (#481, `4205605`).
|
||
- **No live wrong=0 hazard:** the multi-actor + distractor + product-cue shape
|
||
commits the blunt product, but the **prior path committed it identically** —
|
||
anchor-skip declines to fabricate a cross-actor reading rather than widening the
|
||
surface. Dormant (no corpus case), confined to the sealed pool.
|
||
|
||
### Still open
|
||
|
||
- **`0010` (multi-referent, the lone spurious):** "Maria has 12 books. Sara has 8
|
||
books. How many altogether?" → answers 20, should refuse (H1). A graduation/refusal
|
||
question per [ADR-0163-F2 §2.1](./ADR-0163-F2-confuser-corpus-spec.md), not a wrong.
|
||
- **CP-2b (cue precision)** remains the path to *solving* (rather than refusing) the
|
||
disguised-polarity / `for`-overloaded cases later.
|