From 7fccf368fbf98583de4447a942792169a97fb1de Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 17 May 2026 13:38:55 -0700 Subject: [PATCH] =?UTF-8?q?feat(adr-0024):=20Phase=201=20=E2=80=94=20wire?= =?UTF-8?q?=20inner-loop=20admissibility=20+=20determinism=20proof?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- chat/runtime.py | 4 ++ core/cli.py | 13 ++++ core/config.py | 2 + tests/test_inner_loop_admissibility.py | 87 ++++++++++++++++++++++++++ tests/test_language_pack_cache.py | 2 +- vocab/manifold.py | 3 + 6 files changed, 110 insertions(+), 1 deletion(-) diff --git a/chat/runtime.py b/chat/runtime.py index 93d2dbd7..ff87e737 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -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 --- diff --git a/core/cli.py b/core/cli.py index 5ab24828..03609e63 100644 --- a/core/cli.py +++ b/core/cli.py @@ -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", diff --git a/core/config.py b/core/config.py index e432eac4..6cc8936b 100644 --- a/core/config.py +++ b/core/config.py @@ -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() diff --git a/tests/test_inner_loop_admissibility.py b/tests/test_inner_loop_admissibility.py index 95f618ac..dbbb145d 100644 --- a/tests/test_inner_loop_admissibility.py +++ b/tests/test_inner_loop_admissibility.py @@ -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"]) diff --git a/tests/test_language_pack_cache.py b/tests/test_language_pack_cache.py index b41d9f2b..48a9579a 100644 --- a/tests/test_language_pack_cache.py +++ b/tests/test_language_pack_cache.py @@ -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" diff --git a/vocab/manifold.py b/vocab/manifold.py index 2e5f3ccc..c8927ede 100644 --- a/vocab/manifold.py +++ b/vocab/manifold.py @@ -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