"""SS3 confirmatory loader — bind the immutable data dir to the frozen §6 gates. **RQ1 only** (the fully frozen-specified half). This reconstructs the RQ1 ``(entry-segment, exit-segment)`` circuit pairs from the **real per-hop pcaps** the executor wrote (``executor.bin_pcap_bytes`` → ``detectors.score_matrix``), never re-synthesising them, so :func:`confirm.rq1_p1_leak` scores measured data. The candidate true pairing is the diagonal (same circuit); off-diagonal pairs are unlinked — exactly the §4 "AUC over the (entry, exit) pair set, bootstrap over circuit pairs" unit. Deliberately NOT here (held for the operator ruling — see OVERSEER NEEDS-OPERATOR): * **RQ2** — the frozen DV is a *per-circuit adversary sender posterior* (§3/§4), whose construction is not formula-pinned in the prereg and not produced by the instrument; guessing a posterior model post-freeze would be HARKing. * **RQ1-P2** nopad/pad *pairing* convention for the paired bootstrap — the pairing rule across the bridge-on and bridge-on+padding arms is a choice; not guessed. Blinding (prereg §2, binding): callers must **not** run this on a confirmatory data dir until the full battery has completed — no inspection of intermediate confirmatory results. Its plumbing is unit-tested on synthetic pcaps only. """ from __future__ import annotations import json from pathlib import Path from typing import Dict, List, Tuple from cmd_chat.sor.analysis.detectors import score_matrix from cmd_chat.sor.executor import DEFAULT_BINS, ExecutorError, bin_pcap_bytes Pair = Tuple[float, bool] def classify_run(run_dir: Path) -> Dict[str, object]: """Read the run's ``metrics.json`` for its cell identity + arm flags (both persisted by the executor). Raises if absent — a run is never guessed.""" mp = Path(run_dir) / "metrics.json" if not mp.exists(): raise ExecutorError(f"no metrics.json in {run_dir} — cannot classify run") d = json.loads(mp.read_text(encoding="utf-8")) return { "cell_id": d["cell_id"], "rq": d["rq"], "padding_applied": bool(d.get("padding_applied", False)), } def _circuit_series(run_dir: Path, bins: int, hops: int) -> Tuple[List[List[int]], List[List[int]]]: """Per-circuit (ingress=hop0, egress=last-hop) binned byte series, read from the REAL pcaps under ``run_dir/circuits//pcap/``. Refuses a circuit that is missing either endpoint pcap (never invents a flow).""" cdir = Path(run_dir) / "circuits" if not cdir.is_dir(): raise ExecutorError(f"no circuits/ under {run_dir}") ingress: List[List[int]] = [] egress: List[List[int]] = [] for circuit in sorted(p for p in cdir.iterdir() if p.is_dir()): pdir = circuit / "pcap" ing, eg = pdir / "hop0.pcap", pdir / f"hop{hops - 1}.pcap" if not ing.exists() or not eg.exists(): raise ExecutorError(f"missing ingress/egress pcap in {circuit}") ingress.append(bin_pcap_bytes(ing, bins)) egress.append(bin_pcap_bytes(eg, bins)) return ingress, egress def reconstruct_run_pairs(run_dir: Path, *, bins: int = DEFAULT_BINS, hops: int = 3) -> List[Pair]: """The RQ1 pair set for one run: ``S[i][j] = pearson(ingress_i, egress_j)`` over the run's circuits; the diagonal (``i == j``) is the linked (same-circuit) pair, every off-diagonal cell an unlinked pair. Measured from real pcaps.""" ingress, egress = _circuit_series(Path(run_dir), bins, hops) scores = score_matrix(ingress, egress) n = len(scores) return [(scores[i][j], i == j) for i in range(n) for j in range(n)] def collect_rq1_p1_pairs(data_dir: Path, *, bins: int = DEFAULT_BINS, hops: int = 3) -> List[Pair]: """Aggregate the RQ1-P1 leak-arm pairs across every **bridge-on (no padding)** run in the battery data dir — the pair set :func:`confirm.rq1_p1_leak` scores. Frozen cell ids end in ``bridge=on`` for the leak arm (``bridge=on+padding`` and ``bridge=off`` are excluded; ``padding_applied`` double-guards).""" pairs: List[Pair] = [] for run_dir in sorted(p for p in Path(data_dir).iterdir() if p.is_dir()): if not (run_dir / "metrics.json").exists(): continue info = classify_run(run_dir) if info["rq"] == "RQ1" and str(info["cell_id"]).endswith("bridge=on") and not info["padding_applied"]: pairs.extend(reconstruct_run_pairs(run_dir, bins=bins, hops=hops)) return pairs