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.
This commit is contained in:
Shay 2026-05-17 13:38:55 -07:00
parent f0dbe9a57c
commit 7fccf368fb
6 changed files with 110 additions and 1 deletions

View file

@ -182,6 +182,8 @@ class ChatRuntime:
use_salience=config.use_salience,
salience_top_k=config.salience_top_k,
inhibition_threshold=config.inhibition_threshold,
inner_loop_admissibility=config.inner_loop_admissibility,
admissibility_threshold=config.admissibility_threshold,
)
else:
resolved_config = config
@ -396,6 +398,8 @@ class ChatRuntime:
use_salience=self.config.use_salience,
salience_top_k=self.config.salience_top_k,
inhibition_threshold=self.config.inhibition_threshold,
inner_loop_admissibility=self.config.inner_loop_admissibility,
admissibility_threshold=self.config.admissibility_threshold,
)
# --- Articulation fidelity: replace bare S-P-O join with intent-aware surface ---

View file

@ -134,6 +134,8 @@ def _runtime_config_from_args(args: argparse.Namespace):
use_salience=not args.no_salience,
salience_top_k=args.salience_top_k,
inhibition_threshold=args.inhibition_threshold,
inner_loop_admissibility=getattr(args, "inner_loop_admissibility", False),
admissibility_threshold=getattr(args, "admissibility_threshold", 0.0),
)
@ -684,6 +686,17 @@ def _add_runtime_policy_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--vault-reproject-interval", type=int, default=20, help="vault null-cone reprojection cadence; default: 20 stores")
parser.add_argument("--salience-top-k", type=int, default=16, help="salience candidate budget; default: 16")
parser.add_argument("--inhibition-threshold", type=float, default=0.3, help="attention inhibition threshold; default: 0.3")
parser.add_argument(
"--inner-loop-admissibility",
action="store_true",
help="enable ADR-0024 per-rotor inner-loop admissibility (re-select on rejection)",
)
parser.add_argument(
"--admissibility-threshold",
type=float,
default=0.0,
help="inner-loop admissibility score threshold; default: 0.0",
)
parser.add_argument("--no-salience", action="store_true", help="disable salience attention and use full-manifold generation")
parser.add_argument(
"--allow-cross-language-generation",

View file

@ -20,6 +20,8 @@ class RuntimeConfig:
use_salience: bool = True
salience_top_k: int = 16
inhibition_threshold: float = 0.3
inner_loop_admissibility: bool = False
admissibility_threshold: float = 0.0
DEFAULT_CONFIG = RuntimeConfig()

View file

@ -30,6 +30,7 @@ from generate.admissibility import (
AdmissibilityVerdict,
RegionSource,
)
from generate.result import GenerationResult
from generate.stream import generate
@ -241,5 +242,91 @@ class TestCanonicalOmitsEmptyRejectedAttempts:
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"])

View file

@ -37,4 +37,4 @@ def test_load_pack_entries_returns_new_list_from_cached_tuple() -> None:
entries_a.pop()
assert len(entries_a) == len(entries_b) - 1
assert entries_b[-1].entry_id == "en-core-cog-070"
assert entries_b[-1].entry_id == "en-core-cog-085"

View file

@ -262,6 +262,9 @@ class VocabManifold:
best_score = -np.inf
best_idx = -1
# Strict `>` is load-bearing: on ties, the first candidate in iteration
# order wins. ADR-0024 inner-loop re-selection relies on this for
# deterministic rejected_attempts ordering across runs.
for i in candidates:
if i in blocked:
continue