Patch post-merge wrong=0 hazards from #820 review without reverting Sprint 9 organs: bind affine/temporal answers to asked subjects and body actors, anchor overtime thresholds to per-shift/day surfaces, fix affine completeness double-counting, narrow percent_partition group skip, and expand temporal question/rental verb recognition.
278 lines
No EOL
9.6 KiB
Python
278 lines
No EOL
9.6 KiB
Python
"""Gate A2n — affine fraction-delta (fraction-of-reference plus offset).
|
||
|
||
Experience Flywheel Sprint 9 / microscope ``affine_equation_fraction_delta``
|
||
(train_sample **0010**): a prior clause establishes a reference entity's current
|
||
amount (including initial-loss mutation); a follow-on clause states
|
||
``N/M more than what <entity> currently has, plus K``; the question asks the
|
||
subject's total count.
|
||
|
||
GSM8K gold for this family computes:
|
||
|
||
answer = reference × (N/M) + K
|
||
|
||
not ``reference × (1 + N/M)``. Narrow organ — not ``decrease to N/M of``
|
||
(fraction_decrease), not comparative ``twice as many``, not a generic affine
|
||
equation parser. Promotion requires:
|
||
|
||
- exactly one ``N/M more than what <entity> currently has`` clause;
|
||
- explicit ``plus K`` offset in the same clause;
|
||
- resolvable reference quantity from a single prior initial-mutation clause;
|
||
- question binds ``how many`` + possession cue for the affine subject;
|
||
- hazard refusal (decrease-to fraction, percent, goal language, multiple fractions).
|
||
|
||
Deterministic; sealed module (no ``chat/`` import).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Final
|
||
|
||
from collections import Counter
|
||
|
||
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 PRONOUNS, leading_subject_token
|
||
from generate.derivation.target import _question_clause
|
||
from generate.derivation.verify import Resolution, SelfVerification
|
||
from generate.math_candidate_parser import _init_mutation_admitted
|
||
from generate.math_roundtrip import _token_in, _tokens, _value_grounds
|
||
|
||
_AFFINE_FRACTION_RE: Final[re.Pattern[str]] = re.compile(
|
||
r"(\d+)\s*/\s*(\d+)\s+more\s+than\s+what\s+(\w+)\s+currently\s+has,\s+plus\s+(\d+)",
|
||
re.IGNORECASE,
|
||
)
|
||
_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"}
|
||
)
|
||
_POSSESSION_CUES: Final[frozenset[str]] = frozenset({"has", "have", "had"})
|
||
_QUESTION_POSSESSION_RE: Final[re.Pattern[str]] = re.compile(
|
||
r"how\s+many\b.+?\bdoes\s+(\w+)\s+have\b",
|
||
re.IGNORECASE,
|
||
)
|
||
|
||
|
||
def _asks_subject_total(question_clause: str) -> bool:
|
||
tokens = _tokens(question_clause)
|
||
return "how" in tokens and "many" in tokens and bool(_POSSESSION_CUES & tokens)
|
||
|
||
|
||
def _affine_match(problem_text: str) -> re.Match[str] | None:
|
||
matches = list(_AFFINE_FRACTION_RE.finditer(problem_text))
|
||
if len(matches) != 1:
|
||
return None
|
||
return matches[0]
|
||
|
||
|
||
def _affine_clause(problem_text: str) -> str | None:
|
||
match = _affine_match(problem_text)
|
||
if match is None:
|
||
return None
|
||
for clause in segment_clauses(problem_text):
|
||
if match.group(0) in clause:
|
||
return clause
|
||
return None
|
||
|
||
|
||
def _affine_subject(problem_text: str) -> str | None:
|
||
clause = _affine_clause(problem_text)
|
||
if clause is None:
|
||
return None
|
||
subject = leading_subject_token(clause)
|
||
return subject.lower() if subject is not None else None
|
||
|
||
|
||
def _question_possession_subject(question_clause: str) -> str | None:
|
||
match = _QUESTION_POSSESSION_RE.search(question_clause)
|
||
if match is None:
|
||
return None
|
||
return match.group(1).lower()
|
||
|
||
|
||
def _question_subject_matches_affine(
|
||
problem_text: str, question_clause: str
|
||
) -> bool:
|
||
affine_subject = _affine_subject(problem_text)
|
||
asked_subject = _question_possession_subject(question_clause)
|
||
if affine_subject is None or asked_subject is None:
|
||
return False
|
||
if asked_subject in PRONOUNS:
|
||
return False
|
||
return asked_subject == affine_subject
|
||
|
||
|
||
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
|
||
if _DECREASE_TO_FRACTION_RE.search(problem_text):
|
||
return True
|
||
text_tokens = _tokens(problem_text)
|
||
question_tokens = _tokens(question_clause)
|
||
if "%" in problem_text or "percent" in text_tokens or "percentage" in text_tokens:
|
||
return True
|
||
if text_tokens & _GOAL_INTENT:
|
||
return True
|
||
if "twice" in text_tokens or "thrice" in text_tokens or "double" in text_tokens:
|
||
return True
|
||
match = _affine_match(problem_text)
|
||
if match is None:
|
||
return True
|
||
fractions = _EXTRA_FRACTION_RE.findall(problem_text)
|
||
token = f"{match.group(1)}/{match.group(2)}"
|
||
extra = [f for f in fractions if f.replace(" ", "") != token.replace(" ", "")]
|
||
return len(extra) > 0
|
||
|
||
|
||
def _reference_candidate(problem_text: str, entity: str):
|
||
question_clause = _question_clause(problem_text)
|
||
match = _affine_match(problem_text)
|
||
if match is None:
|
||
return None
|
||
affine_clause = match.group(0)
|
||
for clause in segment_clauses(problem_text):
|
||
if clause == question_clause or affine_clause in clause:
|
||
continue
|
||
for candidate in _init_mutation_admitted(clause):
|
||
if candidate.initial.entity.lower() != entity.lower():
|
||
continue
|
||
return candidate
|
||
return None
|
||
|
||
|
||
def _reference_quantity(problem_text: str, entity: str) -> Quantity | None:
|
||
candidate = _reference_candidate(problem_text, entity)
|
||
if candidate is None:
|
||
return None
|
||
qty = candidate.initial.quantity
|
||
return Quantity(
|
||
value=qty.value,
|
||
unit=qty.unit,
|
||
source_token=candidate.matched_value_token,
|
||
)
|
||
|
||
|
||
def build_affine_fraction_delta(problem_text: str) -> GroundedDerivation | None:
|
||
"""Construct ``reference × (N/M) + K``, or ``None``."""
|
||
question_clause = _question_clause(problem_text)
|
||
if not _asks_subject_total(question_clause):
|
||
return None
|
||
if not _question_subject_matches_affine(problem_text, question_clause):
|
||
return None
|
||
if _has_hazard_surface(problem_text, question_clause):
|
||
return None
|
||
|
||
match = _affine_match(problem_text)
|
||
if match is None:
|
||
return None
|
||
|
||
num_s, den_s, entity, offset_s = (
|
||
match.group(1),
|
||
match.group(2),
|
||
match.group(3),
|
||
match.group(4),
|
||
)
|
||
try:
|
||
num = int(num_s)
|
||
den = int(den_s)
|
||
offset = float(offset_s)
|
||
except ValueError:
|
||
return None
|
||
if num <= 0 or den <= 0:
|
||
return None
|
||
|
||
reference = _reference_quantity(problem_text, entity)
|
||
if reference is None:
|
||
return None
|
||
|
||
factor = num / den
|
||
fraction_token = f"{num_s}/{den_s}"
|
||
|
||
return GroundedDerivation(
|
||
start=reference,
|
||
steps=(
|
||
Step(
|
||
op="multiply",
|
||
operand=Quantity(value=factor, unit="", source_token=fraction_token),
|
||
cue="more",
|
||
),
|
||
Step(
|
||
op="add",
|
||
operand=Quantity(value=offset, unit=reference.unit, source_token=offset_s),
|
||
cue="plus",
|
||
),
|
||
),
|
||
)
|
||
|
||
|
||
def _self_verifies_affine_fraction_delta(
|
||
derivation: GroundedDerivation, problem_text: str
|
||
) -> SelfVerification:
|
||
reasons: list[str] = []
|
||
tokens = _tokens(problem_text)
|
||
question_clause = _question_clause(problem_text)
|
||
|
||
operands = [derivation.start, *(s.operand for s in derivation.steps if not s.comparative)]
|
||
for q in operands:
|
||
if not _value_grounds(q.source_token, tokens):
|
||
reasons.append(f"operand {q.source_token!r} not grounded in text")
|
||
|
||
for step in derivation.steps:
|
||
if not _token_in(step.cue, tokens):
|
||
reasons.append(f"operation cue {step.cue!r} not grounded in text")
|
||
|
||
primary_unit = derivation.start.unit
|
||
for step in derivation.steps:
|
||
if step.op in {"add", "subtract"} and step.operand.unit != primary_unit:
|
||
reasons.append(
|
||
f"unit mismatch: {step.op} of {step.operand.unit!r} into {primary_unit!r}"
|
||
)
|
||
|
||
match = _affine_match(problem_text)
|
||
if match is None:
|
||
reasons.append("missing affine fraction clause")
|
||
else:
|
||
reference = _reference_quantity(problem_text, match.group(3))
|
||
if reference is None:
|
||
reasons.append("missing reference initial-mutation quantity")
|
||
|
||
body = problem_text.replace(question_clause, "").strip()
|
||
body_quantities = Counter(q.source_token for q in extract_quantities(body))
|
||
used = Counter(
|
||
[derivation.start.source_token]
|
||
+ [step.operand.source_token for step in derivation.steps]
|
||
)
|
||
if match is not None:
|
||
used[match.group(1)] += 1
|
||
used[match.group(2)] += 1
|
||
reference_candidate = _reference_candidate(problem_text, match.group(3))
|
||
if reference_candidate is not None:
|
||
for token in reference_candidate.consumed_value_tokens:
|
||
used[token] += 1
|
||
unused = body_quantities - used
|
||
if unused:
|
||
reasons.append(f"incomplete: unused body quantities {sorted(unused.elements())}")
|
||
|
||
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
|
||
|
||
|
||
def compose_affine_fraction_delta(problem_text: str) -> Resolution | None:
|
||
"""Gate the typed affine fraction-delta chain through self-verification."""
|
||
derivation = build_affine_fraction_delta(problem_text)
|
||
if derivation is None:
|
||
return None
|
||
if not _self_verifies_affine_fraction_delta(derivation, problem_text).verified:
|
||
return None
|
||
return Resolution(
|
||
answer=derivation.answer,
|
||
answer_unit=derivation.answer_unit,
|
||
derivation=derivation,
|
||
)
|
||
|
||
|
||
def resolve_promotable_affine_fraction_delta(problem_text: str) -> Resolution | None:
|
||
"""Serving promotion bridge (Gate A2n)."""
|
||
return compose_affine_fraction_delta(problem_text) |