feat(r4): goal-residual production — ADR-0207 §5 step 2 (sealed lane)

First composition lift-target built end-to-end: cv-0005 (train_sample 0037)
now resolves in the sealed pool as goal - Σprogress = 10 - 3 - 4 = 3.

- generate/derivation/goal_residual.py: new R4 production. Reads a GOAL anchor
  (goal-intent lexeme) + a residual question, subtracts each same-referent
  progress quantity (progress reduces the residual regardless of world-polarity).
  Gated by the unchanged self-verification gate.
- wrong=0 firewall (test_reads_goal_not_possession): on a gain goal the
  goal-residual (20-5-6=9) DIVERGES from possession-accumulation (20+5+6=31);
  the production gives 9 and is all-subtract -> it reads the goal, not the
  possession. This is the coincidental-correctness trap cv-0005 alone hides
  (10-3-4 == 10-(3+4)).
- pool.py: goal_residual added to pooled_candidates (sealed). Verified: fires on
  exactly one train_sample case (0037, correct), zero new pool wrong-commits
  (the 8 are pre-existing, gated off serving by product_bridge).

Does NOT move the serving metric: train_sample stays 6/44/0 byte-identical
(serving = candidate-graph; product_bridge promotes only pure products, never a
subtract chain). The serving promotion gate for goal-residual is the next,
separately-gated step (needs the sealed 1,319 verdict). Smoke 73, math 4,
derivation/pool/practice 196, corpus, completeness-guard all green.
This commit is contained in:
Shay 2026-06-03 21:50:59 -07:00
parent f7e95be211
commit 94d8137ad1
3 changed files with 187 additions and 0 deletions

View file

@ -0,0 +1,111 @@
"""ADR-0207 §5 step 2 / R4 — goal-residual reading (single-referent).
The first **residual-to-target** comprehension reading, distinct from GB-3b.1
accumulation (:mod:`generate.derivation.accumulate`):
- accumulation reads a **possession** that changes ``start ± changes`` (``Sam has
14 apples. He buys 9 more.`` -> ``14 + 9``);
- this reads a **goal** and the **progress** toward it ``goal Σprogress``
(``Michael wants to lose 10 pounds. He lost 3, then 4. How much more to meet his
goal?`` -> ``10 3 4``).
**Critical wrong=0 distinction the coincidental-correctness trap.** Goal-residual is
*not* possession-accumulation. For a **loss** goal the two arithmetics coincide
(``10 3 4`` == ``10 (3+4)``), so a naive "make accumulation fire" change would
pass ``cv-0005`` for the wrong reason reading the goal ``10`` as a possession start.
For a **gain/save** goal they **diverge**: ``wants to save 20, saved 5, saved 6`` ->
residual ``20 5 6 = 9``, whereas possession-accumulation gives ``20 + 5 + 6 = 31``.
This production fires **only** on goal-language and **always subtracts** progress
(progress reduces the residual regardless of its world-polarity), so it reads the goal,
never the possession. The gain-goal divergence is the wrong=0 firewall test.
Lexeme-level (ADR-0165): goal-intent and residual-question cues are closed token sets,
not sentence-shape grammar. The constructed chain runs through the unchanged
self-verification gate (grounding unit completeness uniqueness) refuse-
preferring; this only proposes a structurally-licensed candidate.
Sealed (no ``chat/`` import); deterministic.
"""
from __future__ import annotations
from typing import Final
from generate.derivation.clauses import segment_clauses
from generate.derivation.extract import extract_quantities
from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.state.bind import continues_anchor_referent, leading_subject_token
from generate.derivation.state.change import classify_change_polarity, select_change_cue
from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, select_self_verified
from generate.math_roundtrip import _tokens
# Goal-intent lexemes: the anchor quantity is a *target*, not a possession. Closed set.
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
)
# Residual-question lexemes: the question asks the remaining distance to the goal.
_RESIDUAL_CUES: Final[frozenset[str]] = frozenset({"more", "left", "remaining"})
_GOAL_REACH: Final[frozenset[str]] = frozenset({"meet", "reach", "hit", "achieve"})
def _asks_residual(question_clause: str) -> bool:
"""The question asks the remaining distance to a goal (lexeme-level)."""
q = _tokens(question_clause)
return bool(_RESIDUAL_CUES & q) or ("goal" in q and bool(_GOAL_REACH & q))
def build_goal_residual(problem_text: str) -> GroundedDerivation | None:
"""Construct the ungated goal-residual chain ``goal Σprogress``, or ``None``.
Fires only when: (a) the question asks a residual-to-goal; (b) the first
quantity-clause carries a goal-intent lexeme and establishes exactly one goal
quantity; (c) every later quantity-clause stays on the same referent and carries a
licensed change cue (its quantities are *progress*). Progress is **subtracted**
(it reduces the residual) regardless of the change's world-polarity — this is what
makes the reading goal-residual, not possession-accumulation.
"""
if not _asks_residual(_question_clause(problem_text)):
return None
clauses = segment_clauses(problem_text)
quantity_clauses = [c for c in clauses if extract_quantities(c)]
if len(quantity_clauses) < 2:
return None
anchor_clause, *progress_clauses = quantity_clauses
if not (_GOAL_INTENT & _tokens(anchor_clause)):
return None # the anchor must be goal-language, not a possession
anchor_quantities = extract_quantities(anchor_clause)
if len(anchor_quantities) != 1:
return None # exactly one goal quantity
goal = anchor_quantities[0]
anchor_subject = leading_subject_token(anchor_clause)
steps: list[Step] = []
for clause in progress_clauses:
if not continues_anchor_referent(clause, anchor_subject):
return None # new named actor -> referent hazard -> refuse
polarity = classify_change_polarity(clause)
if polarity is None:
return None # progress must carry a licensed change cue
cue = select_change_cue(clause, polarity)
progress = [q for q in extract_quantities(clause) if (not q.unit) or q.unit == goal.unit]
if not progress:
return None # no same-unit progress quantity -> refuse
for q in progress:
operand = Quantity(value=q.value, unit=goal.unit, source_token=q.source_token)
steps.append(Step(op="subtract", operand=operand, cue=cue))
if not steps:
return None
return GroundedDerivation(start=goal, steps=tuple(steps))
def compose_goal_residual(problem_text: str) -> Resolution | None:
"""R4 goal-residual composer. Refuse-preferring: gates the chain through the
unchanged self-verification gate (grounding unit completeness uniqueness)."""
derivation = build_goal_residual(problem_text)
if derivation is None:
return None
return select_self_verified([derivation], problem_text, target_units=())

View file

@ -30,6 +30,7 @@ never invoked here).
from __future__ import annotations
from generate.derivation.accumulate import accumulation_candidates
from generate.derivation.goal_residual import build_goal_residual
from generate.derivation.model import GroundedDerivation
from generate.derivation.multistep import candidate_chains
from generate.derivation.search import multiplicative_candidates
@ -42,10 +43,12 @@ def pooled_candidates(problem_text: str) -> list[GroundedDerivation]:
order (accumulation, then multiplicative, then chain)."""
seen: set[tuple[object, ...]] = set()
pooled: list[GroundedDerivation] = []
_goal_residual = build_goal_residual(problem_text)
for derivation in (
*accumulation_candidates(problem_text),
*multiplicative_candidates(problem_text),
*candidate_chains(problem_text),
*((_goal_residual,) if _goal_residual is not None else ()),
):
key = (
round(derivation.answer, 9),

View file

@ -0,0 +1,73 @@
"""ADR-0207 §5 step 2 / R4 — goal-residual production + its wrong=0 firewall.
The corpus lift-target cv-0005 (train_sample 0037) must read `goal Σprogress`.
The load-bearing test is the **gain-goal divergence** (`test_reads_goal_not_possession`):
it proves the production reads the goal, not the possession, on a case where the two
arithmetics differ the coincidental-correctness trap cv-0005 alone cannot catch.
"""
from __future__ import annotations
from generate.derivation.goal_residual import build_goal_residual, compose_goal_residual
# cv-0005 / train_sample 0037 (loss goal — residual and possession coincide at 3).
CV0005 = (
"Michael wants to lose 10 pounds by June. He lost 3 pounds in March and 4 pounds "
"in April. How much weight does he have to lose in May to meet his goal?"
)
# Adversarial gain goal — residual (20-5-6=9) DIVERGES from possession (20+5+6=31).
# Uses a recognized gain verb ("earned") so the progress cue is licensed.
SAVE_GOAL = (
"Maria wants to earn 20 dollars for a gift. She earned 5 dollars in May and 6 dollars "
"in June. How much more does she need to earn to reach her goal?"
)
def _answer(text):
res = compose_goal_residual(text)
return None if res is None else res.answer
def test_cv0005_goal_residual_solves() -> None:
"""cv-0005: goal 10 (3 + 4) = 3, read as a goal (start=10, all-subtract)."""
deriv = build_goal_residual(CV0005)
assert deriv is not None
assert deriv.start.value == 10.0
assert all(s.op == "subtract" for s in deriv.steps)
assert _answer(CV0005) == 3.0
def test_reads_goal_not_possession() -> None:
"""WRONG=0 FIREWALL. On a gain goal, goal-residual (9) diverges from possession-
accumulation (31). The production must give 9 (reads the goal) never 31, and the
chain must be all-subtract (progress reduces the residual regardless of polarity)."""
deriv = build_goal_residual(SAVE_GOAL)
assert deriv is not None
assert _answer(SAVE_GOAL) == 9.0, "must read goal-residual, not possession 31"
assert all(s.op == "subtract" for s in deriv.steps), "progress always subtracts"
assert deriv.start.value == 20.0
def test_no_goal_language_does_not_fire() -> None:
"""A possession case (no goal-intent lexeme) must NOT fire this production —
it belongs to accumulation, not goal-residual."""
possession = "Sam has 14 apples. He gives away 3 apples and 2 apples. How many are left?"
assert build_goal_residual(possession) is None
def test_no_residual_question_does_not_fire() -> None:
"""Goal language but no residual question → does not fire."""
no_residual = (
"Michael wants to lose 10 pounds by June. He lost 3 pounds in March and 4 pounds "
"in April. How much weight did he lose in total?"
)
assert build_goal_residual(no_residual) is None
def test_incomplete_reading_refuses() -> None:
"""A progress clause with a new named actor (referent hazard) must refuse — the
same-referent guard is inherited, never weakened."""
cross_referent = (
"Michael wants to lose 10 pounds by June. Sarah lost 3 pounds in March. "
"How much more does Michael need to lose to meet his goal?"
)
assert build_goal_residual(cross_referent) is None