"""ADR-0182 — cross-composer disagreement pooling. The distractor-quantity confusers (``confuser-v1-0014`` → 300, ``0016`` → 3840) misfire because a blunt product-of-all is the *unique* self-verifying reading: the completeness clause forces the distractor into it, and no rival reading exists to trigger the wrong=0 disagreement rule. The microscope showed no tight cue/gate rule separates these from the legitimate cross-unit products (``for`` licenses both the 0014 distractor and the correct ``train-0021`` product) — that is the deferred cue-precision problem (ADR-0177). This module sidesteps cue precision: it pools the **ungated candidate readings of every composer** (accumulation, in-clause multiplicative, target-guided chain) for one problem and resolves them together. A reading is classified (:func:`generate.derivation.verify.classify_derivation`) as ``complete`` (commit-eligible) or ``exempt`` (commit-INELIGIBLE — verified but for an isolated-foreign *distractor* quantity it leaves unused). The rule: * candidates disagree (more than one distinct answer across the pool) → **refuse** (the wrong=0 disagreement rule does the work); * a single distinct answer → commit **only if** a ``complete`` candidate produced it (an ``exempt``-only answer never commits — full completeness is still required to resolve, so ADR-0175's multi-step-incomplete defence is untouched). For a distractor problem this gives the product (``complete``) a competing additive reading (``exempt``) to disagree with → refusal; for a genuine product (0021/0003) there is no rival reading, so it still commits. Deterministic; sealed (serving is never invoked here). """ from __future__ import annotations from generate.derivation.accumulate import accumulation_candidates from generate.derivation.model import GroundedDerivation from generate.derivation.multistep import candidate_chains from generate.derivation.search import multiplicative_candidates from generate.derivation.target import asks_prior_state from generate.derivation.verify import Resolution, classify_derivation def pooled_candidates(problem_text: str) -> list[GroundedDerivation]: """Every composer's ungated candidate readings, de-duplicated. Deterministic order (accumulation, then multiplicative, then chain).""" seen: set[tuple[object, ...]] = set() pooled: list[GroundedDerivation] = [] for derivation in ( *accumulation_candidates(problem_text), *multiplicative_candidates(problem_text), *candidate_chains(problem_text), ): key = ( round(derivation.answer, 9), derivation.start.source_token, tuple((step.op, step.operand.source_token) for step in derivation.steps), ) if key in seen: continue seen.add(key) pooled.append(derivation) return pooled def resolve_pooled(problem_text: str) -> Resolution | None: """Resolve a problem by pooling every composer's readings. Refuse-preferring. Refuses on no verifying candidate, on disagreement among the pool, or when the sole answer is produced only by commit-ineligible (``exempt``) readings. ADR-0182 question-scope guard: a question asking for a *prior* state ("how much did Lisa have **before** lunch?") asks for a temporal point the forward composers do not compute — they derive the final/net state. Until a question-time reader exists, that is a refusal, never a guess at the wrong point. """ if asks_prior_state(problem_text): return None classified = [ (kind, derivation) for derivation in pooled_candidates(problem_text) if (kind := classify_derivation(derivation, problem_text)) is not None ] if not classified: return None distinct = {round(derivation.answer, 9) for _, derivation in classified} if len(distinct) != 1: return None # disagreement -> refuse (wrong=0) committable = [d for kind, d in classified if kind == "complete"] if not committable: return None # exempt-only -> refuse (a commit requires full completeness) chosen = committable[0] return Resolution( answer=chosen.answer, answer_unit=chosen.answer_unit, derivation=chosen, )