core/docs/decisions/ADR-0182-cross-composer-disagreement-pooling.md
Shay 6ed8fc21fc
docs(adr-0182): realized results + status Accepted/Implemented (lookback amendment) (#483)
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.
2026-05-29 14:50:22 -07:00

16 KiB
Raw Blame History

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 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 (the confuser probe), ADR-0175 (the self-verification gate), and ADR-0177 (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 (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 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 12requires 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) 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). 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, not a wrong.
  • CP-2b (cue precision) remains the path to solving (rather than refusing) the disguised-polarity / for-overloaded cases later.