core/tests/test_inner_loop_admissibility.py
Shay 7fccf368fb feat(adr-0024): Phase 1 — wire inner-loop admissibility + determinism proof
Phase 1 of the post-ADR-0024 sequence: wire the inner-loop flag into live
cognition paths and prove deterministic-when-wired in the same milestone.

Changes:
- RuntimeConfig: add inner_loop_admissibility + admissibility_threshold.
- ChatRuntime: pass both into generate() on the chat hot path.
- CLI: --inner-loop-admissibility / --admissibility-threshold flags.
- vocab/manifold.py: document strict `>` tie-break as load-bearing for
  ADR-0024 rejected_attempts ordering (determinism by construction, not
  by accident).
- tests/test_inner_loop_admissibility.py: three new determinism tests —
  identical rejected_attempts across 5 runs, identical trace hash across
  5 runs (non-empty), and legacy hash equivalence when no rejections
  occur (flag on/off byte-identical).
- tests/test_language_pack_cache.py: fix stale fixture (en-core-cog-070
  -> en-core-cog-085 after pack growth).

Suite: 995 passed, 0 failed, 2 skipped.

Acceptance criteria met:
- wired through RuntimeConfig + CLI + ChatRuntime + generate()
- deterministic rejected_attempts sequence (verified by repetition)
- deterministic trace hash under inner_loop=True
- legacy ADR-0023 trace hashes preserved when no rejections
- nearest_next determinism is by construction (sequenced iteration +
  strict > tie-break), now documented

Next: Phase 2 — corpus-observation eval on existing v1 corpus with the
four-condition matrix (boundary-only, null control, inner-loop t=0.0,
inner-loop t>0) and exhaustion_rate + latency metrics.
2026-05-17 13:38:55 -07:00

332 lines
12 KiB
Python

"""ADR-0024 — inner-loop per-rotor admissibility tests.
These tests exercise ``generate()`` with a stub vocab so we can
deterministically control selection order and the CGA inner-product
score of each candidate against the region's relation blade. They
prove four properties stated in the ADR:
1. Default ``inner_loop_admissibility=False`` preserves ADR-0023
boundary-only behavior (no ``rejected_attempts`` recorded).
2. With the flag on, a candidate whose verdict is *not* admitted is
skipped and the next admitted candidate is selected; the
rejected one shows up in the step's ``rejected_attempts``.
3. Exhaustion (every admissible candidate rejected) raises
``ValueError`` with the region label embedded — the same shape
ADR-0022 §2 commits to for empty admissible sets.
4. Empty ``rejected_attempts`` is *not* folded into
``AdmissibilityTraceStep.canonical()``, so the trace hash stays
byte-identical with ADR-0023 turns.
"""
from __future__ import annotations
import numpy as np
import pytest
from field.state import FieldState
from generate.admissibility import (
AdmissibilityRegion,
AdmissibilityTraceStep,
AdmissibilityVerdict,
RegionSource,
)
from generate.result import GenerationResult
from generate.stream import generate
def _basis_versor(weight_index: int) -> np.ndarray:
"""A 32-component versor with a single non-zero component.
``cga_inner`` is a dot-product over the 32-dim CGA basis (with the
metric applied in algebra.cga), so two basis-aligned versors yield
a positive score iff they share their non-zero component's grade
sign under the metric. We pick component 1 (e1, spatial, metric
``+1``) so the scores are predictable.
"""
v = np.zeros(32, dtype=np.float32)
v[1] = 1.0 # e1 — metric +1
v[1] *= float(weight_index)
return v
class _ControllableVocab:
"""Stub vocab whose ``nearest`` returns candidates in a fixed
preference order so the test controls which one the walk sees
first.
Each word has a basis-aligned versor whose CGA inner product with
the test region's blade we can predict deterministically.
"""
def __init__(self, *, words: list[str], preference: list[int],
versor_signs: list[float]) -> None:
assert len(words) == len(versor_signs)
self._words = words
self._preference = preference
# Build per-word versors. versor_signs[i] picks the sign on
# the e1 component; positive ⇒ admitted vs a +e1 region blade,
# negative ⇒ rejected.
self._versors: list[np.ndarray] = []
for sign in versor_signs:
v = np.zeros(32, dtype=np.float32)
v[1] = float(sign)
self._versors.append(v)
def __len__(self) -> int:
return len(self._words)
def nearest(self, F, exclude_idx: int = -1, exclude_indices=None,
candidate_indices=None):
blocked = set(exclude_indices or ())
if candidate_indices is not None:
allowed = {int(i) for i in candidate_indices}
else:
allowed = set(range(len(self._words)))
for idx in self._preference:
if idx == exclude_idx or idx in blocked or idx not in allowed:
continue
return self._words[idx], idx
raise ValueError("No candidate word available after exclusions.")
def get_versor_at(self, idx: int) -> np.ndarray:
return self._versors[idx]
def index_of(self, word: str) -> int:
try:
return self._words.index(word)
except ValueError: # pragma: no cover
raise KeyError(word)
class _IdentityPersona:
def apply(self, F: np.ndarray) -> np.ndarray:
return F
def _initial_state(vocab: _ControllableVocab) -> FieldState:
"""Field state seeded with a +e1 versor so the rotor application
is well-defined; we only care about selection in these tests."""
F = np.zeros(32, dtype=np.float32)
F[1] = 1.0
return FieldState(
F=F,
node=0,
step=0,
)
def _positive_blade_region(allowed: tuple[int, ...]) -> AdmissibilityRegion:
"""Region whose blade scores positively for +e1 versors and
negatively for -e1 versors, with the given admissible token set."""
blade = np.zeros(32, dtype=np.float32)
blade[1] = 1.0
return AdmissibilityRegion(
allowed_indices=np.asarray(allowed, dtype=np.int64),
relation_blade=blade,
source=RegionSource.RELATION,
label="adr0024-test",
)
class TestDefaultOffPreservesBehavior:
def test_no_rejected_attempts_when_flag_off(self) -> None:
# The "preferred" word has a -e1 versor (verdict rejected).
# With inner-loop OFF, the walk still emits it; the trace
# records the rejected verdict but no rejected_attempts.
vocab = _ControllableVocab(
words=["seed", "alpha", "beta"],
preference=[1, 2],
versor_signs=[+1.0, -1.0, +1.0],
)
result = generate(
_initial_state(vocab),
vocab,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2)),
inner_loop_admissibility=False,
)
assert result.tokens == ("alpha",)
assert len(result.admissibility_trace) == 1
step = result.admissibility_trace[0]
assert step.rejected_attempts == ()
# Boundary-only path: verdict still computed and recorded.
assert step.verdict.admitted is False
class TestInnerLoopRejectionDrivesReselection:
def test_rejected_candidate_skipped(self) -> None:
# Preferred order is [alpha (rejected), beta (admitted)].
# Inner loop should skip alpha and emit beta.
vocab = _ControllableVocab(
words=["seed", "alpha", "beta"],
preference=[1, 2],
versor_signs=[+1.0, -1.0, +1.0],
)
result = generate(
_initial_state(vocab),
vocab,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2)),
inner_loop_admissibility=True,
)
assert result.tokens == ("beta",)
step = result.admissibility_trace[0]
assert step.selected_index == 2
assert step.selected_word == "beta"
assert step.verdict.admitted is True
# The rejected alpha should be in rejected_attempts with its
# negative score recorded.
assert len(step.rejected_attempts) == 1
idx, word, score = step.rejected_attempts[0]
assert (idx, word) == (1, "alpha")
assert score < 0.0
class TestExhaustionRaisesHonestRefusal:
def test_all_rejected_raises_value_error(self) -> None:
# Both admissible candidates have -e1 versors so the region
# rejects them both; inner loop must raise ValueError.
vocab = _ControllableVocab(
words=["seed", "alpha", "beta"],
preference=[1, 2],
versor_signs=[+1.0, -1.0, -1.0],
)
with pytest.raises(ValueError, match="adr0024-test"):
generate(
_initial_state(vocab),
vocab,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2)),
inner_loop_admissibility=True,
)
class TestCanonicalOmitsEmptyRejectedAttempts:
def test_empty_rejected_attempts_not_in_canonical(self) -> None:
# Construct a step with default (empty) rejected_attempts and
# verify the canonical form does not include the new key —
# this is what keeps ADR-0023 turn hashes byte-identical.
step = AdmissibilityTraceStep(
step_index=0,
region_label="r",
region_source="relation",
candidates_before=(1, 2),
candidates_after=(1, 2),
selected_index=1,
selected_word="x",
verdict=AdmissibilityVerdict(
admitted=True, score=0.0, region_label="r", reason="ok"
),
)
canonical = step.canonical()
assert "rejected_attempts" not in canonical
def test_non_empty_rejected_attempts_in_canonical(self) -> None:
step = AdmissibilityTraceStep(
step_index=0,
region_label="r",
region_source="relation",
candidates_before=(1, 2),
candidates_after=(1, 2),
selected_index=2,
selected_word="y",
verdict=AdmissibilityVerdict(
admitted=True, score=0.5, region_label="r", reason="ok"
),
rejected_attempts=((1, "x", -0.25),),
)
canonical = step.canonical()
assert canonical["rejected_attempts"] == [[1, "x", -0.25]]
class TestInnerLoopDeterminism:
"""Phase 1 acceptance criterion (ADR-0024 follow-up).
The inner-loop re-selection introduces a new ordering-sensitive
path: candidates excluded by rejection feed back into the next
``vocab.nearest`` call. Determinism here relies on:
* ``vocab.nearest`` using a strict ``>`` tie-break over a
sequenced iteration (load-bearing comment in
``vocab/manifold.py``);
* ``step_exclude`` being used only for set membership, never
iterated;
* ``rejected_attempts`` being appended in loop order.
These tests pin determinism *by repetition* — same inputs, same
output across N runs, with non-empty rejection sequences in scope.
"""
def _run(self) -> GenerationResult:
vocab = _ControllableVocab(
words=["seed", "alpha", "beta", "gamma"],
preference=[1, 2, 3],
versor_signs=[+1.0, -1.0, -1.0, +1.0],
)
return generate(
_initial_state(vocab),
vocab,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2, 3)),
inner_loop_admissibility=True,
)
def test_repeated_runs_produce_identical_rejected_attempts(self) -> None:
results = [self._run() for _ in range(5)]
baseline = results[0].admissibility_trace[0].rejected_attempts
# Two rejections precede the admitted selection.
assert len(baseline) == 2
for result in results[1:]:
step = result.admissibility_trace[0]
assert step.rejected_attempts == baseline
assert step.selected_word == "gamma"
def test_repeated_runs_produce_identical_trace_hash(self) -> None:
from core.cognition.trace import hash_admissibility_trace
hashes = {
hash_admissibility_trace(self._run().admissibility_trace)
for _ in range(5)
}
assert len(hashes) == 1
only_hash = next(iter(hashes))
assert only_hash != "" # non-empty trace ⇒ non-empty hash
def test_inner_loop_off_preserves_legacy_trace_hash(self) -> None:
"""No rejections ⇒ canonical omits the new key ⇒ hash is byte-
identical to what an ADR-0023 turn would have produced."""
from core.cognition.trace import hash_admissibility_trace
vocab_off = _ControllableVocab(
words=["seed", "alpha", "beta"],
preference=[1, 2],
versor_signs=[+1.0, +1.0, +1.0],
)
result_off = generate(
_initial_state(vocab_off),
vocab_off,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2)),
inner_loop_admissibility=False,
)
result_on = generate(
_initial_state(vocab_off),
vocab_off,
_IdentityPersona(),
max_tokens=1,
region=_positive_blade_region((1, 2)),
inner_loop_admissibility=True,
)
# No rejections in either run ⇒ traces hash identically.
h_off = hash_admissibility_trace(result_off.admissibility_trace)
h_on = hash_admissibility_trace(result_on.admissibility_trace)
assert h_off == h_on
if __name__ == "__main__": # pragma: no cover
pytest.main([__file__, "-v"])