core/generate/derivation/sequential_comparative_scale.py
Shay 8e794ea4a7 feat(derivation): capability paradigm sprint 10 frontier lift
Add Gate A2o affine_comparative_inversion_total (0009) and Gate A2p
sequential_comparative_scale (0006). Reject wholesale multiplicative_aggregate
and defer 0013 piecewise calendar until month day-count grounds in text.

Serving: 21/29/0 → 23/27/0, wrong=0 preserved. report.json and sealed
artifacts untouched.
2026-06-18 11:57:55 -07:00

198 lines
No EOL
6.7 KiB
Python
Raw 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 A2p — sequential comparative scale (running quantity × scale factors).
Sprint 10: train_sample **0006** — an initial quantity plus an ordered chain of
``N times longer`` / ``times the previous length`` scale factors applied to a
running state.
Chain:
answer = initial × scale₁ × scale₂ ×× scaleₙ
Narrow organ — not broad ``multiplicative_aggregate``, not age-timeline parsing,
not generic nearby-number multiplication. Promotion requires:
- question asks ``how many pages``;
- body states an initial ``<N> pages`` anchor;
- at least two scale clauses with ``times longer`` or ``times the previous length``;
- hazard refusal (fractions, percent, money, goal language, ``doubled`` surfaces);
- age/year scaffolding excluded from completeness obligation.
Deterministic; sealed module (no ``chat/`` import).
"""
from __future__ import annotations
import re
from typing import Final
from collections import Counter
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 _token_in, _tokens, _value_grounds
_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
_INITIAL_PAGES_RE: Final[re.Pattern[str]] = re.compile(
r"(\d+)\s+pages\b",
re.IGNORECASE,
)
_SCALE_LONGER_RE: Final[re.Pattern[str]] = re.compile(
r"(\d+)\s+times\s+longer\b",
re.IGNORECASE,
)
_SCALE_PREVIOUS_RE: Final[re.Pattern[str]] = re.compile(
r"(\d+)\s+times\s+the\s+previous\s+length\b",
re.IGNORECASE,
)
_TEXT_BLOCKERS: Final[frozenset[str]] = frozenset(
{
"doubled",
"insurance",
"percent",
"percentage",
"profit",
"weight",
"weighed",
"pounds",
"pound",
}
)
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
{"want", "wants", "wanted", "need", "needs", "goal", "plans"}
)
_SCALE_OBLIGATION_UNITS: Final[frozenset[str]] = frozenset({"pages", "times"})
def _asks_page_count(question_clause: str) -> bool:
tokens = _tokens(question_clause)
return "how" in tokens and "many" in tokens and "pages" in tokens
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
if _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:
return True
if text_tokens & _TEXT_BLOCKERS:
return True
if question_tokens & _GOAL_INTENT:
return True
if "$" in problem_text:
return True
return False
def _initial_pages(problem_text: str) -> Quantity | None:
match = _INITIAL_PAGES_RE.search(problem_text)
if match is None:
return None
value = float(match.group(1))
return Quantity(value=value, unit="pages", source_token=match.group(1))
def _scale_factors_in_order(problem_text: str) -> list[tuple[float, str, str]]:
"""Return ``(value, source_token, cue)`` for each scale clause in narrative order."""
factors: list[tuple[float, str, str]] = []
for match in _SCALE_LONGER_RE.finditer(problem_text):
factors.append((float(match.group(1)), match.group(1), "longer"))
for match in _SCALE_PREVIOUS_RE.finditer(problem_text):
factors.append((float(match.group(1)), match.group(1), "previous"))
if not factors:
return []
# Re-sort by position in text to preserve narrative order when both patterns appear.
ordered: list[tuple[float, str, str, int]] = []
for match in _SCALE_LONGER_RE.finditer(problem_text):
ordered.append((float(match.group(1)), match.group(1), "longer", match.start()))
for match in _SCALE_PREVIOUS_RE.finditer(problem_text):
ordered.append(
(float(match.group(1)), match.group(1), "previous", match.start())
)
ordered.sort(key=lambda item: item[3])
return [(v, token, cue) for v, token, cue, _ in ordered]
def build_sequential_comparative_scale(problem_text: str) -> GroundedDerivation | None:
"""Construct the ungated sequential scale chain, or ``None``."""
question_clause = _question_clause(problem_text)
if not _asks_page_count(question_clause):
return None
if _has_hazard_surface(problem_text, question_clause):
return None
initial = _initial_pages(problem_text)
factors = _scale_factors_in_order(problem_text)
if initial is None or len(factors) < 2:
return None
steps = tuple(
Step(
op="multiply",
operand=Quantity(value=value, unit="times", source_token=token),
cue=cue,
)
for value, token, cue in factors
)
return GroundedDerivation(start=initial, steps=steps)
def _obligation_quantities(problem_text: str) -> Counter[str]:
return Counter(
q.source_token
for q in extract_quantities(problem_text)
if q.unit in _SCALE_OBLIGATION_UNITS
)
def _self_verifies_sequential_scale(
derivation: GroundedDerivation, problem_text: str
) -> SelfVerification:
from generate.derivation.verify import _base_reasons
tokens = _tokens(problem_text)
reasons = list(_base_reasons(derivation, tokens))
for step in derivation.steps:
if not _token_in(step.cue, tokens):
reasons.append(f"operation cue {step.cue!r} not grounded in text")
obligation = _obligation_quantities(problem_text)
used = Counter(
[
derivation.start.source_token,
*(step.operand.source_token for step in derivation.steps),
]
)
unused = obligation - used
if unused:
reasons.append(f"incomplete: unused scale quantities {sorted(unused.keys())}")
initial = _initial_pages(problem_text)
if initial is None or not _value_grounds(initial.source_token, tokens):
reasons.append("missing grounded initial pages anchor")
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
def compose_sequential_comparative_scale(problem_text: str) -> Resolution | None:
"""Gate the typed sequential scale chain through self-verification."""
derivation = build_sequential_comparative_scale(problem_text)
if derivation is None:
return None
if not _self_verifies_sequential_scale(derivation, problem_text).verified:
return None
return Resolution(
answer=derivation.answer,
answer_unit=derivation.answer_unit,
derivation=derivation,
)
def resolve_promotable_sequential_comparative_scale(
problem_text: str,
) -> Resolution | None:
"""Serving promotion bridge (Gate A2p)."""
return compose_sequential_comparative_scale(problem_text)