Merge pull request #436 from AssetOverflow/feat/adr-0175-selfverify-completeness
ADR-0175: strengthen self-verification with a completeness clause (practice wrongs 9→2)
This commit is contained in:
commit
37cbdeee3f
2 changed files with 38 additions and 6 deletions
|
|
@ -30,6 +30,8 @@ from typing import Final
|
||||||
# Canonical grounding primitives — reused so this gate stays identical to the
|
# Canonical grounding primitives — reused so this gate stays identical to the
|
||||||
# round-trip filter's notion of "appears in the problem text".
|
# round-trip filter's notion of "appears in the problem text".
|
||||||
from generate.math_roundtrip import _token_in, _tokens, _value_grounds
|
from generate.math_roundtrip import _token_in, _tokens, _value_grounds
|
||||||
|
from collections import Counter
|
||||||
|
from generate.derivation.extract import extract_quantities
|
||||||
from generate.derivation.model import GroundedDerivation
|
from generate.derivation.model import GroundedDerivation
|
||||||
|
|
||||||
_SAME_UNIT_REQUIRED: Final[frozenset[str]] = frozenset({"add", "subtract"})
|
_SAME_UNIT_REQUIRED: Final[frozenset[str]] = frozenset({"add", "subtract"})
|
||||||
|
|
@ -77,6 +79,20 @@ def self_verifies(derivation: GroundedDerivation, problem_text: str) -> SelfVeri
|
||||||
if step.op == "divide" and step.operand.value == 0:
|
if step.op == "divide" and step.operand.value == 0:
|
||||||
reasons.append("division by zero")
|
reasons.append("division by zero")
|
||||||
|
|
||||||
|
# 5. completeness — a trustworthy derivation must account for every quantity
|
||||||
|
# the problem states. A derivation that ignores given numbers is an
|
||||||
|
# incomplete reading (typically a correct *first step* of a multi-step
|
||||||
|
# problem, mistaken for the whole answer). Refuse-preferring: unused
|
||||||
|
# quantities -> not self-verified. This is the clause the practice-lane
|
||||||
|
# microscope identified (ADR-0175 self-verification strengthening): it
|
||||||
|
# catches the multi-step-incomplete attempts the cue/grounding clauses
|
||||||
|
# cannot, because their operands ARE grounded.
|
||||||
|
problem_quantities = Counter(q.source_token for q in extract_quantities(problem_text))
|
||||||
|
used = Counter([derivation.start.source_token] + [step.operand.source_token for step in derivation.steps])
|
||||||
|
unused = problem_quantities - used
|
||||||
|
if unused:
|
||||||
|
reasons.append(f"incomplete: unused problem quantities {sorted(unused.keys())}")
|
||||||
|
|
||||||
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,15 +162,31 @@ class TestSelectUnique:
|
||||||
assert select_self_verified([spurious], "Martha has 20 apples and 5 friends.") is None
|
assert select_self_verified([spurious], "Martha has 20 apples and 5 friends.") is None
|
||||||
|
|
||||||
def test_disagreeing_self_verified_refuses(self) -> None:
|
def test_disagreeing_self_verified_refuses(self) -> None:
|
||||||
# two grounded derivations that disagree on the answer -> refuse (wrong=0)
|
# two COMPLETE grounded derivations that disagree -> refuse (wrong=0).
|
||||||
|
# Both use all problem quantities {5, 3}; different ops -> different answers.
|
||||||
|
text = "She has 5 apples and 3 apples."
|
||||||
|
d_add = GroundedDerivation(
|
||||||
|
start=_q(5, "apples", "5"),
|
||||||
|
steps=(Step(op="add", operand=_q(3, "apples", "3"), cue="and"),),
|
||||||
|
) # 8
|
||||||
|
d_mul = GroundedDerivation(
|
||||||
|
start=_q(5, "apples", "5"),
|
||||||
|
steps=(Step(op="multiply", operand=_q(3, "apples", "3"), cue="and"),),
|
||||||
|
) # 15
|
||||||
|
assert d_add.answer != d_mul.answer
|
||||||
|
assert select_self_verified([d_add, d_mul], text) is None
|
||||||
|
|
||||||
|
def test_incomplete_derivation_refuses(self) -> None:
|
||||||
|
# completeness clause: a derivation that ignores a stated quantity is an
|
||||||
|
# incomplete reading -> not self-verified (catches multi-step first-steps).
|
||||||
text = "He bench presses 15 pounds for 10 reps and does 3 sets."
|
text = "He bench presses 15 pounds for 10 reps and does 3 sets."
|
||||||
d1 = _mult_0021() # 450
|
partial = GroundedDerivation( # uses 15,10 but ignores the stated 3
|
||||||
d2 = GroundedDerivation( # 15 x 10 = 150 (grounded but different answer)
|
|
||||||
start=_q(15, "pounds", "15"),
|
start=_q(15, "pounds", "15"),
|
||||||
steps=(Step(op="multiply", operand=_q(10, "reps", "10"), cue="reps"),),
|
steps=(Step(op="multiply", operand=_q(10, "reps", "10"), cue="for"),),
|
||||||
)
|
)
|
||||||
assert d1.answer != d2.answer
|
sv = self_verifies(partial, text)
|
||||||
assert select_self_verified([d1, d2], text) is None
|
assert sv.verified is False
|
||||||
|
assert any("incomplete" in r for r in sv.reasons)
|
||||||
|
|
||||||
def test_agreeing_self_verified_resolves(self) -> None:
|
def test_agreeing_self_verified_resolves(self) -> None:
|
||||||
# two self-verifying derivations that AGREE -> resolve (convergent evidence)
|
# two self-verifying derivations that AGREE -> resolve (convergent evidence)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue