core/generate/derivation/percent_partition.py
Shay 80753e13de
fix(gsm8k): harden sprint 9 temporal and affine gates (#822)
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.
2026-06-18 10:57:36 -07:00

182 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Gate A2l — equal half-split percent partition aggregate.
Experience Flywheel Sprint 8 / composition-validation **cv-0008** (train_sample
**0046**): total population, equal ``half`` girls / ``other half`` boys split,
two subgroup ``N% of the <group>`` clauses, question asks how many total own the
attribute.
subgroup = total × half
answer = subgroup × pct_girls + subgroup × pct_boys
Narrow organ — not a generic percent equation parser, not unequal partitions, not
multi-container DCS. Promotion requires:
- question asks ``how many`` + aggregate ownership cue;
- total population quantity with unit;
- equal half / other-half split language;
- exactly two percent-of-subgroup clauses with distinct group referents;
- hazard refusal (fraction surfaces, money, profit, goal language).
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.comparatives import comparative_step, extract_comparative_scalars
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
from generate.math_roundtrip import _tokens
_PERCENT_OF_GROUP_RE: Final[re.Pattern[str]] = re.compile(
r"(\d+(?:\.\d+)?)\s*%\s+of\s+the\s+(\w+)",
re.IGNORECASE,
)
_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"}
)
_OWNERSHIP_CUES: Final[frozenset[str]] = frozenset(
{"own", "owns", "have", "has", "had"}
)
def _asks_partition_total(question_clause: str) -> bool:
tokens = _tokens(question_clause)
return "how" in tokens and "many" in tokens and bool(_OWNERSHIP_CUES & tokens)
def _has_equal_half_split(problem_text: str) -> bool:
lowered = problem_text.lower()
return "half" in lowered and "other half" in lowered
def _percent_clauses(problem_text: str) -> tuple[tuple[float, str, str], tuple[float, str, str]] | None:
question_clause = _question_clause(problem_text)
matches: list[tuple[float, str, str]] = []
for clause in segment_clauses(problem_text):
if clause == question_clause:
continue
for match in _PERCENT_OF_GROUP_RE.finditer(clause):
pct = float(match.group(1)) / 100.0
group = match.group(2).lower()
source = match.group(1)
matches.append((pct, group, source))
if len(matches) != 2:
return None
groups = {m[1] for m in matches}
if len(groups) != 2:
return None
return matches[0], matches[1]
def _total_population(problem_text: str, question_clause: str) -> Quantity | None:
for clause in segment_clauses(problem_text):
if clause == question_clause:
continue
lowered = clause.lower()
if "half" in lowered or _PERCENT_OF_GROUP_RE.search(clause):
continue
if "group" in lowered and "group of" not in lowered:
continue
quantities = [q for q in extract_quantities(clause) if q.unit]
if len(quantities) == 1:
return quantities[0]
return None
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
text_tokens = _tokens(problem_text)
if _FRACTION_RE.search(problem_text):
return True
if text_tokens & _GOAL_INTENT:
return True
if "profit" in text_tokens or "money" in text_tokens or "$" in problem_text:
return True
if not _has_equal_half_split(problem_text):
return True
comparatives = extract_comparative_scalars(problem_text)
if not any(cs.cue == "half" for cs in comparatives):
return True
return False
def build_percent_partition(problem_text: str) -> GroundedDerivation | None:
"""Construct equal half-split percent partition total, or ``None``."""
question_clause = _question_clause(problem_text)
if not _asks_partition_total(question_clause):
return None
if _has_hazard_surface(problem_text, question_clause):
return None
total = _total_population(problem_text, question_clause)
percent_pair = _percent_clauses(problem_text)
if total is None or percent_pair is None:
return None
(pct_a, _, source_a), (pct_b, _, source_b) = percent_pair
comparatives = extract_comparative_scalars(problem_text)
half = next((cs for cs in comparatives if cs.cue == "half"), None)
if half is None:
return None
subgroup = total.value * half.scalar
contrib_a = subgroup * pct_a
contrib_b = subgroup * pct_b
return GroundedDerivation(
start=total,
steps=(
comparative_step(half),
Step(
op="multiply",
operand=Quantity(value=pct_a, unit="", source_token=source_a),
cue="of",
),
Step(
op="add",
operand=Quantity(value=contrib_b, unit=total.unit, source_token=source_b),
cue="and",
),
),
)
def _self_verifies_percent_partition(
derivation: GroundedDerivation, problem_text: str
) -> SelfVerification:
from generate.derivation.verify import _base_reasons
tokens = _tokens(problem_text)
reasons = list(_base_reasons(derivation, tokens))
if _percent_clauses(problem_text) is None:
reasons.append("expected two distinct percent-of-subgroup clauses")
if _total_population(problem_text, _question_clause(problem_text)) is None:
reasons.append("missing total population quantity")
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
def compose_percent_partition(problem_text: str) -> Resolution | None:
"""Gate the typed percent-partition chain through self-verification."""
derivation = build_percent_partition(problem_text)
if derivation is None:
return None
if not _self_verifies_percent_partition(derivation, problem_text).verified:
return None
return Resolution(
answer=derivation.answer,
answer_unit=derivation.answer_unit,
derivation=derivation,
)
def resolve_promotable_percent_partition(problem_text: str) -> Resolution | None:
"""Serving promotion bridge (Gate A2l)."""
return compose_percent_partition(problem_text)