feat(sor): SS3 RQ1 confirmatory loader (pcap -> frozen pair set)
confirm_load.py reconstructs the RQ1 (entry,exit) circuit-pair set from the REAL per-hop pcaps (bin_pcap_bytes -> score_matrix; diagonal=linked, off-diagonal=unlinked) so confirm.rq1_p1_leak scores measured data — the frozen §4 unit. classify_run reads persisted metrics.json; collect_rq1_p1_pairs selects only bridge=on no-pad runs. Plumbing unit-tested on SYNTHETIC pcaps only (prereg §2 blinding — no inspecting intermediate confirmatory results before the battery completes). RQ1-P2 nopad/pad pairing + all RQ2 held for the operator ruling (NEEDS-OPERATOR: RQ2 per-circuit posterior underspecified). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""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/<id>/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
|
||||
Reference in New Issue
Block a user