"""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 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 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)