feat: retire fraction_decrease prose regex — bind dilation from frame scale
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:
parent
6b4bfbe675
commit
2a3f7ad125
5 changed files with 204 additions and 74 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ...]:
|
||||
|
|
|
|||
2
plan.md
2
plan.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue