187 lines
6.5 KiB
Python
187 lines
6.5 KiB
Python
"""Gate A2k — fraction-decrease delta (decrease-to fraction of base).
|
||
|
||
Experience Flywheel Sprint 8 / composition-validation **cv-0007** (train_sample
|
||
**0005**): forecast clause states a quantity will ``decrease to N/M of`` its
|
||
current value; a follow-on clause states the current amount; the question asks
|
||
the **decrease by** delta (not the final value).
|
||
|
||
decrease_by = base × (1 − N/M)
|
||
|
||
Narrow organ — not a generic affine equation parser, not ``N/M more than`` affine
|
||
surfaces (0010-class), not percent-decrease. Promotion requires:
|
||
|
||
- exactly one ``decrease to N/M of`` forecast (no ``more than`` fraction affine);
|
||
- question binds ``decrease`` + ``by`` (delta target, not final-value question);
|
||
- exactly one explicit current/base quantity with a unit;
|
||
- hazard refusal (percent, goal language, multiple decrease-to fractions).
|
||
|
||
Deterministic; sealed module (no ``chat/`` import).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
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.target import _question_clause
|
||
from generate.derivation.verify import Resolution, SelfVerification, select_self_verified
|
||
from generate.math_roundtrip import _tokens
|
||
|
||
_DECREASE_TO_FRACTION_RE: Final[re.Pattern[str]] = re.compile(
|
||
r"decrease\s+to\s+(\d+)\s*/\s*(\d+)\s+of",
|
||
re.IGNORECASE,
|
||
)
|
||
_EXTRA_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
|
||
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
|
||
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
|
||
)
|
||
_FINAL_VALUE_CUES: Final[frozenset[str]] = frozenset({"be", "will", "what"})
|
||
|
||
|
||
def _asks_decrease_delta(question_clause: str) -> bool:
|
||
tokens = _tokens(question_clause)
|
||
return "decrease" in tokens and "by" in tokens
|
||
|
||
|
||
def _asks_final_value(question_clause: str) -> bool:
|
||
tokens = _tokens(question_clause)
|
||
if "by" in tokens:
|
||
return False
|
||
if "decrease" not in tokens:
|
||
return False
|
||
return bool(_FINAL_VALUE_CUES & tokens) or "temperature" in tokens
|
||
|
||
|
||
def _target_fraction(problem_text: str) -> tuple[int, int, str] | None:
|
||
matches = list(_DECREASE_TO_FRACTION_RE.finditer(problem_text))
|
||
if len(matches) != 1:
|
||
return None
|
||
num_s, den_s = matches[0].group(1), matches[0].group(2)
|
||
try:
|
||
num = int(num_s)
|
||
den = int(den_s)
|
||
except ValueError:
|
||
return None
|
||
if num <= 0 or den <= 0 or num >= den:
|
||
return None
|
||
return num, den, f"{num_s}/{den_s}"
|
||
|
||
|
||
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
|
||
text_tokens = _tokens(problem_text)
|
||
question_tokens = _tokens(question_clause)
|
||
if "more" in text_tokens and "than" in text_tokens:
|
||
return True
|
||
if "%" in problem_text or "percent" in text_tokens or "percentage" in text_tokens:
|
||
return True
|
||
if text_tokens & _GOAL_INTENT:
|
||
return True
|
||
if _asks_final_value(question_clause):
|
||
return True
|
||
fractions = _EXTRA_FRACTION_RE.findall(problem_text)
|
||
target = _target_fraction(problem_text)
|
||
if target is None:
|
||
return True
|
||
_, _, token = target
|
||
extra = [f for f in fractions if f.replace(" ", "") != token.replace(" ", "")]
|
||
return len(extra) > 0
|
||
|
||
|
||
def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity | None:
|
||
for clause in segment_clauses(problem_text):
|
||
if _DECREASE_TO_FRACTION_RE.search(clause):
|
||
continue
|
||
tokens = _tokens(clause)
|
||
if "current" not in tokens and "now" not in tokens:
|
||
continue
|
||
quantities = [q for q in extract_quantities(clause) if q.unit]
|
||
if len(quantities) == 1:
|
||
return quantities[0]
|
||
for clause in segment_clauses(problem_text):
|
||
if clause == question_clause:
|
||
continue
|
||
if _DECREASE_TO_FRACTION_RE.search(clause):
|
||
continue
|
||
quantities = [q for q in extract_quantities(clause) if q.unit]
|
||
if len(quantities) == 1:
|
||
return quantities[0]
|
||
return None
|
||
|
||
|
||
def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
|
||
"""Construct ``base × (1 − N/M)`` decrease delta, or ``None``."""
|
||
question_clause = _question_clause(problem_text)
|
||
if not _asks_decrease_delta(question_clause):
|
||
return None
|
||
if _has_hazard_surface(problem_text, question_clause):
|
||
return None
|
||
|
||
fraction = _target_fraction(problem_text)
|
||
base = _current_base_quantity(problem_text, question_clause)
|
||
if fraction is None or base is None:
|
||
return None
|
||
|
||
num, den, source_token = fraction
|
||
delta_factor = 1.0 - (num / den)
|
||
if delta_factor <= 0:
|
||
return None
|
||
|
||
return GroundedDerivation(
|
||
start=base,
|
||
steps=(
|
||
Step(
|
||
op="multiply",
|
||
operand=Quantity(
|
||
value=delta_factor,
|
||
unit="",
|
||
source_token=source_token,
|
||
),
|
||
cue="decrease",
|
||
),
|
||
),
|
||
)
|
||
|
||
|
||
def _self_verifies_fraction_decrease(
|
||
derivation: GroundedDerivation, problem_text: str
|
||
) -> SelfVerification:
|
||
from generate.derivation.verify import _base_reasons
|
||
|
||
tokens = _tokens(problem_text)
|
||
reasons = list(_base_reasons(derivation, tokens))
|
||
|
||
fraction = _target_fraction(problem_text)
|
||
if fraction is None:
|
||
reasons.append("missing decrease-to fraction forecast")
|
||
else:
|
||
_, _, token = fraction
|
||
if token not in problem_text.replace(" ", ""):
|
||
reasons.append(f"fraction token {token!r} not grounded in text")
|
||
|
||
base = _current_base_quantity(problem_text, _question_clause(problem_text))
|
||
if base is None:
|
||
reasons.append("missing explicit current/base quantity")
|
||
|
||
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
||
|
||
|
||
def compose_fraction_decrease(problem_text: str) -> Resolution | None:
|
||
"""Gate the typed fraction-decrease chain through self-verification."""
|
||
derivation = build_fraction_decrease(problem_text)
|
||
if derivation is None:
|
||
return None
|
||
if not _self_verifies_fraction_decrease(derivation, problem_text).verified:
|
||
return None
|
||
return Resolution(
|
||
answer=derivation.answer,
|
||
answer_unit=derivation.answer_unit,
|
||
derivation=derivation,
|
||
)
|
||
|
||
|
||
def resolve_promotable_fraction_decrease(problem_text: str) -> Resolution | None:
|
||
"""Serving promotion bridge (Gate A2k)."""
|
||
return compose_fraction_decrease(problem_text)
|