CP-2a populates the CP-1 ledger from gold-labelled candidate readings and reports per-pattern reliability — the measurement the cue-precision thesis rests on. Plus the function-word unit filter, whose value this measurement makes concrete (clean unit_shape labelling). What landed (all sealed; serving 3/47/0 byte-identical): - generate/cue_precision/trainer.py — train_from_cases(cases, enumerators): folds gold-labelled candidate chains into the ledger via record_case. Decoupled (the candidate enumerators are injected, so the package still imports nothing from search). candidates_for dedupes a reading shared by two enumerators. - generate/derivation/multistep.py — extracted the enumeration half of search_chain into public candidate_chains(problem_text); search_chain now delegates (verified byte-identical: ms3 tests + practice counts unchanged). CP-2 needs the readings the search weighs, not just the one it resolves. - generate/derivation/extract.py — function-word unit filter (_NON_UNIT_WORDS): blanks spurious function-word units ($0.75 each -> "", 3/4 of -> "") that corrupt same-unit detection and unit_shape. Closed lexeme set, ADR-0165-safe. - evals/gsm8k_math/practice/v1/cue_precision_report.py — trains over 200 sealed cases (50 train_sample + 150 ADR-0163-F additive) with the real enumerators and prints the per-pattern reliability table. - tests/test_adr_0177_cp2a_training.py — trainer obligations (credit/dedupe/ determinism/empty) via synthetic enumerators; real-measurement well-formedness; search_chain parity. Load-bearing finding (recorded in ADR-0177): over 200 cases EVERY (cue,op,unit_shape) pattern floors at ~0.0 reliability (best: for-multiply-cross_unit 0.0116 at 2/34). The blunt product/sum-of-all readings are almost always wrong vs gold, so the conservative floor correctly trusts nothing. => CP-2b (trust reliable cues) is blocked on candidate GENERATION, not the ledger: candidate readings must get less crude (clause/referent structure, ADR-0178 GB-3b) before any cue earns reliability. Cue-precision and compositional structure are coupled; structure comes first. Verification: 107 targeted tests green (CP-2a/CP-1/extract/ms3/GB-1/2/3/MS-1/2) + architectural invariants; serving CLAIMS.md sha unchanged; practice 4/1/45 and 0/1/149 unchanged. Inert: trains/reports only, consulted by no search/gate.
103 lines
4.9 KiB
Python
103 lines
4.9 KiB
Python
"""ADR-0176 MS-3 — target-guided bounded multi-step search.
|
|
|
|
Composes everything: extract body+question quantities (MS-1 ``Target``),
|
|
comparative scalars (the pack), enumerate a small, principled set of candidate
|
|
**chains** that use *all* the quantities, and route them through the self-
|
|
verification gate (grounding ∧ cue ∧ unit ∧ completeness) + ``target_units`` +
|
|
uniqueness (MS-2 ``select_self_verified``).
|
|
|
|
Deliberately **shape-based, not blind enumeration**: it tries the arithmetic
|
|
shapes the gold structures actually use (product-of-all, sum-of-all, each
|
|
optionally followed by the comparative scalars), each licensed by a *present*
|
|
cue. This is bounded by construction (a handful of candidates) and deterministic.
|
|
|
|
Honest posture (wrong=0-first): when more than one shape self-verifies and they
|
|
disagree, the uniqueness rule **refuses** — broad cues cannot tell which op the
|
|
text licenses, and that ambiguity is a refusal, not a guess. Coverage is therefore
|
|
gated on cue *precision* (the ADR-0175 learning loop), exactly as the practice
|
|
eliminations will show. Refuse-preferring throughout; runs only in the sealed
|
|
practice lane.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Final
|
|
|
|
from generate.derivation.comparatives import comparative_step, extract_comparative_scalars
|
|
from generate.derivation.extract import extract_quantities
|
|
from generate.derivation.model import GroundedDerivation, Quantity, Step
|
|
from generate.derivation.search import MULTIPLICATIVE_CUES
|
|
from generate.derivation.target import Target, extract_target
|
|
from generate.derivation.verify import Resolution, select_self_verified
|
|
from generate.math_roundtrip import _tokens
|
|
|
|
MAX_QUANTITIES: Final[int] = 6
|
|
|
|
|
|
def _chain(quantities: list[Quantity], op: str, cue: str, tail: tuple[Step, ...]) -> GroundedDerivation:
|
|
"""Left-fold all quantities under ``op`` (cue ``cue``), then append ``tail``."""
|
|
start, *rest = quantities
|
|
steps = tuple(Step(op=op, operand=q, cue=cue) for q in rest) + tail
|
|
return GroundedDerivation(start=start, steps=steps)
|
|
|
|
|
|
def _candidate_chains(
|
|
quantities: list[Quantity], problem_text: str, target: Target
|
|
) -> list[GroundedDerivation]:
|
|
"""The small, principled candidate set. Bounded + deterministic."""
|
|
tokens = _tokens(problem_text)
|
|
comparative_tail = tuple(
|
|
comparative_step(cs) for cs in extract_comparative_scalars(problem_text)
|
|
)
|
|
|
|
# product-of-all is licensed by a present multiplicative cue;
|
|
# sum-of-all by a present aggregation hint (a strong, single-word sum signal).
|
|
mult_cue = next((c for c in MULTIPLICATIVE_CUES if c in tokens), None)
|
|
add_cue = target.aggregation if (target.aggregation in tokens) else None
|
|
|
|
candidates: list[GroundedDerivation] = []
|
|
for op, cue in (("multiply", mult_cue), ("add", add_cue)):
|
|
if cue is None:
|
|
continue
|
|
candidates.append(_chain(quantities, op, cue, ()))
|
|
if comparative_tail:
|
|
candidates.append(_chain(quantities, op, cue, comparative_tail))
|
|
return candidates
|
|
|
|
|
|
def candidate_chains(
|
|
problem_text: str, target: Target | None = None
|
|
) -> list[GroundedDerivation]:
|
|
"""The bounded, deterministic candidate readings :func:`search_chain` weighs.
|
|
|
|
The *enumeration* half of the search, with no gate applied: extract quantities,
|
|
refuse-on-overflow (``> MAX_QUANTITIES``) / too-few (``< 2``) by yielding no
|
|
candidates, derive the target, and build the principled set. Exposed for CP-2
|
|
ledger training (ADR-0177), which must see every reading the search considers —
|
|
not just the one it resolves to. ``search_chain`` delegates here, so the two
|
|
can never drift.
|
|
"""
|
|
quantities = list(extract_quantities(problem_text))
|
|
if not 2 <= len(quantities) <= MAX_QUANTITIES:
|
|
return [] # refuse-on-overflow / too few to compose
|
|
resolved: Target = target if target is not None else extract_target(
|
|
problem_text, known_units=tuple(q.unit for q in quantities)
|
|
)
|
|
return _candidate_chains(quantities, problem_text, resolved)
|
|
|
|
|
|
def search_chain(problem_text: str, target: Target | None = None) -> Resolution | None:
|
|
"""Target-guided bounded multi-step search over the problem's quantities.
|
|
|
|
Returns a :class:`Resolution` only when a single candidate chain self-verifies
|
|
(and, when a target unit is known, matches it); refuses on no candidate,
|
|
disagreement, or > :data:`MAX_QUANTITIES` quantities. Deterministic.
|
|
"""
|
|
# Target-UNIT matching is deferred: the model's answer_unit is the start
|
|
# quantity's unit, which is wrong for cross-unit products (6 boxes x 50 apples
|
|
# -> value 300 but unit "boxes"), so a unit gate would over-refuse correct
|
|
# answers. The Target still prunes via its aggregation cue + supplies the
|
|
# question quantities. Unit matching returns once a result-unit model exists.
|
|
return select_self_verified(
|
|
candidate_chains(problem_text, target), problem_text, target_units=()
|
|
)
|