MS-2 of multi-step composition. Extends the derivation model so a chain mixes text-quantity operands and COMPARATIVE-scalar operands (twice->x2, 'N times'->xN, half->x0.5), self-verifying the whole chain with completeness over body+question and question-target matching. - model.py: Step gains comparative flag. - comparatives.py: ComparativeScalar gains number_token (the '<N> times' number, so completeness counts the consumed body quantity); comparative_step(cs) bridges a scalar into a Step (operand grounded by cue, not a text value token). - verify.py: self_verifies exempts comparative operands from value-grounding (clause 1) — they are cue-grounded (clause 2); completeness (Counter) counts a digit comparative's number_token as consuming the body quantity. Adds target_units to select_self_verified: a chain whose answer_unit isn't the asked unit is dropped (question-target match; empty target_units imposes no constraint). Proves the multi-step shapes from the gold structures: 0024 (text sum then 'three times' scale -> 438), 0033 father-chain (digit-comparative '7 times' + fixed 'half' + text add -> 47). Full 0033 DAG (quantity reuse + the question's 25) deferred. 25 MS-2 tests; full derivation surface 69/69 (3a/3b/comparatives/ms1/ms2); ruff clean; smoke 67. Not wired into serving (model ready for MS-3 target-guided search).
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
"""ADR-0175 Phase 3a — grounded-derivation value model.
|
|
|
|
A derivation is a left-fold over text-sourced quantities: a ``start`` quantity
|
|
followed by ordered ``Step``s. Each step names the operation, its operand, and
|
|
the **licensing cue** — the surface lexeme the search claims licenses that
|
|
operation. The cue is verified against the problem text by the gate
|
|
(:mod:`generate.derivation.verify`); the model itself only computes the value.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
VALID_OPS: Final[frozenset[str]] = frozenset({"multiply", "divide", "add", "subtract"})
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Quantity:
|
|
"""A quantity drawn from the problem. ``source_token`` is the surface token
|
|
as it appears in the text (used by the gate to prove the value is grounded)."""
|
|
|
|
value: float
|
|
unit: str
|
|
source_token: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Step:
|
|
"""One operation applied to the running value.
|
|
|
|
``cue`` is the surface lexeme the search asserts licenses ``op`` here; the
|
|
gate refuses to self-verify unless ``cue`` actually appears in the text.
|
|
"""
|
|
|
|
op: str
|
|
operand: Quantity
|
|
cue: str
|
|
# ADR-0176 MS-2: when True the operand is a comparative scalar (twice -> x2,
|
|
# 'N times' -> xN). It is grounded by ``cue`` (the comparative lexeme), not by a
|
|
# text value token, and it does not count as a body quantity for completeness.
|
|
comparative: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.op not in VALID_OPS:
|
|
raise ValueError(f"op must be one of {sorted(VALID_OPS)}, got {self.op!r}")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GroundedDerivation:
|
|
start: Quantity
|
|
steps: tuple[Step, ...]
|
|
|
|
@property
|
|
def answer(self) -> float:
|
|
"""Left-fold the steps over ``start``. Raises on divide-by-zero (the gate
|
|
rejects such derivations before this is relied upon)."""
|
|
value = self.start.value
|
|
for step in self.steps:
|
|
operand = step.operand.value
|
|
if step.op == "multiply":
|
|
value = value * operand
|
|
elif step.op == "divide":
|
|
value = value / operand # ZeroDivisionError surfaces; gate guards
|
|
elif step.op == "add":
|
|
value = value + operand
|
|
else: # subtract
|
|
value = value - operand
|
|
return value
|
|
|
|
@property
|
|
def answer_unit(self) -> str:
|
|
"""The aggregate keeps the primary (``start``) unit. Multiply/divide
|
|
compose across units onto the primary; add/subtract require (and the gate
|
|
enforces) a shared unit, so the primary is correct in every admitted case."""
|
|
return self.start.unit
|