core/tests/test_admissibility_trace.py
Shay c504796165 feat(adr-0023): Forward Semantic Control proof evidence — Accepted
Extends ADR-0022 with inspection/telemetry surfaces that turn the
forward-semantic-control claim from "mechanism exists" into "mechanism
is causally load-bearing, isolated, and replayable."

Changes (zero runtime semantics change beyond a pipeline bug fix):

- AdmissibilityTraceStep + GenerationResult.admissibility_trace —
  per-transition record of region label, candidates before/after,
  selected destination, and the typed AdmissibilityVerdict.
- ChatResponse + CognitiveTurnResult expose admissibility_trace,
  admissibility_trace_hash, ratification_outcome,
  region_was_unconstrained.
- hash_admissibility_trace + compute_trace_hash fold the new fields
  only when they carry non-default values, so pre-ADR-0023 turn
  hashes remain byte-preserved.
- Same-path ablation leg in evals/forward_semantic_control/runner.py:
  generate(..., region=None) vs generate(..., region=R) on the same
  runtime/vocab/field/persona/prompt — isolates the region as cause.
- Lane expansion: 8 dev cases across 4 relation axes (cause, means,
  precedes, part_of) including 2 adversarial distractor cases.
- Lane metrics now report region_only_constrained_rate /
  region_only_gap / ratified_rate / demoted_rate / passthrough_rate /
  passthrough_on_scored.
- Bug fix surfaced by the new accounting: _ratify_intent looked up
  runtime.vocab (always None) instead of runtime.session.vocab —
  every production turn was silently PASSTHROUGH. Fixed; ratifier
  now actually gates intent classification.
- tests/test_admissibility_trace.py: hash determinism +
  pre-ADR-0023 byte-preservation tests.

Lane evidence (dev, 8 cases):
- constrained_pass_rate=0.80, causality_gap=0.80
- region_only_gap=1.00 (5/5 with region, 0/5 without — same path)
- ratified_rate=1.00, passthrough_on_scored=false
- overall_pass=true

Bench: 9.41s / 20 turns (~470ms/turn), well inside the +5% budget.

Full pytest: 922 passed, 1 pre-existing failure
(test_language_pack_cache, unrelated to ADR-0023).
2026-05-17 12:55:19 -07:00

161 lines
5.5 KiB
Python

"""ADR-0023 — admissibility trace + trace-hash determinism tests.
Pure-unit checks on the trace surface introduced by ADR-0023. No
runtime, no pipeline; just the typed dataclasses and the hashing
helpers in ``core.cognition.trace``.
"""
from __future__ import annotations
import numpy as np
import pytest
from core.cognition.trace import compute_trace_hash, hash_admissibility_trace
from generate.admissibility import (
AdmissibilityRegion,
AdmissibilityTraceStep,
AdmissibilityVerdict,
check_transition,
region_from_relation_chain,
)
def _step(
*,
step_index: int = 0,
region_label: str = "region",
region_source: str = "relation",
candidates_before: tuple[int, ...] = (1, 2, 3),
candidates_after: tuple[int, ...] = (2, 3),
selected_index: int = 2,
selected_word: str = "beta",
admitted: bool = True,
reason: str = "ok",
) -> AdmissibilityTraceStep:
return AdmissibilityTraceStep(
step_index=step_index,
region_label=region_label,
region_source=region_source,
candidates_before=candidates_before,
candidates_after=candidates_after,
selected_index=selected_index,
selected_word=selected_word,
verdict=AdmissibilityVerdict(
admitted=admitted,
score=0.42,
region_label=region_label,
reason=reason,
),
)
class TestHashAdmissibilityTrace:
def test_empty_trace_returns_empty_string(self) -> None:
assert hash_admissibility_trace(()) == ""
def test_same_trace_same_hash(self) -> None:
trace = (_step(step_index=0), _step(step_index=1, selected_word="gamma"))
assert hash_admissibility_trace(trace) == hash_admissibility_trace(trace)
def test_mutation_changes_hash(self) -> None:
original = (_step(step_index=0),)
mutated = (_step(step_index=0, selected_word="zeta"),)
assert hash_admissibility_trace(original) != hash_admissibility_trace(mutated)
def test_reason_change_changes_hash(self) -> None:
original = (_step(reason="ok"),)
mutated = (_step(reason="below threshold"),)
assert hash_admissibility_trace(original) != hash_admissibility_trace(mutated)
class TestComputeTraceHashBackwardCompat:
"""Pre-ADR-0023 calls (without the new kwargs) must produce the
*exact* hash they would have produced before ADR-0023, so existing
recorded turn hashes do not silently drift."""
def _baseline_kwargs(self) -> dict[str, object]:
return {
"input_text": "hello",
"filtered_tokens": ("hello",),
"surface": "hi",
"walk_surface": "hi",
"articulation_surface": "hi",
"dialogue_role": "assert",
"versor_condition": 1e-9,
"vault_hits": 0,
}
def test_default_kwargs_byte_preserved(self) -> None:
baseline = compute_trace_hash(**self._baseline_kwargs())
with_defaults = compute_trace_hash(
**self._baseline_kwargs(),
admissibility_trace_hash="",
ratification_outcome="",
region_was_unconstrained=True,
)
assert baseline == with_defaults
def test_non_default_trace_hash_changes_hash(self) -> None:
baseline = compute_trace_hash(**self._baseline_kwargs())
with_trace = compute_trace_hash(
**self._baseline_kwargs(),
admissibility_trace_hash="deadbeef",
)
assert baseline != with_trace
def test_non_default_ratification_outcome_changes_hash(self) -> None:
baseline = compute_trace_hash(**self._baseline_kwargs())
ratified = compute_trace_hash(
**self._baseline_kwargs(),
ratification_outcome="ratified",
)
assert baseline != ratified
def test_region_was_constrained_changes_hash(self) -> None:
baseline = compute_trace_hash(**self._baseline_kwargs())
constrained = compute_trace_hash(
**self._baseline_kwargs(),
region_was_unconstrained=False,
)
assert baseline != constrained
class TestRegionFromRelationChainTrace:
"""End-to-end: a region built from real versors yields verdicts that
round-trip through ``AdmissibilityTraceStep`` and hash deterministically.
"""
def _versor(self, seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
return rng.standard_normal(32).astype(np.float32)
def test_verdict_round_trips_through_step(self) -> None:
anchors = [self._versor(i) for i in range(3)]
region = region_from_relation_chain(anchors, label="chain")
verdict = check_transition(
region, candidate_index=7, candidate_versor=anchors[0]
)
step = AdmissibilityTraceStep(
step_index=0,
region_label=region.label,
region_source=region.source.value,
candidates_before=(7, 8),
candidates_after=(7,),
selected_index=7,
selected_word="alpha",
verdict=verdict,
)
canonical = step.canonical()
assert canonical["region_label"] == "chain"
assert canonical["verdict_admitted"] == verdict.admitted
def test_unconstrained_region_admits_any(self) -> None:
region = AdmissibilityRegion(label="unconstrained")
verdict = check_transition(
region, candidate_index=0, candidate_versor=np.zeros(32, dtype=np.float32)
)
assert verdict.admitted is True
if __name__ == "__main__": # pragma: no cover
pytest.main([__file__, "-v"])