feat: retire fraction_decrease prose regex — bind dilation from frame scale
Some checks failed
lane-shas / verify pinned lane SHAs (pull_request) Has been cancelled
smoke / smoke (-m "not quarantine") (pull_request) Has been cancelled

The post-CGA / ProblemFrame pivot comes to fruition: geometric dilation for
fraction_decrease is built from the bound scale role's GroundedScalar
(exact Fraction from kernel slash_fraction / pack numerics), not from a
local "decrease to N/M of" regex on evidence spans.

- Remove _build_fraction_decrease_payload_and_bind (legacy overfitting path)
- Add _fraction_decrease_scale_binding + _versor_binding_from_scale_value
- assess_fraction_decrease attaches VersorBindings when runnable
- assess_geometric_proposals uses frame scale only for fraction_decrease
- compose fallback prefers obligation-complete contracts with bindings
- Guard test: contracts source must not reintroduce the prose regex

Invariants: construction-boundary CGA only; versor_condition < 1e-6;
no new derivation-organ prose parser.
This commit is contained in:
Shay 2026-07-08 20:09:49 -07:00
parent 6b4bfbe675
commit 2a3f7ad125
5 changed files with 204 additions and 74 deletions

View file

@ -164,19 +164,32 @@ def _self_verifies_fraction_decrease(
def _resolve_fraction_decrease_contract(
problem_text: str,
) -> ContractAssessment | None:
"""Build a geometric ContractAssessment for fraction_decrease when callers
omit an explicit contract (sprint/dev API compatibility).
"""Build a ContractAssessment for fraction_decrease when callers omit one.
Uses ``assess_geometric_proposals`` so VersorBinding payloads come from the
owned CGA construction path (pack signature or legacy N/M helper).
Prefers ``assess_contracts`` (obligation-complete + frame-grounded scale
VersorBinding). Falls back to ``assess_geometric_proposals`` (same frame
scale binding path). Never re-parses "decrease to N/M of" prose.
"""
from generate.problem_frame_builder import build_problem_frame
from generate.problem_frame_contracts import assess_geometric_proposals
from generate.problem_frame_contracts import (
assess_contracts,
assess_geometric_proposals,
)
frame = build_problem_frame(problem_text)
assessments = assess_geometric_proposals(frame)
for assessment in assess_contracts(frame):
if (
assessment.candidate_organ == "fraction_decrease"
and assessment.runnable
and assessment.bindings
):
return assessment
return next(
(a for a in assessments if a.candidate_organ == "fraction_decrease"),
(
a
for a in assess_geometric_proposals(frame)
if a.candidate_organ == "fraction_decrease" and a.bindings
),
None,
)

View file

@ -496,6 +496,12 @@ def assess_fraction_decrease(frame: ProblemFrame) -> ContractAssessment:
)
)
runnable = not missing and not unresolved
# Geometric dilation binding from frame scale (KernelFacts), not prose.
geometric_bindings: list[VersorBinding] = []
if runnable:
scale_binding = _fraction_decrease_scale_binding(frame)
if scale_binding is not None:
geometric_bindings.append(scale_binding)
return ContractAssessment(
candidate_organ="fraction_decrease",
missing_bindings=tuple(dict.fromkeys(missing)),
@ -508,6 +514,7 @@ def assess_fraction_decrease(frame: ProblemFrame) -> ContractAssessment:
+ ", ".join((*dict.fromkeys(missing), *sorted(unresolved)))
),
evidence_spans=evidence_spans,
bindings=geometric_bindings,
)
@ -911,46 +918,103 @@ def _scale_from_geometric_signature(geom: dict) -> float | None:
return None
def _build_fraction_decrease_payload_and_bind(span_text: str) -> tuple[np.ndarray, str] | None:
"""Construct CGA dilation versor payload and bind text for fraction-decrease evidence.
def _versor_binding_from_scale_value(
scale_value: object,
*,
semantic_identity: str,
source_span: tuple[int, int],
) -> VersorBinding | None:
"""Build a dilation VersorBinding from an exact kernel scale (Fraction/float).
Prefer pack-driven geometric_signature (via resolve_geometric_signature on
the span or an embedded slash-fraction token). LEGACY EXCEPTION: if packs
do not yet ratify the parameterized phrase, extract ``N/M`` from evidence
text via a local regex migration target is pack senses.jsonl entries
with ``geometric_signature: {numerator, denominator}`` (or ``scale``).
This is the post-pivot construction path: scale arrives as a grounded
scalar fact on ProblemFrame never re-parsed from "decrease to N/M of"
prose. CGA dilation is a pure construction-boundary op on that number.
"""
# Pack-first: whole span, then first slash-fraction token.
candidates: list[str] = [span_text]
frac_match = re.search(r"(\d+\s*/\s*\d+)", span_text)
if frac_match:
candidates.append(frac_match.group(1).replace(" ", ""))
for cand in candidates:
signature = resolve_geometric_signature(cand)
if not signature:
continue
_, geom = signature
scale = _scale_from_geometric_signature(geom)
if scale is None:
continue
bind_text = cand if cand != span_text else (frac_match.group(1) if frac_match else span_text)
return _dilation_versor_payload(scale), bind_text
try:
k = float(scale_value) # Fraction implements __float__
except (TypeError, ValueError):
return None
if not np.isfinite(k) or k <= 0.0:
return None
payload = _dilation_versor_payload(k)
return VersorBinding(
source_span=source_span,
semantic_identity=semantic_identity,
geometric_payload=payload,
versor_error=versor_condition(payload),
)
# LEGACY EXCEPTION: local prose extract until pack signatures cover
# parameterized "decrease to N/M of" phrases (kernel substrate rule).
m = re.search(r"decrease to (\d+)/(\d+)\s+of", span_text)
if not m:
def _fraction_decrease_scale_binding(frame: ProblemFrame) -> VersorBinding | None:
"""Resolve fraction-decrease dilation binding from ProblemFrame scale role.
Order of truth (no local prose parse):
1. Bound ``decrease_to_fraction`` relation role ``scale`` GroundedScalar.value
2. Optional pack geometric_signature on the scale *surface* (named fractions
/ pack-ratified lemmas) when present still identity = surface token,
not a phrase regex.
"""
relations = [
relation
for relation in frame.bound_relations
if relation.relation_type == "decrease_to_fraction"
]
if len(relations) != 1:
return None
n, d = int(m.group(1)), int(m.group(2))
if d == 0:
relation = relations[0]
scale_id = _role_target(relation, "scale")
if scale_id is None:
return None
bind_text = frac_match.group(1) if frac_match else span_text
return _dilation_versor_payload(n / d), bind_text
quantities = _quantity_value_by_mention_id(frame)
scale_q = quantities.get(scale_id)
if scale_q is None:
return None
mentions = _mention_map(frame)
mention = mentions.get(scale_id)
surface = getattr(mention, "surface", None) or str(getattr(scale_q, "value", ""))
span = getattr(mention, "span", None)
if span is None:
role_spans = _role_spans(relation, "scale")
if not role_spans:
return None
span = role_spans[0]
source_span = (span.start, span.end)
# Prefer exact kernel scalar (slash_fraction / named fraction already
# canonicalized to Fraction on the frame).
binding = _versor_binding_from_scale_value(
getattr(scale_q, "value", scale_q),
semantic_identity=str(surface),
source_span=source_span,
)
if binding is not None:
return binding
# Pack geometric_signature on scale surface (e.g. named fraction lemmas
# with senses geometric_signature) — substrate lookup only.
signature = resolve_geometric_signature(str(surface))
if signature is None:
return None
_, geom = signature
scale = _scale_from_geometric_signature(geom)
if scale is None:
return None
return _versor_binding_from_scale_value(
scale,
semantic_identity=str(surface),
source_span=source_span,
)
def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
"""Geometric contract assessments from proposals + frame-grounded scale.
For ``fraction_decrease``, dilation VersorBindings are built from the
ProblemFrame scale role (KernelFacts GroundedScalar), not from re-parsing
proposal evidence spans with a local "decrease to N/M of" regex.
"""
assessments = []
# Pre-compute reverse mapping from family_id to candidate_organ
family_to_organ = {}
for organ, contract in _CONTRACT_REGISTRY.items():
@ -961,13 +1025,19 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
candidate_organ = family_to_organ.get(prop.family_id)
if not candidate_organ:
continue
bindings = []
for span in prop.evidence_spans:
signature = resolve_geometric_signature(span.text)
payload = None
bind_text = span.text
if signature:
bindings: list[VersorBinding] = []
if candidate_organ == "fraction_decrease":
# Frame-first: one binding from bound scale role.
scale_binding = _fraction_decrease_scale_binding(frame)
if scale_binding is not None:
bindings.append(scale_binding)
else:
for span in prop.evidence_spans:
signature = resolve_geometric_signature(span.text)
if not signature:
continue
_, geom = signature
scale = _scale_from_geometric_signature(geom)
if scale is not None:
@ -976,33 +1046,30 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
# Unit payload when signature exists but carries no scale.
payload = np.zeros(32, dtype=np.float64)
payload[0] = 1.0
elif candidate_organ == "fraction_decrease":
frac_result = _build_fraction_decrease_payload_and_bind(span.text)
if frac_result:
payload, bind_text = frac_result
if payload is not None:
binding = VersorBinding(
source_span=(span.start, span.end),
semantic_identity=bind_text,
geometric_payload=payload,
versor_error=versor_condition(payload),
bindings.append(
VersorBinding(
source_span=(span.start, span.end),
semantic_identity=span.text,
geometric_payload=payload,
versor_error=versor_condition(payload),
)
)
bindings.append(binding)
runnable = False
if bindings and all(b.versor_error < 1e-6 for b in bindings):
runnable = True
runnable = bool(bindings) and all(b.versor_error < 1e-6 for b in bindings)
assessment = ContractAssessment(
bindings=bindings,
candidate_organ=candidate_organ,
runnable=runnable,
explanation="Assessed via geometric algebra.",
evidence_spans=prop.evidence_spans
explanation=(
"Assessed via geometric algebra from frame-grounded scale"
if candidate_organ == "fraction_decrease"
else "Assessed via geometric algebra."
),
evidence_spans=prop.evidence_spans,
)
assessments.append(assessment)
return assessments
def assess_contracts(frame: ProblemFrame, depth: dict | None = None) -> tuple[ContractAssessment, ...]:

View file

@ -73,7 +73,7 @@ Alignment: AGENTS.md (pre-edit traces, immutability via replace/new, exact recal
- **Same-turn root recog (done):** `resolve_token_depths` + pipeline early wire before graph.
- **Capability obligations (done):** `tests/test_3lang_depth_capability.py` (he/grc exemplars, result fields, construction, dilation).
- **public_demo budget (done):** 60s reference budget; soft case (hard raise opt-in); re-pin lane SHAs.
- **Geometric signature (partial):** pack-scale preferred; legacy N/M regex retained with explicit migration note.
- **Geometric signature (done 2026-07-08):** fraction_decrease dilation binds from ProblemFrame scale role (KernelFacts GroundedScalar Fraction) — legacy "decrease to N/M of" prose regex **removed** from problem_frame_contracts.
- Rebase/merge per git workflow after review (Forgejo / `tea`).
- (retired) No longer create formal HANDOFF files. Use session-break-summary-<DATETIME>.md on pauses per AGENTS.md.

View file

@ -177,7 +177,7 @@ def test_construction_assess_with_he_root_depth() -> None:
def test_dilation_payload_from_scale_and_signature() -> None:
"""P4: pack-shaped geometric_signature scale drives dilation versor."""
"""Pack-shaped geometric_signature + frame scale drive dilation versor."""
payload = _dilation_versor_payload(0.5)
assert payload.shape == (32,)
assert float(versor_condition(payload)) < 1e-6
@ -188,11 +188,26 @@ def test_dilation_payload_from_scale_and_signature() -> None:
)
assert _scale_from_geometric_signature({"note": "no scale"}) is None
# Legacy extract still works for parameterized decrease phrase.
from generate.problem_frame_contracts import _build_fraction_decrease_payload_and_bind
# Post-pivot: frame-grounded scale (KernelFacts Fraction), not prose regex.
from generate.problem_frame_contracts import (
assess_fraction_decrease,
assess_geometric_proposals,
)
frac = _build_fraction_decrease_payload_and_bind("decrease to 3/4 of the original")
assert frac is not None
payload2, bind = frac
assert float(versor_condition(payload2)) < 1e-6
assert "3" in bind and "4" in bind
text = (
"In one hour, Addison mountain's temperature will decrease to 3/4 of its temperature. "
"If the current temperature of the mountain is 84 degrees, what will the temperature "
"decrease by?"
)
frame = build_problem_frame(text)
geom = assess_geometric_proposals(frame)
assert len(geom) == 1
assert geom[0].runnable
assert geom[0].bindings
assert geom[0].bindings[0].semantic_identity == "3/4"
assert float(versor_condition(geom[0].bindings[0].geometric_payload)) < 1e-6
obligation = assess_fraction_decrease(frame)
assert obligation.runnable
assert obligation.bindings
assert obligation.bindings[0].semantic_identity == "3/4"

View file

@ -1,5 +1,13 @@
from pathlib import Path
from generate.problem_frame_builder import build_problem_frame
from generate.problem_frame_contracts import assess_contracts, assess_fraction_decrease, assess_percent_partition
from generate.problem_frame_contracts import (
assess_contracts,
assess_fraction_decrease,
assess_geometric_proposals,
assess_percent_partition,
)
from algebra.versor import versor_condition
FRACTION_DECREASE_CASE = (
@ -59,6 +67,33 @@ def test_fraction_decrease_contract_is_runnable_from_problemframe() -> None:
assert assessment.runnable
assert assessment.missing_bindings == ()
assert assessment.evidence_spans
# Pivot revelation: geometric dilation binds from frame scale Fraction(3/4),
# not from re-parsing "decrease to N/M of" prose.
assert assessment.bindings
assert assessment.bindings[0].semantic_identity in {"3/4", "3 / 4"}
assert float(versor_condition(assessment.bindings[0].geometric_payload)) < 1e-6
def test_fraction_decrease_geometric_uses_frame_scale_not_prose_regex() -> None:
frame = build_problem_frame(FRACTION_DECREASE_CASE)
geom = assess_geometric_proposals(frame)
assert len(geom) == 1
assert geom[0].candidate_organ == "fraction_decrease"
assert geom[0].runnable
assert geom[0].bindings[0].semantic_identity.replace(" ", "") == "3/4"
# Payload is dilation for k=0.75 (same as Fraction(3,4)).
from generate.problem_frame_contracts import _dilation_versor_payload
import numpy as np
expected = _dilation_versor_payload(0.75)
assert np.allclose(geom[0].bindings[0].geometric_payload, expected)
def test_no_legacy_decrease_to_fraction_prose_regex_in_contracts() -> None:
"""Guard: the overfitting local prose parser must not return."""
source = Path("generate/problem_frame_contracts.py").read_text(encoding="utf-8")
assert r"decrease to (\d+" not in source
assert "_build_fraction_decrease_payload_and_bind" not in source
def test_fraction_decrease_sibling_is_runnable() -> None: