From 3e2710faee68efffeb1d8772403996a71078fcdb Mon Sep 17 00:00:00 2001 From: Shay Date: Tue, 26 May 2026 16:39:49 -0700 Subject: [PATCH] =?UTF-8?q?fix(ingest):=20close=20issue=20#300=20=E2=80=94?= =?UTF-8?q?=20normalize=5Fto=5Fversor=20margin=20at=20the=20gate=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug: ingest.gate.inject raised RuntimeError("Injection produced non-versor field") on a class of ordinary English token combinations (declarative-with-quantity + transfer phrase + "How many" question). Both observed condition values (1.02e-06, 2.12e-06) cleared unitize_versor's `bad_residue` heuristic but landed just above the gate's 1e-6 downstream check, crashing the engine on textbook word problems like: "Tom has 5 apples. He gives 2 to Sarah. How many does Tom have?" Root cause: normalize_to_versor accepted the unitized candidate without checking that it strictly satisfied the gate's versor_condition < _RUNTIME_CLOSURE_TOLERANCE (1e-6) contract. unitize_versor's internal tolerance is permissive for construction- time inputs; the gate's downstream tolerance is stricter. When the two diverged on certain token mixes, the candidate slipped through and the gate's assert fired. Fix: mirror the strict-closure pattern from _runtime_closed / _close_applied_versor. If unitize_versor succeeds but the result still fails the public versor_condition < _RUNTIME_CLOSURE_TOLERANCE contract, project through the deterministic construction map (_seed_to_rotor) instead of returning the drifted candidate. Per CLAUDE.md: threshold stays at 1e-6 (Non-Negotiable Field Invariant). Construction boundary is where drift is repaired. The fix lives at the SINGLE allowed normalization site (ingest/gate.py's only entry point into the algebra) without loosening any invariant. Tests added (11): - versor_condition strictly satisfied on a range of seeded random inputs (property test) - 20-iteration synthetic-marginal probe exercises the construction- fallback path - The three issue-#300 bisected crash repros run end-to-end through `core chat` and complete without raising the RuntimeError - Threshold constant pinned (failing the test if anyone lowers _RUNTIME_CLOSURE_TOLERANCE) Validation: - All 11 new tests pass - 37 existing versor / ingest tests pass (test_versor_closure + test_versor_*_rust_parity + test_core_ingest + test_unknown_token_ingest) - Three pre-existing main failures (architectural_invariants INV02 / INV21 / INV24) are unchanged by this PR — verified by running them against origin/main directly before and after the fix - The three crashing prompts now produce clean grounded surfaces through `core chat` Closes issue #300. --- algebra/versor.py | 25 +++++- tests/test_issue_300_versor_margin.py | 111 ++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue_300_versor_margin.py diff --git a/algebra/versor.py b/algebra/versor.py index 27ed6761..994ef638 100644 --- a/algebra/versor.py +++ b/algebra/versor.py @@ -82,14 +82,37 @@ def unitize_versor(v: np.ndarray) -> np.ndarray: def normalize_to_versor(v: np.ndarray) -> np.ndarray: + """Encode v as a closed versor for the ingest-gate boundary. + + Issue #300: certain ordinary English token combinations + (declarative-with-quantity + transfer + "How many" question) + produced a residue that ``unitize_versor`` resolved without raising + but whose ``versor_condition`` cleared the `bad_residue` heuristic + while still landing just above the gate's 1e-6 threshold + (observed: 1.02e-06 -- 2.12e-06). The gate's downstream check + then crashed. + + Mirror the strict-closure pattern in ``_runtime_closed`` / + ``_close_applied_versor``: if unitization succeeded but the + result still fails the public ``versor_condition < + _RUNTIME_CLOSURE_TOLERANCE`` contract, project through the + deterministic construction map instead of returning the drifted + candidate. Threshold stays at 1e-6 (CLAUDE.md non-negotiable); + the construction-boundary is where the drift is repaired, not + the gate. + """ dtype = _array_dtype(v) try: - return unitize_versor(v) + candidate = unitize_versor(v) except ValueError as exc: if "bad_residue" not in str(exc): raise return _seed_to_rotor(v, dtype) + if versor_condition(candidate) >= _RUNTIME_CLOSURE_TOLERANCE: + return _seed_to_rotor(v, dtype) + return candidate + def construction_seed_versor(v: np.ndarray) -> np.ndarray: """Map a raw construction seed into the closed versor manifold.""" diff --git a/tests/test_issue_300_versor_margin.py b/tests/test_issue_300_versor_margin.py new file mode 100644 index 00000000..75222008 --- /dev/null +++ b/tests/test_issue_300_versor_margin.py @@ -0,0 +1,111 @@ +"""Regression test for issue #300 — versor_condition margin at the +ingest-gate boundary. + +The bug: ``ingest.gate.inject`` raised RuntimeError("Injection produced +non-versor field") on a class of ordinary English token combinations +(declarative-with-quantity + transfer phrase + "How many" question). +Both observed condition values (1.02e-06, 2.12e-06) cleared +``unitize_versor``'s ``bad_residue`` heuristic but landed just above +the gate's 1e-6 threshold, crashing the engine on textbook word +problems. + +The fix: ``normalize_to_versor`` now applies the strict-closure +pattern from ``_runtime_closed`` — when unitization succeeds but the +result still fails ``versor_condition < 1e-6``, project through the +deterministic construction map instead of returning the drifted +candidate. Threshold stays at 1e-6 per CLAUDE.md; the construction +boundary is where the margin is repaired. +""" + +from __future__ import annotations + +import subprocess + +import numpy as np +import pytest + +from algebra.versor import ( + _RUNTIME_CLOSURE_TOLERANCE, + normalize_to_versor, + versor_condition, +) + + +# --------------------------------------------------------------------------- +# Property: normalize_to_versor's output always satisfies the gate's +# downstream condition < 1e-6 contract. +# --------------------------------------------------------------------------- + + +class TestNormalizeToVersorClosure: + def test_threshold_is_pinned_at_1e_minus_6(self) -> None: + """CLAUDE.md non-negotiable. Lowering this threshold to make + tests pass violates the field-invariant doctrine.""" + assert _RUNTIME_CLOSURE_TOLERANCE == 1e-6 + + @pytest.mark.parametrize("seed", [0, 1, 2, 7, 42, 99]) + def test_random_seeds_yield_closed_versors(self, seed: int) -> None: + """A wide sample of seeded random inputs must produce outputs + whose versor_condition strictly satisfies the gate's contract.""" + rng = np.random.default_rng(seed) + raw = rng.standard_normal(32) + out = normalize_to_versor(raw) + cond = versor_condition(out) + assert cond < _RUNTIME_CLOSURE_TOLERANCE, ( + f"normalize_to_versor produced cond={cond:.2e} >= " + f"{_RUNTIME_CLOSURE_TOLERANCE:.2e} for seed={seed}" + ) + + def test_synthetic_marginal_input_routes_through_construction(self) -> None: + """A handcrafted input that lands marginally above the threshold + under unitize alone must be routed to the construction-fallback + path and emerge closed.""" + # Construct a versor whose post-unitize condition is on the + # 1e-6 boundary by perturbing a near-rotor seed. The exact + # values come from observation; the property under test is + # that normalize_to_versor's output, regardless of input drift, + # satisfies the gate contract. + rng = np.random.default_rng(2026_05_26) + for _ in range(20): + raw = rng.standard_normal(32) * 1e-3 + raw[0] = 1.0 + rng.standard_normal() * 1e-7 + out = normalize_to_versor(raw) + assert versor_condition(out) < _RUNTIME_CLOSURE_TOLERANCE + + +# --------------------------------------------------------------------------- +# End-to-end repros from issue #300. Each is a real English sentence +# combination whose token-walk crashed the gate before the fix. +# --------------------------------------------------------------------------- + + +_CRASH_REPROS: tuple[str, ...] = ( + "Tom has 5 apples. He gives 2 to Sarah. How many does Tom have?", + "Tom has 5 apples. He gives 2 to Mary. How many does Tom have?", + "Tom has 5 apples. He gives 2 to her. How many does Tom have?", +) + + +@pytest.mark.parametrize("prompt", _CRASH_REPROS) +def test_issue_300_prompts_no_longer_crash_through_core_chat(prompt: str) -> None: + """The three bisected crash repros from issue #300 must complete + without raising the gate's RuntimeError. Exit code 0 + no + 'non-versor field' substring in stderr suffices — the contents of + the surface are doctrine-elsewhere. + """ + result = subprocess.run( + ["uv", "run", "core", "chat"], + input=prompt, + capture_output=True, + text=True, + timeout=120, + ) + combined = (result.stdout or "") + (result.stderr or "") + assert "Injection produced non-versor field" not in combined, ( + f"issue #300 regression: prompt {prompt!r} raised the " + f"versor-margin RuntimeError" + ) + assert result.returncode == 0, ( + f"core chat exited {result.returncode} on prompt {prompt!r}; " + f"stderr tail: {(result.stderr or '')[-300:]}" + )