SS3: frozen §6 confirmatory pass on real 180/180 — honest null/negative

One auditable, deterministic run of the four lead-paper §6 tests on the
frozen raw battery. Calibration gate PASS (linked AUC 1.0000, unlinked
0.5036 on §5 fixtures) → AUCs reportable.

- RQ1-P1 (bridge leak): AUC 0.4660 CI[0.4523,0.4798] = anomaly-below-chance.
  NO measurable leak; Holm-sig but in the WRONG direction (not evidence of
  linkability).
- RQ2-P1 (federation): ΔH −0.9587 bits CI[−1.056,−0.864] = SHRINK. Holm-sig
  HONEST-NEGATIVE — federation reduces the anonymity set (opposite of RQ2's
  motivation); reported with equal prominence per §6 two-sided framing.
- RQ1-P2 (padding): ΔAUC +0.0113 CI[−0.0025,+0.0234] padding-ineffective
  (raw p 0.091, Holm adj-p 0.456, not rejected).
- RQ2-P3 (funnelling): Spearman ρ=0, zero-variance concentration =
  as-instrumented degeneracy flagged in advance; inconclusive.

Holm family=7 report-4 (multipliers 7,6,5,4); only RQ1-P1 + RQ2-P1 survive.
Ran ONLY the pre-registered tests — no re-slicing, no post-hoc subgroups.

Method faithfulness: RQ1-P1/P2 CIs use a bit-faithful fast numpy bootstrap
(frozen O(n²)-per-fold jackknife intractable at n=75000); stage06_run.py
--verify proves both == frozen stats.bootstrap_ci bit-for-bit (point/lo/hi/
method within 1e-12, incl the average-rank tie path). RQ2-P1/P3 stay on the
frozen confirm.* paths (tractable). Frozen prereg SHA INTACT.

Artifacts: docs/stage-06-analysis.md (methods-faithful narrative, Holm table,
CONFIRMATORY/EXPLORATORY labels, nulls reported honestly) + force-added
output/…/analysis/stage06-results.json (regenerable via stage06_run).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-21 00:55:44 -07:00
parent 47b2937789
commit 33b82b39ca
4 changed files with 732 additions and 0 deletions
+394
View File
@@ -0,0 +1,394 @@
"""SS3 — the FROZEN §6 confirmatory analysis, run ONCE on the real 180-cell
battery. Deterministic and regenerable from the frozen raw data + seeds.
python -m cmd_chat.sor.analysis.stage06_run <data_dir> [--out results.json]
python -m cmd_chat.sor.analysis.stage06_run <data_dir> --verify # identity self-check
This computes the four lead-paper confirmatory tests exactly per the frozen prereg
§6 + the RATIFIED stage-05 clarifications, applies Holm over the frozen size-7
family (reporting the 4 RQ1/RQ2 tests, multipliers 7,6,5,4), and runs the §5
correlator-calibration sanity gate. **Nulls are reported as results**; nothing is
tuned to the data; only the pre-registered tests are run.
Faithfulness of the RQ1-P1 fast path
------------------------------------
RQ1-P1's pooled (entry,exit) pair set is ~75 000 units; the frozen
``stats.bootstrap_ci`` BCa path does an O(n) leave-one-out jackknife (each fold an
O(n²) rank-AUC), which is structurally intractable at that scale. We therefore run
a *performance-faithful* bootstrap (:func:`_bootstrap_auc_ci`) that reproduces the
frozen ``stats.bootstrap_ci`` **bit-for-bit** — the SAME ``random.Random(seed)``
resample sequence, an AUC provably identical to the frozen detector
(:func:`_fast_auc`, verified against ``detectors.auc``), the frozen
``_bca_endpoints`` / ``_percentile`` for the interval, and a vectorised
leave-one-out jackknife whose per-fold value equals the frozen per-fold recompute.
``--verify`` asserts this equals ``stats.bootstrap_ci`` on a subsample. The other
three tests use the frozen ``confirm.*`` functions unchanged.
"""
from __future__ import annotations
import argparse
import json
import random
from pathlib import Path
from typing import Dict, List, Sequence, Tuple
import numpy as np
from cmd_chat.sor.analysis import confirm, confirm_load, confirm_load_rq2 as rq2
from cmd_chat.sor.analysis.detectors import auc as frozen_auc, bridge_correlation_auc
from cmd_chat.sor.analysis.detectors import synthetic_bridge_fixture
from cmd_chat.sor.analysis import stats as S
from cmd_chat.sor.battery import S0, C_CIRCUITS, derive_seed, enumerate_cells
from cmd_chat.sor.executor import _circuit_seed
Pair = Tuple[float, bool]
ANALYSIS_SEED = S0 # 20260719 — the frozen base seed; fixed pre-data (§4).
N_RESAMPLES = S.DEFAULT_RESAMPLES # 10_000 (frozen §6)
ALPHA = S.DEFAULT_ALPHA
# --------------------------------------------------------------------------- #
# Fast MannWhitney AUC — provably identical to detectors.auc (ties => 0.5).
# --------------------------------------------------------------------------- #
def _fast_auc(pos: np.ndarray, neg: np.ndarray) -> float:
if pos.size == 0 or neg.size == 0:
return 0.5
allv = np.concatenate([pos, neg])
order = allv.argsort(kind="mergesort")
sv = allv[order]
# Vectorised average-rank tie correction (bit-identical to the per-group
# (first_rank+last_rank)/2 assignment): a tied run at sorted positions
# [start, end) carries sequential ranks start+1..end, mean = (start+1+end)/2.
_uniq, inv, counts = np.unique(sv, return_inverse=True, return_counts=True)
ends = np.cumsum(counts) # exclusive end position per group
starts = ends - counts # 0-based start position per group
avg_rank = (starts + 1 + ends) / 2.0
ranks = np.empty(allv.size, dtype=float)
ranks[order] = avg_rank[inv] # back to concatenated (pos|neg) order
u = ranks[:pos.size].sum() - pos.size * (pos.size + 1) / 2.0
return float(u / (pos.size * neg.size))
def _jackknife_aucs(scores: np.ndarray, linked: np.ndarray) -> np.ndarray:
"""Leave-one-out AUC for every original index, vectorised. Each value equals
the frozen per-fold recompute ``_auc_of_pairs([units[j] for j != i])``."""
pos = np.sort(scores[linked])
neg = np.sort(scores[~linked])
P, N = pos.size, neg.size
# Full MannWhitney U with ties counted 0.5.
lt = np.searchsorted(neg, pos, side="left").astype(float)
le = np.searchsorted(neg, pos, side="right").astype(float)
contrib_pos = lt + 0.5 * (le - lt) # each pos point's U share
U = contrib_pos.sum()
s = scores
is_pos = linked
# contributions keyed by value via searchsorted on the sorted arrays
p_lt = np.searchsorted(neg, s, side="left").astype(float)
p_le = np.searchsorted(neg, s, side="right").astype(float)
pos_share = p_lt + 0.5 * (p_le - p_lt) # valid where is_pos
n_gt = (P - np.searchsorted(pos, s, side="right")).astype(float)
n_eq = (np.searchsorted(pos, s, side="right") - np.searchsorted(pos, s, side="left")).astype(float)
neg_share = n_gt + 0.5 * n_eq # valid where ~is_pos
denom_pos = (P - 1) * N
denom_neg = P * (N - 1)
auc_pos = (U - pos_share) / denom_pos if denom_pos > 0 else np.full(s.size, 0.5)
auc_neg = (U - neg_share) / denom_neg if denom_neg > 0 else np.full(s.size, 0.5)
out = np.where(is_pos, auc_pos, auc_neg)
return out
def _bootstrap_auc_ci(pairs: Sequence[Pair], *, seed: int, n_resamples: int,
alpha: float) -> Tuple[S.CIResult, List[float]]:
"""Bit-faithful re-implementation of ``stats.bootstrap_ci(pairs, _auc, method='bca')``
for the pooled RQ1-P1 pair set: identical RNG sequence, identical AUC values,
frozen ``_bca_endpoints`` / ``_percentile``. Tractable via numpy + vectorised
jackknife. Also returns the sorted resample distribution so the two-sided p can
reuse it (the p is order-invariant). ``--verify`` checks the CI equals the frozen
function on a subsample."""
n = len(pairs)
scores = np.fromiter((s for s, _ in pairs), dtype=float, count=n)
linked = np.fromiter((1 if l else 0 for _, l in pairs), dtype=bool, count=n)
def auc_of_idx(idx: np.ndarray) -> float:
s = scores[idx]
l = linked[idx]
return _fast_auc(s[l], s[~l])
theta_hat = _fast_auc(scores[linked], scores[~linked])
rng = random.Random(seed) # SAME sequence as frozen bootstrap_ci
thetas: List[float] = []
for _ in range(n_resamples):
idx = np.fromiter((rng.randrange(n) for _ in range(n)), dtype=np.int64, count=n)
thetas.append(auc_of_idx(idx))
thetas.sort()
jack = list(_jackknife_aucs(scores, linked)) # order = original index order
q_lo, q_hi, used = S._bca_endpoints(thetas, theta_hat, jack, alpha)
ci = S.CIResult(theta_hat, S._percentile(thetas, q_lo), S._percentile(thetas, q_hi),
alpha, n_resamples, used, seed)
return ci, thetas
def _bootstrap_delta_auc_ci(units: Sequence[confirm.PairedCircuit], *, seed: int,
n_resamples: int, alpha: float) -> Tuple[S.CIResult, List[float]]:
"""Bit-faithful re-implementation of ``stats.bootstrap_ci(units, confirm._delta_auc,
method='bca')`` for RQ1-P2 — same class of intractability as RQ1-P1 (each of the
10 000 resamples pools ~75 000 pairs through the O(pos×neg) frozen ``auc`` twice).
Same RNG sequence, the AUC ``_fast_auc`` proven identical to ``detectors.auc``,
frozen ``_bca_endpoints`` / ``_percentile``, and the frozen leave-one-out
jackknife over the paired units. ``--verify`` checks it equals the frozen
``bootstrap_ci`` on a small PairedCircuit set. Returns the sorted resample
distribution so the two-sided p reuses it (order-invariant)."""
n = len(units)
npd_s = [np.fromiter((s for s, _ in u.nopad_pairs), float, len(u.nopad_pairs)) for u in units]
npd_l = [np.fromiter((1 if l else 0 for _, l in u.nopad_pairs), bool, len(u.nopad_pairs)) for u in units]
pad_s = [np.fromiter((s for s, _ in u.pad_pairs), float, len(u.pad_pairs)) for u in units]
pad_l = [np.fromiter((1 if l else 0 for _, l in u.pad_pairs), bool, len(u.pad_pairs)) for u in units]
def delta(idxs) -> float:
ns = np.concatenate([npd_s[i] for i in idxs]); nl = np.concatenate([npd_l[i] for i in idxs])
ps = np.concatenate([pad_s[i] for i in idxs]); pl = np.concatenate([pad_l[i] for i in idxs])
return _fast_auc(ns[nl], ns[~nl]) - _fast_auc(ps[pl], ps[~pl])
theta_hat = delta(range(n))
rng = random.Random(seed) # SAME sequence as frozen bootstrap_ci
thetas: List[float] = []
for _ in range(n_resamples):
idxs = [rng.randrange(n) for _ in range(n)]
thetas.append(delta(idxs))
thetas.sort()
jack = [delta([j for j in range(n) if j != i]) for i in range(n)]
q_lo, q_hi, used = S._bca_endpoints(thetas, theta_hat, jack, alpha)
ci = S.CIResult(theta_hat, S._percentile(thetas, q_lo), S._percentile(thetas, q_hi),
alpha, n_resamples, used, seed)
return ci, thetas
# --------------------------------------------------------------------------- #
# RQ2 arm assembly from the per-cell dirs (no aggregate battery-results.json was
# written; per-circuit seeds are regenerated deterministically — SS2 verified the
# reconstruction against all 180 run-dir fingerprints).
# --------------------------------------------------------------------------- #
def _regen_circuit_seeds(cell_id: str, run_index: int, c: int = C_CIRCUITS) -> List[int]:
run_seed = derive_seed(cell_id, run_index)
return [_circuit_seed(run_seed, i) for i in range(c)]
def collect_rq2_arms(data_dir: Path):
"""Return (federated, single_house, bridge_only, concentration, per_h) exactly
as ``confirm_load_rq2.collect_rq2_p1_arms`` / ``collect_rq2_p3`` would, but
sourced from the per-cell dirs via regenerated per-circuit seeds."""
cells = {c.cell_id: c for c in enumerate_cells()}
federated: List[List[int]] = []
single: List[List[int]] = []
bridge_only: List[List[int]] = []
conc: List[float] = []
per_h: List[float] = []
for rd in sorted(p for p in Path(data_dir).iterdir() if p.is_dir()):
mp = rd / "metrics.json"
if not mp.exists():
continue
m = json.loads(mp.read_text(encoding="utf-8"))
cell = cells.get(m["cell_id"])
if cell is None or cell.rq != "RQ2":
continue
seeds = _regen_circuit_seeds(m["cell_id"], int(m["run_index"]))
specs = rq2.reconstruct_run(cell, seeds)
vecs = rq2.per_circuit_posteriors(specs)
topo = cell.factors.get("topology")
if topo == "1house-N":
single.extend(vecs)
elif topo in ("bridge-federated", "directory-federated"):
federated.extend(vecs)
if topo == "bridge-federated":
bridge_only.extend(vecs)
xs, ys = rq2.rq2_p3_pairs(specs)
conc.extend(xs)
per_h.extend(ys)
return federated, single, bridge_only, conc, per_h
# --------------------------------------------------------------------------- #
# Calibration sanity gate (§5 gate item 3) — independent of the confirmatory data.
# --------------------------------------------------------------------------- #
def calibration_gate(seeds=range(40)) -> Dict:
linked = [bridge_correlation_auc(*synthetic_bridge_fixture(s, linked=True)) for s in seeds]
unlinked = [bridge_correlation_auc(*synthetic_bridge_fixture(s, linked=False)) for s in seeds]
lo = sum(linked) / len(linked)
ul = sum(unlinked) / len(unlinked)
ok = (lo >= 0.95) and (0.40 <= ul <= 0.60)
return {"linked_mean_auc": lo, "unlinked_mean_auc": ul, "n_seeds": len(list(seeds)),
"passes": ok, "criterion": "linked>=0.95 and 0.40<=unlinked<=0.60"}
# --------------------------------------------------------------------------- #
def run(data_dir: str) -> Dict:
data = Path(data_dir)
calib = calibration_gate()
if not calib["passes"]:
return {"calibration": calib, "ABORT": "correlator calibration failed — numbers NOT reported"}
# ---- RQ1-P1 (leak): bridge-on AUC, gate = CI excludes 0.5 (frozen). --------
p1_pairs = confirm_load.collect_rq1_p1_pairs(data)
ci, p1_thetas = _bootstrap_auc_ci(p1_pairs, seed=ANALYSIS_SEED,
n_resamples=N_RESAMPLES, alpha=ALPHA)
if ci.excludes(0.5) and ci.strictly_greater(0.5):
d1, l1 = "leak", ("material" if ci.lo >= confirm.MATERIALITY_FLOOR else "weak-but-real")
elif ci.strictly_less(0.5):
d1, l1 = "anomaly-below-chance", ""
else:
d1, l1 = "null", ""
# bootstrap p vs 0.5 from the SAME resample distribution (order-invariant count).
p1 = confirm.ConfirmTest("RQ1-P1", "AUC", ci, d1, l1,
S.two_sided_bootstrap_p(p1_thetas, 0.5))
# ---- RQ1-P2 (padding efficacy): run_index-paired ΔAUC. ---------------------
# Same intractability as RQ1-P1 (each resample pools ~75k pairs through the
# O(n^2) frozen auc twice); CI via the performance-faithful bootstrap that
# reproduces stats.bootstrap_ci(units, _delta_auc, bca) bit-for-bit. The frozen
# decision + p-ordering are replicated unchanged.
paired = confirm_load.collect_rq1_p2_paired(data)
p2_ci, p2_thetas = _bootstrap_delta_auc_ci(paired, seed=ANALYSIS_SEED,
n_resamples=N_RESAMPLES, alpha=ALPHA)
p2_decision = "padding-effective" if p2_ci.strictly_greater(0.0) else "padding-ineffective"
p2 = confirm.ConfirmTest("RQ1-P2", "ΔAUC", p2_ci, p2_decision, "",
S.two_sided_bootstrap_p(p2_thetas, 0.0))
p2_delta = confirm_load.per_run_delta_aucs(paired)
# ---- RQ2 arms (ratified per-circuit posterior). ---------------------------
federated, single, bridge_only, conc, per_h = collect_rq2_arms(data)
p1h = confirm.rq2_p1_delta_h(federated, single, seed=ANALYSIS_SEED,
n_resamples=N_RESAMPLES, alpha=ALPHA)
p3 = confirm.rq2_p3_funnel(conc, per_h, seed=ANALYSIS_SEED,
n_resamples=N_RESAMPLES, alpha=ALPHA)
# EXPLORATORY: bridge-federated-only ΔH (the directive's narrower contrast).
exp_bridge = confirm.rq2_p1_delta_h(bridge_only, single, seed=ANALYSIS_SEED,
n_resamples=N_RESAMPLES, alpha=ALPHA)
tests = [p1, p2, p1h, p3]
holm = confirm.apply_holm(tests)
holm_by = {h.name: h for h in holm}
def test_row(t: confirm.ConfirmTest) -> Dict:
h = holm_by[t.name]
return {**t.as_dict(),
"raw_p": t.p_for_holm, "holm_p": h.p_adjusted,
"holm_multiplier": h.multiplier, "holm_rank": h.rank,
"holm_reject": h.reject, "label_type": "CONFIRMATORY"}
return {
"meta": {
"data_dir": str(data), "analysis_seed": ANALYSIS_SEED,
"n_resamples": N_RESAMPLES, "alpha": ALPHA,
"frozen_prereg_sha256": "f22331a72e0d0ccf38b787e63acabbe9d666456ec76076787a6d545c3193425b",
"holm_family": list(confirm.FROZEN_FAMILY), "holm_family_size": confirm.FROZEN_FAMILY_SIZE,
"reported": list(confirm.LEAD_PAPER_TESTS),
},
"calibration": calib,
"n": {
"rq1_p1_pairs": len(p1_pairs),
"rq1_p1_linked": int(sum(1 for _, l in p1_pairs if l)),
"rq1_p2_paired_runs": len(paired),
"rq2_federated_circuits": len(federated),
"rq2_single_house_circuits": len(single),
"rq2_bridge_federated_circuits": len(bridge_only),
"rq2_p3_points": len(conc),
},
"confirmatory": {
"RQ1-P1": {**test_row(p1),
"method_note": ("pooled pair-set BCa jackknife is O(n^2)-intractable at "
"n=%d; CI via performance-faithful bootstrap reproducing "
"stats.bootstrap_ci bit-for-bit (verified on subsample). "
"Point estimate + gate unchanged." % len(p1_pairs))},
"RQ1-P2": {**test_row(p2), "per_run_delta_aucs": p2_delta,
"method_note": ("paired ΔAUC over run-index units; each resample pools "
"~75k pairs through the O(n^2) frozen auc twice, so the CI "
"uses the performance-faithful bootstrap reproducing "
"stats.bootstrap_ci(units, _delta_auc, bca) bit-for-bit "
"(verified). Decision + gate unchanged.")},
"RQ2-P1": test_row(p1h),
"RQ2-P3": {**test_row(p3),
"instrument_caveat": ("bridge-federated assigns a fresh bridge per circuit "
"seed; willing-bridge reuse is minimal so the P3 "
"concentration distribution may be near-degenerate "
"(an honest as-instrumented property).")},
},
"exploratory": {
"RQ2-P1-bridge-federated-only": {**exp_bridge.as_dict(),
"label_type": "EXPLORATORY",
"note": "ΔH(bridge-federated) H(1house-N); narrower than the confirmatory "
"federated-pooled arm; not Holm-corrected; reported for transparency."},
},
}
def _verify():
"""Assert _bootstrap_auc_ci reproduces stats.bootstrap_ci bit-for-bit on a
subsample, and _fast_auc == frozen auc."""
# _fast_auc must equal the frozen detector, INCLUDING heavy-tie inputs (the
# vectorised average-rank tie correction path).
trng = random.Random(7)
for _ in range(200):
pv = np.array([trng.randrange(5) for _ in range(trng.randint(1, 12))], float)
nv = np.array([trng.randrange(5) for _ in range(trng.randint(1, 12))], float)
assert abs(_fast_auc(pv, nv) - frozen_auc(list(pv), list(nv))) < 1e-12, (pv, nv)
rng = random.Random(1)
pairs = [(rng.random(), (i % 25 == 0)) for i in range(2000)] # ~80 linked
from cmd_chat.sor.analysis.confirm import _auc_of_pairs
fast, _fast_thetas = _bootstrap_auc_ci(pairs, seed=123, n_resamples=300, alpha=0.05)
frozen, _ = S.bootstrap_ci(list(pairs), _auc_of_pairs, n_resamples=300, alpha=0.05,
seed=123, method="bca", return_dist=True)
assert abs(fast.point - frozen.point) < 1e-12, (fast.point, frozen.point)
assert abs(fast.lo - frozen.lo) < 1e-12, (fast.lo, frozen.lo)
assert abs(fast.hi - frozen.hi) < 1e-12, (fast.hi, frozen.hi)
assert fast.method == frozen.method, (fast.method, frozen.method)
print("VERIFY OK: fast RQ1-P1 bootstrap == frozen stats.bootstrap_ci "
f"(point={fast.point:.6f} ci=[{fast.lo:.6f},{fast.hi:.6f}] method={fast.method})")
# RQ1-P2 fast ΔAUC bootstrap == frozen bootstrap_ci(units, _delta_auc, bca).
urng = random.Random(9)
def _mk_unit():
npd = tuple((urng.random(), (urng.random() < 0.4)) for _ in range(urng.randint(6, 20)))
pad = tuple((urng.random(), (urng.random() < 0.4)) for _ in range(urng.randint(6, 20)))
return confirm.PairedCircuit(npd, pad)
units = [_mk_unit() for _ in range(8)]
fast2, _t2 = _bootstrap_delta_auc_ci(units, seed=123, n_resamples=300, alpha=0.05)
frozen2, _ = S.bootstrap_ci(list(units), confirm._delta_auc, n_resamples=300, alpha=0.05,
seed=123, method="bca", return_dist=True)
assert abs(fast2.point - frozen2.point) < 1e-12, (fast2.point, frozen2.point)
assert abs(fast2.lo - frozen2.lo) < 1e-12, (fast2.lo, frozen2.lo)
assert abs(fast2.hi - frozen2.hi) < 1e-12, (fast2.hi, frozen2.hi)
assert fast2.method == frozen2.method, (fast2.method, frozen2.method)
print("VERIFY OK: fast RQ1-P2 ΔAUC bootstrap == frozen stats.bootstrap_ci "
f"(point={fast2.point:.6f} ci=[{fast2.lo:.6f},{fast2.hi:.6f}] method={fast2.method})")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("data_dir", nargs="?")
ap.add_argument("--out")
ap.add_argument("--verify", action="store_true")
args = ap.parse_args()
if args.verify:
_verify()
return
res = run(args.data_dir)
text = json.dumps(res, indent=2)
if args.out:
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
Path(args.out).write_text(text, encoding="utf-8")
print("wrote", args.out)
else:
print(text)
if __name__ == "__main__":
main()