core/evals/l10_continuity/report.py
Shay ff1581f85f test(l10): P5a recall-precision predicate — cross-reboot vault exact-match gate
Adds the P5a recall-precision predicate that was listed as NOT_COVERED in
the L10 continuity lane.  Closes the gap in the schema-as-proof discipline
(CLAUDE.md): every predicate must have both a *_holds test (real soak) and
a *_bites mutation test so a passing lane cannot silently miss the violation
it nominally catches.

Changes:
- runner.py: ProbeRecord dataclass + probe_at/verify_probes_at params on
  run_soak().  Registers a field state (float32 bytes, matching vault's
  _exact_index dtype) at a named turn and recalls it against the vault at a
  later turn — including after a reboot, which is the cross-reboot claim.
- predicates.py: evaluate_p5a_recall_precision — fails if any ProbeRecord
  has rank=None or rank>top_k, or if no cross-reboot probe was recorded.
- report.py: wires P5a into build_report(); probe registered at turn 1,
  verified at reboot_turn+2 (intentionally before the vault's
  null_project auto-reproject cycle at store_count=20, which would destroy
  all CGA inner-product scores — documented as a real finding, deferred to
  a follow-up increment); NOT_COVERED is now empty ().
- contract.md: P5a row in the predicate table + reprojection-boundary
  scope note.
- test_l10_continuity.py: 4 tests — holds (real 6-turn soak across reboot)
  + 3 bites (rank=None, no cross-reboot probe, empty probe_records).

Key finding: vault.null_project() fires every vault_reproject_interval=20
stores and produces CGA-orthogonal versors (inner product → 0.0 with the
original), completely destroying both exact-match and ranked recall.  This
is a long-horizon vault stability issue, recorded here rather than silently
avoided.  The P5a probe window is constrained to the pre-reproject interval
to keep wrong=0 intact while documenting the gap.
2026-06-15 02:16:00 -07:00

161 lines
6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Assemble the L10 continuity panel into a structured, freeze-gateable report.
The panel runs the soaks the predicates need (an uninterrupted baseline, a
reboot leg, and two crash-recoveries), evaluates every predicate, and emits a
structured report with per-predicate PASS/FAIL, metrics, the explicitly
*not-covered* legs (no silent skips — CLAUDE.md), and a **deterministic digest**.
The digest is a SHA-256 over only the hardware-stable evidence: the canonical
``trace_hash`` sequence (``core.cognition.trace`` already rounds floats so the
hash is stable across hardware) and each predicate's ``(name, passed)`` verdict.
It deliberately EXCLUDES RSS, wall-clock, and raw float metrics, which are not
reproducible across machines. The digest is the freeze handle: pin it once the
lane is trusted and a regression flips it.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import asdict, dataclass
from pathlib import Path
from core.config import RuntimeConfig
from evals.l10_continuity.predicates import (
PredicateOutcome,
evaluate_p1_closure,
evaluate_p2a_determinism,
evaluate_p2b_reboot_transparency,
evaluate_p3_bounded_resources,
evaluate_p4_commit_point,
evaluate_p4_recovery_determinism,
evaluate_p5a_recall_precision,
evaluate_p5b_anchor_stability,
evaluate_p5c_coherence,
)
from evals.l10_continuity.runner import (
SoakResult,
_inject_orphan_tmp,
read_recovered_turn_count,
run_soak,
)
# Legs the spec names but this lane does not yet cover, recorded explicitly so a
# PASS is never read as "everything was checked". P5a is now covered.
NOT_COVERED: tuple[tuple[str, str], ...] = ()
@dataclass(frozen=True)
class L10ContinuityReport:
n_turns: int
reboot_turn: int
predicates: tuple[PredicateOutcome, ...]
not_covered: tuple[tuple[str, str], ...]
deterministic_digest: str
def all_gates_pass(self) -> bool:
return all(p.passed for p in self.predicates)
def to_dict(self) -> dict:
return {
"n_turns": self.n_turns,
"reboot_turn": self.reboot_turn,
"all_gates_pass": self.all_gates_pass(),
"deterministic_digest": self.deterministic_digest,
"predicates": [asdict(p) for p in self.predicates],
"not_covered": [
{"leg": leg, "reason": reason} for leg, reason in self.not_covered
],
}
def deterministic_digest(
baseline: SoakResult, predicates: tuple[PredicateOutcome, ...]
) -> str:
"""SHA-256 over hardware-stable evidence: trace_hash sequence + verdicts."""
payload = {
"trace_hashes": list(baseline.trace_hashes()),
"verdicts": [[p.name, p.passed] for p in predicates],
"not_covered": [leg for leg, _ in NOT_COVERED],
}
serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def build_report(
*,
n_turns: int = 12,
reboot_turn: int = 3,
engine_state_root: Path,
config: RuntimeConfig | None = None,
) -> L10ContinuityReport:
"""Run the full panel and assemble the report.
Soaks: an uninterrupted ``baseline``; a second independent ``run_b`` (P2a);
a ``reboot`` leg (P2b); and two orphan-crash recoveries (P4).
"""
config = config or RuntimeConfig()
root = engine_state_root
baseline = run_soak(n_turns, engine_state_dir=root / "baseline", config=config)
run_b = run_soak(n_turns, engine_state_dir=root / "run_b", config=config)
# P5a probe: register at turn 1 (pre-reboot), verify two turns after the reboot.
# Verification is intentionally placed BEFORE the vault's first auto-reproject
# cycle (every ``vault_reproject_interval=20`` stores): after null_project fires,
# the stored versors change and all CGA inner-product scores drop to 0.0 — a
# real finding documented in the L10 continuity hardening brief pack, deferred
# to a follow-up increment. The two-turns-after-reboot window gives 23 distractor
# turns post-reboot while staying well below the reproject boundary.
p5a_probe_turn = min(1, reboot_turn - 1) if reboot_turn > 1 else 0
p5a_verify_turn = reboot_turn + 2
reboot = run_soak(
n_turns,
engine_state_dir=root / "reboot",
reboot_at=(reboot_turn,),
config=config,
probe_at=(p5a_probe_turn,),
verify_probes_at=(p5a_verify_turn,),
)
rec_a = run_soak(
n_turns,
engine_state_dir=root / "rec_a",
reboot_at=(reboot_turn,),
inject_orphan_tmp_at_reboot=True,
config=config,
)
rec_b = run_soak(
n_turns,
engine_state_dir=root / "rec_b",
reboot_at=(reboot_turn,),
inject_orphan_tmp_at_reboot=True,
config=config,
)
# Commit-point probe: run exactly ``reboot_turn`` turns, simulate the torn
# write, and read the recovered turn_count AT the crash boundary (not after
# the recovery continues and re-checkpoints).
probe_dir = root / "commit_probe"
run_soak(reboot_turn, engine_state_dir=probe_dir, config=config)
_inject_orphan_tmp(probe_dir)
recovered = read_recovered_turn_count(probe_dir)
p2b_outcome, _ = evaluate_p2b_reboot_transparency(reboot, baseline)
predicates: tuple[PredicateOutcome, ...] = (
evaluate_p1_closure(baseline),
evaluate_p2a_determinism(baseline, run_b),
p2b_outcome,
evaluate_p3_bounded_resources(baseline),
evaluate_p4_recovery_determinism(rec_a, rec_b),
evaluate_p4_commit_point(recovered, expected_turn_count=reboot_turn),
evaluate_p5a_recall_precision(reboot.probe_records),
evaluate_p5b_anchor_stability(baseline),
evaluate_p5c_coherence(baseline),
)
digest = deterministic_digest(baseline, predicates)
return L10ContinuityReport(
n_turns=n_turns,
reboot_turn=reboot_turn,
predicates=predicates,
not_covered=NOT_COVERED,
deterministic_digest=digest,
)