fix(ingest): close issue #300 — normalize_to_versor margin at the gate (#312)

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.
This commit is contained in:
Shay 2026-05-26 16:39:49 -07:00 committed by GitHub
parent d22608ddcb
commit 3e2710faee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 135 additions and 1 deletions

View file

@ -82,14 +82,37 @@ def unitize_versor(v: np.ndarray) -> np.ndarray:
def normalize_to_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) dtype = _array_dtype(v)
try: try:
return unitize_versor(v) candidate = unitize_versor(v)
except ValueError as exc: except ValueError as exc:
if "bad_residue" not in str(exc): if "bad_residue" not in str(exc):
raise raise
return _seed_to_rotor(v, dtype) 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: def construction_seed_versor(v: np.ndarray) -> np.ndarray:
"""Map a raw construction seed into the closed versor manifold.""" """Map a raw construction seed into the closed versor manifold."""

View file

@ -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:]}"
)