core/tests/test_phase5_corpus.py
Shay b6649848a6 feat(adr-0024): Phase 5 — stratified mechanism-isolation across 5 failure-mode families
Authors a 20-case corpus stratified across five geometric failure-mode
families and a separate 10-case benign corpus for the
EXHAUSTION_CEILING lane:

  A. near_forbidden_correct_endpoint  (6 cases, gaps 0.002 to 0.55)
  B. near_equal_admissible             (5 cases, diffs ≤ 0.01)
  C. no_admissible_path                (3 cases, honest refusal)
  D. multi_step_admissibility          (3 chained cases)
  E. heterogeneous_relation            (3 chained cases, blade-switching)

phase5_runner runs each case under BOTH threshold and ADR-0026 margin
modes and reports per-family pass_rate, refusal_rate, and (for Family
A) rejection_traced_rate + boundary_overridden_rate.

Headline:
  pass_rate_threshold = 1.00 (20/20)
  pass_rate_margin    = 1.00 (20/20)
  mechanism_isolated  = true (both modes, all five families)
  replay determinism  = byte-identical across 3 reruns

Family C refuses with RefusalReason.INNER_LOOP_EXHAUSTION in both
modes (load-bearing evidence for ADR-0024 Phase 2 typed refusals).
Family B refuses under margin mode (validates ADR-0026 δ=0.4 gate).

Benign inner-loop corpus for EXHAUSTION_CEILING ≤ 0.05 gate:
  boundary_only:    exhaustion 0.00, pass 1.00
  null_control:     exhaustion 0.00, pass 1.00
  inner_loop_t0:    exhaustion 0.00, pass 1.00
  inner_loop_tpos:  exhaustion 0.00, pass 1.00 (threshold 0.25)

Geometric finding documented while authoring the benign corpus:
23 of 85 pack tokens have negative self-cga_inner under Cl(4,1).
Tokens with self-score ≤ 0 cannot serve as single-token expected
endpoints in threshold mode — the algebra's Lorentzian signature
forbids this geometrically.  Phase 5 benign corpus draws expected
endpoints from the 62-token positive-self-score subset.  This is
consistent with Phase 4 characterization: no static threshold
delivers separation_quality ≥ 0.8 — the margin lane survives
because margin compares differences, not absolute scores.

Files:
  evals/forward_semantic_control/public/v2_phase5/cases.jsonl
  evals/forward_semantic_control/public/inner_loop_benign/cases.jsonl
  evals/forward_semantic_control/phase5_runner.py
  evals/forward_semantic_control/phase5_mine.py
  evals/forward_semantic_control/results/phase5_report.json
  evals/forward_semantic_control/results/phase5_benign_inner_loop_report.json
  tests/test_phase5_corpus.py        (20 passing)
  docs/evals/phase5_stratified_findings.md

Tests: 1068 passed, 2 skipped (+20 from Phase 4 baseline).
2026-05-17 15:51:59 -07:00

167 lines
5.9 KiB
Python

"""Phase 5 stratified mechanism-isolation contract tests.
Exercises the Phase 5 corpus end-to-end across all five failure-mode
families and pins:
- Per-family pass_rate = 1.0 under both threshold and margin modes.
- Family C ⇒ honest refusal with RefusalReason.INNER_LOOP_EXHAUSTION
in both modes.
- Family B ⇒ refusal under margin mode (diff < δ by construction).
- Replay determinism: 3 reruns produce byte-identical per-case
selections under margin mode.
These tests are the contract for ADR-0024 Phase 5 and gate against
silent regressions in the inner-loop / margin / refusal pipeline.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from evals.forward_semantic_control.phase5_runner import (
run_lane,
Phase5Report,
)
from generate.exhaustion import RefusalReason
CORPUS_PATH = Path("evals/forward_semantic_control/public/v2_phase5/cases.jsonl")
@pytest.fixture(scope="module")
def corpus() -> list[dict]:
return [
json.loads(line)
for line in CORPUS_PATH.read_text().splitlines()
if line.strip()
]
@pytest.fixture(scope="module")
def report(corpus: list[dict]) -> Phase5Report:
return run_lane(corpus)
class TestOverallContract:
def test_no_skipped_cases(self, report: Phase5Report) -> None:
assert report.metrics["skipped_count"] == 0
def test_all_five_families_present(self, report: Phase5Report) -> None:
expected = {
"near_forbidden_correct_endpoint",
"near_equal_admissible",
"no_admissible_path",
"multi_step_admissibility",
"heterogeneous_relation",
}
assert set(report.per_family.keys()) == expected
def test_mechanism_isolated_threshold(self, report: Phase5Report) -> None:
assert report.metrics["pass_rate_threshold"] == 1.0
assert report.metrics["mechanism_isolated_threshold"] is True
def test_mechanism_isolated_margin(self, report: Phase5Report) -> None:
assert report.metrics["pass_rate_margin"] == 1.0
assert report.metrics["mechanism_isolated_margin"] is True
class TestPerFamilyPassRates:
@pytest.mark.parametrize(
"family",
[
"near_forbidden_correct_endpoint",
"near_equal_admissible",
"no_admissible_path",
"multi_step_admissibility",
"heterogeneous_relation",
],
)
def test_pass_rate_threshold(self, report: Phase5Report, family: str) -> None:
assert report.per_family[family]["pass_rate_threshold"] == 1.0
@pytest.mark.parametrize(
"family",
[
"near_forbidden_correct_endpoint",
"near_equal_admissible",
"no_admissible_path",
"multi_step_admissibility",
"heterogeneous_relation",
],
)
def test_pass_rate_margin(self, report: Phase5Report, family: str) -> None:
assert report.per_family[family]["pass_rate_margin"] == 1.0
class TestRefusalContract:
def test_no_admissible_path_refuses_both_modes(
self, report: Phase5Report
) -> None:
fam = report.per_family["no_admissible_path"]
assert fam["refusal_rate_threshold"] == 1.0
assert fam["refusal_rate_margin"] == 1.0
def test_no_admissible_path_reason_is_inner_loop_exhaustion(
self, report: Phase5Report
) -> None:
expected = RefusalReason.INNER_LOOP_EXHAUSTION.value
for detail in report.case_details:
if detail.get("family") != "no_admissible_path":
continue
t_leg = detail["threshold_leg"]
m_leg = detail["margin_leg"]
assert t_leg["refused"] is True
assert m_leg["refused"] is True
assert t_leg["refusal_reason"] == expected
assert m_leg["refusal_reason"] == expected
def test_near_equal_refuses_under_margin(self, report: Phase5Report) -> None:
fam = report.per_family["near_equal_admissible"]
assert fam["refusal_rate_margin"] == 1.0
def test_near_equal_admits_under_threshold(self, report: Phase5Report) -> None:
fam = report.per_family["near_equal_admissible"]
assert fam["refusal_rate_threshold"] == 0.0
class TestRejectionEvidence:
def test_near_forbidden_traces_rejection_when_overriding_boundary(
self, report: Phase5Report
) -> None:
# When inner-loop overrides boundary's selection, the rejected
# token must appear in the trace. These rates may be < 1.0
# because some cases have boundary already aligned with
# expected, but the floor signal must be positive.
fam = report.per_family["near_forbidden_correct_endpoint"]
assert fam["rejection_traced_rate_threshold"] > 0.0
# rejection_traced ⇒ boundary_overridden by construction.
assert (
fam["boundary_overridden_rate_threshold"]
>= fam["rejection_traced_rate_threshold"]
)
class TestReplayDeterminism:
def test_margin_mode_three_run_byte_identity(
self, corpus: list[dict]
) -> None:
report1 = run_lane(corpus)
report2 = run_lane(corpus)
report3 = run_lane(corpus)
# Compare per-case margin-leg outcomes across all 3 runs.
for d1, d2, d3 in zip(
report1.case_details, report2.case_details, report3.case_details
):
assert d1.get("passed_margin") == d2.get("passed_margin") == d3.get("passed_margin")
# Single-step cases: margin_leg structural equality.
for key in ("threshold_leg", "margin_leg"):
leg1 = d1.get(key)
leg2 = d2.get(key)
leg3 = d3.get(key)
if leg1 is None:
continue
assert leg1.get("refused") == leg2.get("refused") == leg3.get("refused")
assert leg1.get("selected") == leg2.get("selected") == leg3.get("selected")