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:
|
||||
"""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."""
|
||||
|
|
|
|||
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