Merge pull request 'fix(a2k): reject out-of-range fraction-decrease scales' (#89) from fix/a2k-fraction-decrease-scale-range into main

Reviewed-on: #89
This commit is contained in:
Joshua Matthew-Catudio Shay 2026-07-20 04:42:05 +00:00
commit 4b9dbe7236
4 changed files with 187 additions and 22 deletions

View file

@ -469,7 +469,7 @@ def assess_fraction_decrease(frame: ProblemFrame) -> ContractAssessment:
missing.append("state_entity_continuity_unproven")
scale = quantities.get(scale_id) if scale_id is not None else None
if scale is not None and not (0 < scale.value < 1):
if scale is not None and not _is_valid_fraction_decrease_scale(scale.value):
missing.append("scale_out_of_range")
categories = {hazard.category for hazard in frame.hazards}
@ -881,11 +881,30 @@ def assess_percent_partition(frame: ProblemFrame) -> ContractAssessment:
)
def _is_valid_fraction_decrease_scale(scale_value: object) -> bool:
"""Canonical semantic domain for fraction-decrease scale: finite ``0 < k < 1``.
Shared by obligation assessment (``scale_out_of_range``) and geometric
VersorBinding admission so the geometric path cannot grant permission the
obligation layer denies. Accepts ``Fraction`` and numeric types via
``float()``; rejects non-finite, zero, negative, one, and greater-than-one.
"""
try:
k = float(scale_value) # Fraction implements __float__
except (TypeError, ValueError):
return False
return bool(np.isfinite(k) and 0.0 < k < 1.0)
def _dilation_versor_payload(scale: float) -> np.ndarray:
"""CGA dilation versor for multiplicative scale ``k`` (cosh/sinh on log-ratio).
Pure construction boundary produces a 32-float payload with
``versor_condition < 1e-6`` by construction for positive finite ``k``.
Note: mathematical dilation admits any positive finite ``k``. Organ-level
semantic domain for *fraction decrease* is stricter (``0 < k < 1``) and is
enforced by ``_is_valid_fraction_decrease_scale`` before binding.
"""
k = float(scale)
if not np.isfinite(k) or k <= 0.0:
@ -929,6 +948,10 @@ def _versor_binding_from_scale_value(
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.
Callers that represent *fraction-decrease* organ admission must gate with
``_is_valid_fraction_decrease_scale`` first; this helper only enforces the
algebraic dilation precondition (positive finite scale).
"""
try:
k = float(scale_value) # Fraction implements __float__
@ -953,6 +976,10 @@ def _fraction_decrease_scale_binding(frame: ProblemFrame) -> VersorBinding | Non
2. Optional pack geometric_signature on the scale *surface* (named fractions
/ pack-ratified lemmas) when present still identity = surface token,
not a phrase regex.
Domain: only ``0 < scale < 1`` (finite) may produce a VersorBinding. This
matches ``assess_fraction_decrease`` / ``scale_out_of_range`` so geometric
admission cannot bypass the obligation semantic domain.
"""
relations = [
relation
@ -980,24 +1007,36 @@ def _fraction_decrease_scale_binding(frame: ProblemFrame) -> VersorBinding | Non
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
scale_value = getattr(scale_q, "value", scale_q)
# Prefer exact kernel scalar when inside the organ domain.
if _is_valid_fraction_decrease_scale(scale_value):
binding = _versor_binding_from_scale_value(
scale_value,
semantic_identity=str(surface),
source_span=source_span,
)
if binding is not None:
return binding
return None
# Grounded numeric scale outside (0, 1): fail closed. Do not let pack
# geometric_signature re-admit an out-of-range kernel fact.
try:
float(scale_value)
except (TypeError, ValueError):
pass
else:
return None
# Pack geometric_signature on scale surface (e.g. named fraction lemmas
# with senses geometric_signature) — substrate lookup only.
# with senses geometric_signature) — substrate lookup only; still domain-gated.
signature = resolve_geometric_signature(str(surface))
if signature is None:
return None
_, geom = signature
scale = _scale_from_geometric_signature(geom)
if scale is None:
if scale is None or not _is_valid_fraction_decrease_scale(scale):
return None
return _versor_binding_from_scale_value(
scale,

View file

@ -148,6 +148,16 @@ class TestConfuserRefusals:
assert resolve_promotable_fraction_decrease(FINAL_VALUE_CONFUSER) is None
assert _run(FINAL_VALUE_CONFUSER).answer is None
def test_scale_out_of_range_refuses_fraction_decrease(self):
"""Geometric path must not promote scale>1 (negative decrease)."""
case = (
"In one hour, the river will decrease to 5/4 of its level. "
"If the current level is 40 feet, what will the level decrease by?"
)
assert resolve_promotable_fraction_decrease(case) is None
assert compose_fraction_decrease(case) is None
assert _run(case).answer is None
def test_unequal_split_refuses_percent_partition(self):
assert resolve_promotable_percent_partition(UNEQUAL_SPLIT_CONFUSER) is None
assert _run(UNEQUAL_SPLIT_CONFUSER).answer is None

View file

@ -1,7 +1,15 @@
from fractions import Fraction
from pathlib import Path
import dataclasses
import pytest
from generate.derivation.fraction_decrease import resolve_promotable_fraction_decrease
from generate.problem_frame_builder import build_problem_frame
from generate.problem_frame_contracts import (
_fraction_decrease_scale_binding,
_is_valid_fraction_decrease_scale,
assess_contracts,
assess_fraction_decrease,
assess_geometric_proposals,
@ -177,6 +185,117 @@ def test_fraction_decrease_rejects_scale_greater_than_one() -> None:
assert "scale_out_of_range" in assessment.missing_bindings
def test_fraction_decrease_scale_domain_predicate() -> None:
"""Single shared domain: finite 0 < k < 1 for Fraction and float."""
assert _is_valid_fraction_decrease_scale(Fraction(3, 4))
assert _is_valid_fraction_decrease_scale(Fraction(2, 3))
assert _is_valid_fraction_decrease_scale(0.5)
assert not _is_valid_fraction_decrease_scale(Fraction(0, 4))
assert not _is_valid_fraction_decrease_scale(Fraction(4, 4))
assert not _is_valid_fraction_decrease_scale(Fraction(5, 4))
assert not _is_valid_fraction_decrease_scale(Fraction(-1, 4))
assert not _is_valid_fraction_decrease_scale(0.0)
assert not _is_valid_fraction_decrease_scale(1.0)
assert not _is_valid_fraction_decrease_scale(1.25)
assert not _is_valid_fraction_decrease_scale(-0.5)
assert not _is_valid_fraction_decrease_scale(float("nan"))
assert not _is_valid_fraction_decrease_scale(float("inf"))
assert not _is_valid_fraction_decrease_scale(float("-inf"))
assert not _is_valid_fraction_decrease_scale("not-a-number")
assert not _is_valid_fraction_decrease_scale(None)
@pytest.mark.parametrize(
"scale_surface",
["5/4", "4/4", "0/4"],
ids=["gt_one", "one", "zero"],
)
def test_fraction_decrease_geometric_refuses_out_of_range_scale(scale_surface: str) -> None:
"""Geometric admission must agree with obligation scale_out_of_range (no bypass)."""
case = (
f"In one hour, the river will decrease to {scale_surface} of its level. "
"If the current level is 40 feet, what will the level decrease by?"
)
frame = build_problem_frame(case)
obligation = assess_fraction_decrease(frame)
assert not obligation.runnable
assert "scale_out_of_range" in obligation.missing_bindings
assert obligation.bindings == () or not obligation.bindings
geom = [
a
for a in assess_geometric_proposals(frame)
if a.candidate_organ == "fraction_decrease"
]
assert geom, "expected a geometric fraction_decrease proposal assessment"
for assessment in geom:
assert not assessment.runnable
assert not assessment.bindings
assert _fraction_decrease_scale_binding(frame) is None
assert resolve_promotable_fraction_decrease(case) is None
def test_fraction_decrease_scale_gt_one_live_repro_refuses() -> None:
"""Audit repro: 5/4 must not promote a negative decrease answer."""
case = (
"In one hour, the river will decrease to 5/4 of its level. "
"If the current level is 40 feet, what will the level decrease by?"
)
assert resolve_promotable_fraction_decrease(case) is None
frame = build_problem_frame(case)
obligation = assess_fraction_decrease(frame)
assert "scale_out_of_range" in obligation.missing_bindings
geom = assess_geometric_proposals(frame)
assert all(
not (a.candidate_organ == "fraction_decrease" and a.bindings)
for a in geom
)
def test_fraction_decrease_rejects_injected_negative_zero_one_and_gt_one_scale() -> None:
"""Domain gate on mutated Fraction scales (GroundedScalar is Fraction-only)."""
case = (
"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?"
)
base_frame = build_problem_frame(case)
assert assess_fraction_decrease(base_frame).runnable
# Locate scale quantity fact and replace value.
relation = next(
r for r in base_frame.bound_relations if r.relation_type == "decrease_to_fraction"
)
scale_id = next(role.target_id for role in relation.roles if role.role == "scale")
mention = next(m for m in base_frame.mentions if m.mention_id == scale_id)
fact_id = mention.fact_id
assert fact_id is not None
for bad_value in (
Fraction(-1, 4),
Fraction(0, 1),
Fraction(1, 1),
Fraction(5, 4),
):
new_quantities = []
for q in base_frame.quantities:
if q.fact_id == fact_id:
new_quantities.append(dataclasses.replace(q, value=bad_value))
else:
new_quantities.append(q)
frame = dataclasses.replace(base_frame, quantities=tuple(new_quantities))
assessment = assess_fraction_decrease(frame)
assert not assessment.runnable, bad_value
assert "scale_out_of_range" in assessment.missing_bindings, bad_value
assert _fraction_decrease_scale_binding(frame) is None, bad_value
geom = assess_geometric_proposals(frame)
assert all(
not (a.candidate_organ == "fraction_decrease" and a.bindings)
for a in geom
), bad_value
def test_multi_base_candidate_is_refused() -> None:
import dataclasses
case = (

View file

@ -236,22 +236,19 @@ def test_adversarial_refuse_closed(case: AblationCase) -> None:
assert result.answer is None
def test_preexisting_scale_out_of_range_gap_isolated() -> None:
"""Document pre-existing Gate A2k gap: scale>1 may still admit geometrically.
def test_scale_out_of_range_operator_and_baseline_refuse() -> None:
"""Gate A2k: scale>1 is fail-closed on both baseline and geometric operator.
Frame assess_fraction_decrease marks scale_out_of_range (baseline refuses).
Geometric A2k path may still compute a negative delta not introduced by
this ablation. Do not mask; pin the split so a future organ fix flips this.
Formerly pinned a pre-existing geometric bypass (negative delta). Obligation
and geometric admission now share 0 < scale < 1, so operator must refuse.
"""
base = run_baseline(SCALE_OOR_PREEXISTING)
op = run_operator(SCALE_OOR_PREEXISTING)
assert base.outcome == "refused"
assert base.answer is None
# Pre-existing: operator may wrong-commit. If a future PR fixes A2k, this
# assertion should be updated to require refuse.
assert op.outcome in {"refused", "wrong"}
if op.outcome == "wrong":
assert op.answer is not None
assert op.outcome == "refused"
assert op.answer is None
assert op.operator_bindings == 0
def test_malformed_depth_labels_cannot_elevate_runnable() -> None: