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:
parent
f0dbe9a57c
commit
7fccf368fb
6 changed files with 110 additions and 1 deletions
|
|
@ -182,6 +182,8 @@ class ChatRuntime:
|
||||||
use_salience=config.use_salience,
|
use_salience=config.use_salience,
|
||||||
salience_top_k=config.salience_top_k,
|
salience_top_k=config.salience_top_k,
|
||||||
inhibition_threshold=config.inhibition_threshold,
|
inhibition_threshold=config.inhibition_threshold,
|
||||||
|
inner_loop_admissibility=config.inner_loop_admissibility,
|
||||||
|
admissibility_threshold=config.admissibility_threshold,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
resolved_config = config
|
resolved_config = config
|
||||||
|
|
@ -396,6 +398,8 @@ class ChatRuntime:
|
||||||
use_salience=self.config.use_salience,
|
use_salience=self.config.use_salience,
|
||||||
salience_top_k=self.config.salience_top_k,
|
salience_top_k=self.config.salience_top_k,
|
||||||
inhibition_threshold=self.config.inhibition_threshold,
|
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 ---
|
# --- Articulation fidelity: replace bare S-P-O join with intent-aware surface ---
|
||||||
|
|
|
||||||
13
core/cli.py
13
core/cli.py
|
|
@ -134,6 +134,8 @@ def _runtime_config_from_args(args: argparse.Namespace):
|
||||||
use_salience=not args.no_salience,
|
use_salience=not args.no_salience,
|
||||||
salience_top_k=args.salience_top_k,
|
salience_top_k=args.salience_top_k,
|
||||||
inhibition_threshold=args.inhibition_threshold,
|
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("--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("--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("--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("--no-salience", action="store_true", help="disable salience attention and use full-manifold generation")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--allow-cross-language-generation",
|
"--allow-cross-language-generation",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ class RuntimeConfig:
|
||||||
use_salience: bool = True
|
use_salience: bool = True
|
||||||
salience_top_k: int = 16
|
salience_top_k: int = 16
|
||||||
inhibition_threshold: float = 0.3
|
inhibition_threshold: float = 0.3
|
||||||
|
inner_loop_admissibility: bool = False
|
||||||
|
admissibility_threshold: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = RuntimeConfig()
|
DEFAULT_CONFIG = RuntimeConfig()
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ from generate.admissibility import (
|
||||||
AdmissibilityVerdict,
|
AdmissibilityVerdict,
|
||||||
RegionSource,
|
RegionSource,
|
||||||
)
|
)
|
||||||
|
from generate.result import GenerationResult
|
||||||
from generate.stream import generate
|
from generate.stream import generate
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -241,5 +242,91 @@ class TestCanonicalOmitsEmptyRejectedAttempts:
|
||||||
assert canonical["rejected_attempts"] == [[1, "x", -0.25]]
|
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
|
if __name__ == "__main__": # pragma: no cover
|
||||||
pytest.main([__file__, "-v"])
|
pytest.main([__file__, "-v"])
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,4 @@ def test_load_pack_entries_returns_new_list_from_cached_tuple() -> None:
|
||||||
entries_a.pop()
|
entries_a.pop()
|
||||||
|
|
||||||
assert len(entries_a) == len(entries_b) - 1
|
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"
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,9 @@ class VocabManifold:
|
||||||
|
|
||||||
best_score = -np.inf
|
best_score = -np.inf
|
||||||
best_idx = -1
|
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:
|
for i in candidates:
|
||||||
if i in blocked:
|
if i in blocked:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue