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:
parent
d22608ddcb
commit
3e2710faee
2 changed files with 135 additions and 1 deletions
|
|
@ -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."""
|
||||||
|
|
|
||||||
111
tests/test_issue_300_versor_margin.py
Normal file
111
tests/test_issue_300_versor_margin.py
Normal 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:]}"
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue