The mandated lookback review before GB-3 (CLAUDE.md §Lookback Review Discipline) confirmed the audit's hazards H1/H2/H3 were LIVE: compose_sequential summed same-unit quantities from the whole problem, merging unrelated referents/scopes and admitting wrong structures whose value happened to ground: H1 (second actor's apples) -> 6+4+2 = 12 H2 (comparative on other actor) -> (6+4)*2 = 20 H3 (later depletion event) -> 6+4+3 = 13 Root cause is the audit's G1/D2 drift: GB-2a re-extracts from the whole text and ignores GB-1's clause structure. The fix is the GB-3 increment — make the composer clause-scoped (consume segment_clauses), refusing when the licensed structure spans clauses, because this slice cannot model referents: - quantities must live in exactly ONE clause (0 or >1 -> refuse); - a comparative outside that clause -> refuse (unmodelled referent binding). All three hazards now refuse; all 7 GB-2 single-clause structures preserved (list-sum, three-item, sum-then-scale, and the mixed-unit/disagreement/too-few refusals). tests/test_adr_0178_gb3_referent_guard.py would fail against the pre-guard code (12/20/13), so the obligation is proven, not decorative. Scope/safety: - compose_sequential is sealed substrate, not wired to a scorer -> serving byte-identical 3/47/0 (lane-SHA 8/8, generate_claims --check OK); practice unchanged 4/1/45. No new test failures (2 pre-existing on main). - ADR-0178 amended: GB-2 relabelled GB-2a (list slice, drift G1 recorded); GB-3 split into GB-3a (this referent guard, landed) and GB-3b (constructive cross-clause chaining, next).
108 lines
5.4 KiB
Python
108 lines
5.4 KiB
Python
"""ADR-0178 GB-2 — sequential composition: list-structure + comparative-scale.
|
||
|
||
GB-1 read the problem into clauses; GB-2 begins combining structure the blunt MS-3
|
||
shapes could not reach. The first increment adds the **same-unit-list → sum** shape
|
||
(like quantities joined by an additive cue sum) and **always applies trailing
|
||
comparative scalars** (×N / half / doubled) — the `sum-then-scale` family
|
||
(0024-class: `(6+4)×2`). The op for each step comes from the text's structure (list
|
||
⇒ add; comparative ⇒ scale), not a single blunt op.
|
||
|
||
All operands are text quantities (grounded) + comparative steps (cue-grounded), so
|
||
no derived-intermediate model is needed — the running value is the intermediate.
|
||
A stated comparative is part of the problem, so it is always applied (no
|
||
bare-vs-scaled alternative, which would self-disagree). Each licensed base shape
|
||
(list-sum, product) is one candidate; routed through the proven gate (grounding ∧
|
||
cue ∧ unit ∧ completeness ∧ uniqueness). When two bases self-verify and disagree
|
||
(e.g. a same-unit list that also has a multiplicative cue), uniqueness refuses —
|
||
cue precision (ADR-0177) is what later breaks such ties. Refuse-preferring; sealed.
|
||
|
||
Branch/DAG structures (0033's `25−12`) and richer relational ops (per/each, more/
|
||
older) are later GB increments.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Final
|
||
|
||
from generate.derivation.clauses import segment_clauses
|
||
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.multistep import MAX_QUANTITIES
|
||
from generate.derivation.search import MULTIPLICATIVE_CUES
|
||
from generate.derivation.verify import Resolution, select_self_verified
|
||
from generate.math_roundtrip import _tokens
|
||
|
||
# Additive cues that license summing a same-unit list (lexeme-level, ADR-0165).
|
||
_ADDITIVE_CUES: Final[tuple[str, ...]] = ("and", "plus", "altogether", "total")
|
||
|
||
|
||
def _same_unit(quantities: list[Quantity]) -> bool:
|
||
return len({q.unit for q in quantities}) == 1
|
||
|
||
|
||
def compose_sequential(problem_text: str) -> Resolution | None:
|
||
"""GB-2/GB-3 composer — the **clause-local** same-unit list-sum-then-scale.
|
||
|
||
Scope (deliberately narrow): only same-unit quantity *lists*. The list sums
|
||
(additive cue) and any stated comparative scales the sum (sum-then-scale). A
|
||
product base over the same list is added *without* a comparative tail purely as
|
||
a **disagreement-safety** candidate — so a same-unit list that also carries a
|
||
multiplicative cue (ambiguous: sum vs product) refuses rather than guessing.
|
||
|
||
Product-of-all / cross-unit products are **not** this composer's job (that is
|
||
MS-3 ``search_chain``); a non-same-unit problem yields no candidate here and
|
||
refuses. This keeps the composer to the one structure it adds and avoids the
|
||
product×comparative blowups a blunt all-bases composer produced.
|
||
|
||
**GB-3 referent guard (wrong=0-first).** The list-sum structure must be
|
||
licensed *within a single clause*. The earlier whole-problem version summed any
|
||
same-unit quantities anywhere in the text, which silently merged unrelated
|
||
referents/scopes (a later sentence's quantity, a second actor's total, a
|
||
depletion event) into one sum — admitting wrong structures whose value happened
|
||
to ground (audit ADR-0178 hazards H1/H2/H3). This composer cannot model
|
||
referents, so when the licensed structure would span clauses it **refuses**
|
||
(cross-clause, referent-aware chaining is GB-3b):
|
||
|
||
* quantities must live in exactly **one** clause (segment_clauses); 0 or >1
|
||
quantity-bearing clauses ⇒ refuse;
|
||
* any comparative scalar **outside** that clause ⇒ refuse (it binds to a
|
||
referent/structure this slice does not model).
|
||
|
||
Refuse-preferring; deterministic; sealed.
|
||
"""
|
||
# GB-3: the structure must be licensed within a single clause (referent guard).
|
||
quantity_clauses = [c for c in segment_clauses(problem_text) if extract_quantities(c)]
|
||
if len(quantity_clauses) != 1:
|
||
return None
|
||
clause = quantity_clauses[0]
|
||
# A comparative living outside the list clause binds an unmodelled referent.
|
||
if len(extract_comparative_scalars(problem_text)) != len(extract_comparative_scalars(clause)):
|
||
return None
|
||
|
||
quantities = list(extract_quantities(clause))
|
||
if not 2 <= len(quantities) <= MAX_QUANTITIES or not _same_unit(quantities):
|
||
return None
|
||
|
||
tokens = _tokens(clause)
|
||
tail = tuple(comparative_step(cs) for cs in extract_comparative_scalars(clause))
|
||
start, *rest = quantities
|
||
|
||
candidates: list[GroundedDerivation] = []
|
||
add_cue = next((c for c in _ADDITIVE_CUES if c in tokens), None)
|
||
if add_cue is not None: # list-sum (+ applied comparative scale)
|
||
candidates.append(
|
||
GroundedDerivation(
|
||
start=start,
|
||
steps=tuple(Step(op="add", operand=q, cue=add_cue) for q in rest) + tail,
|
||
)
|
||
)
|
||
mult_cue = next((c for c in MULTIPLICATIVE_CUES if c in tokens), None)
|
||
if mult_cue is not None: # product (no tail) — disagreement-safety only
|
||
candidates.append(
|
||
GroundedDerivation(
|
||
start=start,
|
||
steps=tuple(Step(op="multiply", operand=q, cue=mult_cue) for q in rest),
|
||
)
|
||
)
|
||
return select_self_verified(candidates, problem_text, target_units=())
|