SS3: RQ2 rule RATIFIED + offline sender-posterior loader (synthetic-tested)
Flip docs/stage-05-rq2-posterior-clarification.md PROPOSED->RATIFIED (operator Andre, 2026-07-20, ratified while BLIND to RQ2 data — honest prereg-completion of a gap the freeze left, not HARKing). Substance of the construction unchanged. Add cmd_chat/sor/analysis/confirm_load_rq2.py: reconstructs each circuit's spec OFFLINE from the persisted per_circuit_seeds via deterministic assemble(), derives the observation-consistent anonymity set A_i (uniform / max-entropy -> [1]*m_i), per-circuit Miller-Madow H_i, and the willing-bridge concentration series — the exact inputs confirm.rq2_p1_delta_h (ΔH, two-sided) and rq2_p3_funnel (Spearman ρ) consume. Grounded only in the ratified rule ([Serjantov2002]/[Diaz2002]). BLINDING preserved (prereg §2): ratification unblocks CODE, not results. The collect_* real-data entrypoints are BLIND-GATED and NOT run; the reconstruction is unit-tested on SYNTHETIC specs/seeds only (test_sor_confirm_load_rq2.py, 6 passed; full SOR suite 172 passed). Frozen prereg untouched; no RQ2 statistic computed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
# OVERSEER-STATUS — sor-consent build agent
|
# OVERSEER-STATUS — sor-consent build agent
|
||||||
|
|
||||||
NEEDS-OPERATOR: How is the RQ2 per-circuit adversary sender-posterior constructed from a measured confirmatory circuit? The frozen prereg pins RQ2's DV as "Shannon entropy of the adversary's posterior over candidate senders PER CIRCUIT" (§3 L84-88), unit = "the circuit (entropy of the per-circuit sender posterior)" (§4 L108-110), Miller–Madow, BOOTSTRAP OVER CIRCUITS, and RQ2-P3 (§6 L205-207) needs a PER-CIRCUIT H for its Spearman ρ. But (a) the frozen prereg gives no explicit formula for building that per-circuit candidate-sender posterior from an observed circuit (it cites [Serjantov2002] S=2^H and [Diaz2002] d=H/log2 N for effective-size/normalization only), and (b) the built+committed executor (ce68d41) measures RQ2 as ONE per-RUN Shannon entropy of the realized entry-node FREQUENCY across the C=50 circuits — not a per-circuit posterior, and it yields no per-circuit H, so RQ2-P3 is uncomputable as-collected. I will NOT invent a posterior/adversary model post-freeze (that is HARKing + fabrication on a confirmatory DV). Need a ruling, one of: (A) ratify an explicit per-circuit posterior-construction rule (e.g. posterior mass over candidate senders as a function of the observed exit-bridge/house) logged as a stage-05 clarification like the Holm one → RQ2-P1/P3 then computable OFFLINE from the immutable raw records; (B) confirm the per-run realized-frequency entropy IS the intended RQ2 op (still leaves RQ2-P3's per-circuit H undefined); or (C) scope/relabel RQ2-P3. NOTE: the running battery is NOT wasted under any ruling — per_circuit_seeds are persisted + assemble() is deterministic, so every circuit's path/bridge/house is recoverable and RQ2 can be recomputed offline; RQ1 is fully specified and proceeds now. Battery left RUNNING.
|
RQ2 RATIFIED (operator Andre, 2026-07-20, while blind) — observation-consistent-set + uniform-mass per-circuit posterior; docs/stage-05-rq2-posterior-clarification.md flipped PROPOSED→RATIFIED. Offline loader built + synthetic-tested. BLIND HOLD REMAINS: no real RQ2 result until the battery completes.
|
||||||
|
|
||||||
|
|
||||||
- 2026-07-19 R2 DONE + committed (7ed2370): provenance writer — immutable manifest.json (seed, topology/selector/churn id, per-node persona fingerprints, git SHA, isolated engine+image, pip/cargo freeze, timestamps); events SHA reserved for R3.
|
- 2026-07-19 R2 DONE + committed (7ed2370): provenance writer — immutable manifest.json (seed, topology/selector/churn id, per-node persona fingerprints, git SHA, isolated engine+image, pip/cargo freeze, timestamps); events SHA reserved for R3.
|
||||||
@@ -47,3 +47,5 @@ NEEDS-OPERATOR: How is the RQ2 per-circuit adversary sender-posterior constructe
|
|||||||
- BATTERY MONITORING CAVEAT (for the loop): the SS2 launch wrapper reported a benign exit-1 (a trailing `echo > .current-ts` error) — the battery itself is VERIFIED HEALTHY and progressing (spot-checked 66→69 circuits/25s, docker hops cycling, python PID 2539198 alive under wrapper 2539195). Because that background task is now closed, COMPLETION WILL NOT AUTO-NOTIFY. Detect completion by: `output/sor-confirmatory/20260720T060132Z/confirmatory-data/battery-results.json` present AND hop0.pcap count == 9000 (currently ~69). Do NOT re-launch (would duplicate/waste progress). Resume SS3 analysis (post-blinding) only after completion + the RQ2 ruling.
|
- BATTERY MONITORING CAVEAT (for the loop): the SS2 launch wrapper reported a benign exit-1 (a trailing `echo > .current-ts` error) — the battery itself is VERIFIED HEALTHY and progressing (spot-checked 66→69 circuits/25s, docker hops cycling, python PID 2539198 alive under wrapper 2539195). Because that background task is now closed, COMPLETION WILL NOT AUTO-NOTIFY. Detect completion by: `output/sor-confirmatory/20260720T060132Z/confirmatory-data/battery-results.json` present AND hop0.pcap count == 9000 (currently ~69). Do NOT re-launch (would duplicate/waste progress). Resume SS3 analysis (post-blinding) only after completion + the RQ2 ruling.
|
||||||
- 2026-07-20 SS4 (data-independent draft) DONE + committed: docs/stage-07-paper-draft.md — lead paper (G4+RQ1+RQ2). Written BLIND (battery still running, zero confirmatory numbers seen): Abstract skeleton, Intro (G4 consent-gate novelty + RQ1 bridge-leak / RQ2 funnelling motivation), Related Work (onion/SOR [Egners2012], correlation [NasrBH18/OhYMH22], anonymity metrics [Serjantov2002/Diaz2002], social-trust G4 neighbours), full Methods from frozen prereg §2-§6 (design matrix, DVs, R=30×C=50, frozen detectors + §5 gate, Holm family=7 report-4), Limitations from §7, References. Results §5 + Discussion §6 = HELD-BLIND placeholders; RQ2 subsections additionally RATIFICATION-GATED (posterior proposal pending). No prereg edit; no RQ2 number.
|
- 2026-07-20 SS4 (data-independent draft) DONE + committed: docs/stage-07-paper-draft.md — lead paper (G4+RQ1+RQ2). Written BLIND (battery still running, zero confirmatory numbers seen): Abstract skeleton, Intro (G4 consent-gate novelty + RQ1 bridge-leak / RQ2 funnelling motivation), Related Work (onion/SOR [Egners2012], correlation [NasrBH18/OhYMH22], anonymity metrics [Serjantov2002/Diaz2002], social-trust G4 neighbours), full Methods from frozen prereg §2-§6 (design matrix, DVs, R=30×C=50, frozen detectors + §5 gate, Holm family=7 report-4), Limitations from §7, References. Results §5 + Discussion §6 = HELD-BLIND placeholders; RQ2 subsections additionally RATIFICATION-GATED (posterior proposal pending). No prereg edit; no RQ2 number.
|
||||||
- DRIVE STATE: SS1 ✅ SS2 firing ✅ SS3-RQ1 ✅ RQ2-proposal ✅ SS4-skeleton ✅. Battery: 492/9000, RUNNING healthy (PID 2539198). REMAINING GATES: SS3-RQ2 + RQ1-P2 pairing on Andre's RQ2 ratification; SS4 Results/Discussion + SS5 review on (a) battery completion (blinding) AND (b) RQ2 ratification. All data-independent overnight work now exhausted without fabricating or breaking blinding.
|
- DRIVE STATE: SS1 ✅ SS2 firing ✅ SS3-RQ1 ✅ RQ2-proposal ✅ SS4-skeleton ✅. Battery: 492/9000, RUNNING healthy (PID 2539198). REMAINING GATES: SS3-RQ2 + RQ1-P2 pairing on Andre's RQ2 ratification; SS4 Results/Discussion + SS5 review on (a) battery completion (blinding) AND (b) RQ2 ratification. All data-independent overnight work now exhausted without fabricating or breaking blinding.
|
||||||
|
- 2026-07-20 RQ2 RATIFIED + offline loader DONE + committed: docs/stage-05-rq2-posterior-clarification.md flipped PROPOSED→RATIFIED (operator Andre, while BLIND — 0 RQ2 stats computed). Built cmd_chat/sor/analysis/confirm_load_rq2.py: reconstructs each circuit's spec OFFLINE from per_circuit_seeds via deterministic assemble(), derives the observation-consistent anonymity set A_i (uniform/max-entropy → [1]*m_i), per-circuit H_i=Miller–Madow, and the willing-bridge concentration series — feeding confirm.rq2_p1_delta_h (ΔH two-sided) + rq2_p3_funnel (Spearman). Unit-tested on SYNTHETIC specs/seeds ONLY (test_sor_confirm_load_rq2.py, 6 passed; full SOR suite 172 passed). NEEDS-OPERATOR banner removed (resolved).
|
||||||
|
- HARD BLIND HOLD STILL ON: collect_rq2_p1_arms/collect_rq2_p3 are BLIND-GATED — NOT run on real data; no RQ2-P1/P2/P3 result computed/inspected/reported until battery completes. Battery: 3816/9000 (~42%), alive, healthy. INSTRUMENT CAVEAT for SS5: bridge-federated assigns a fresh bridge per circuit seed ⇒ willing-bridge reuse is minimal ⇒ P3 concentration may be near-degenerate as-instrumented (an honest, reportable property — loader does not fix it).
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
"""SS3 RQ2 offline loader — the per-circuit adversary sender-posterior, per the
|
||||||
|
**RATIFIED** stage-05 clarification (``docs/stage-05-rq2-posterior-clarification.md``,
|
||||||
|
operator-ratified 2026-07-20 while blind to RQ2 data).
|
||||||
|
|
||||||
|
This reconstructs each circuit's condition-encoding ``CircuitSpec`` **offline** from
|
||||||
|
its per-circuit seed (deterministic :func:`assembler.assemble`), derives the
|
||||||
|
**observation-consistent anonymity set** A_i (uniform / max-entropy posterior), and
|
||||||
|
yields the exact inputs the frozen §6 RQ2 tests consume:
|
||||||
|
|
||||||
|
* **RQ2-P1 (ΔH):** per-circuit sender-posterior **count vectors** (``[1]*m_i``)
|
||||||
|
for the federated and matched-N single-house arms → :func:`confirm.rq2_p1_delta_h`.
|
||||||
|
* **RQ2-P3 (funnel):** per-circuit **(bridge-concentration c_i, entropy H_i)**
|
||||||
|
over the willing-bridge arm → :func:`confirm.rq2_p3_funnel`.
|
||||||
|
|
||||||
|
**Ratified rule (substance frozen in the clarification; this is only its mechanical
|
||||||
|
implementation — no new modelling choice is introduced here):**
|
||||||
|
|
||||||
|
* The adversary observes the **exit-signature** the circuit leaves the federation
|
||||||
|
through: ``(exit-house, bridge-label)``. It does *not* observe the true entry.
|
||||||
|
* A_i = the consenting **entry-node candidates observation-consistent** with that
|
||||||
|
signature — the distinct realized entry nodes among the run's circuits sharing
|
||||||
|
circuit i's exit-signature. The **single-house arm** is the whole pool
|
||||||
|
(``A_i = matched N``): no federation observation narrows it (ratified doc).
|
||||||
|
* **Uniform mass** over A_i (max-entropy, the standard [Serjantov2002]/[Diaz2002]
|
||||||
|
anonymity-set assumption) → count vector ``[1]*m_i``. Per-circuit entropy
|
||||||
|
``H_i = miller_madow_entropy_bits([1]*m_i) = log2 m_i + (m_i-1)/(2 m_i ln2)``,
|
||||||
|
the §5-calibrated estimator (gate item 4). ``S_i = 2^H_i`` [Serjantov2002];
|
||||||
|
``d_i = H_i / log2 N`` [Diaz2002], N = matched total consenting nodes.
|
||||||
|
|
||||||
|
**BLINDING (prereg §2, binding).** Ratification unblocks the **CODE, not the
|
||||||
|
results**. Callers must NOT run :func:`collect_rq2_p1_arms` / :func:`collect_rq2_p3`
|
||||||
|
on a confirmatory data dir until the full battery has **completed**
|
||||||
|
(``battery-results.json`` present, or all cell-runs carry ``metrics.json``). The
|
||||||
|
core reconstruction below is unit-tested on **synthetic** specs / seeds only — no
|
||||||
|
confirmatory record is read to develop or test it.
|
||||||
|
|
||||||
|
**Instrument caveat (for the SS5 review, not a loader defect).** The bridge-
|
||||||
|
federated assembler derives a *fresh* bridge label per circuit seed
|
||||||
|
(``assembler._bridge_label(cseed)``), so willing-bridge reuse is minimal and the
|
||||||
|
P3 concentration distribution may be near-degenerate as-instrumented; that is an
|
||||||
|
honest property of the built instrument and, if the sealed data bears it out, an
|
||||||
|
inconclusive P3 is a legitimate (reported) outcome — the loader does not "fix" it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
from cmd_chat.sor.analysis.stats import miller_madow_entropy_bits
|
||||||
|
from cmd_chat.sor.assembler import CircuitSpec, assemble
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Per-circuit observable projections (what the exit-adversary sees / does not).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def entry_label(spec: CircuitSpec) -> str:
|
||||||
|
"""The circuit's true entry node — the sender the adversary is trying to
|
||||||
|
identify (NOT observed; it is what the anonymity set conceals)."""
|
||||||
|
return spec.hops[0].node_label
|
||||||
|
|
||||||
|
|
||||||
|
def bridge_label(spec: CircuitSpec) -> Optional[str]:
|
||||||
|
"""The willing-bridge node the circuit crosses, if any (``None`` for a
|
||||||
|
directory-federated / single-house circuit that carries no bridge hop)."""
|
||||||
|
for h in spec.hops:
|
||||||
|
if h.is_bridge:
|
||||||
|
return h.node_label
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def exit_signature(spec: CircuitSpec) -> Tuple[str, Optional[str]]:
|
||||||
|
"""The exit-signature the adversary observes: the (exit-house, bridge-label)
|
||||||
|
through which the circuit leaves the federation."""
|
||||||
|
return (spec.hops[-1].house, bridge_label(spec))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_single_house(spec: CircuitSpec) -> bool:
|
||||||
|
"""The matched-N single-house arm — no federation observation narrows the pool
|
||||||
|
(ratified doc: A_i = all N)."""
|
||||||
|
return spec.topology == "1house-N"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Observation-consistent anonymity set A_i (the ratified construction).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def observation_consistent_sizes(specs: Sequence[CircuitSpec]) -> List[int]:
|
||||||
|
"""Per-circuit anonymity-set size ``m_i = |A_i|`` for one run's circuits.
|
||||||
|
|
||||||
|
Single-house arm: ``m_i = matched N`` for every circuit (whole pool; no
|
||||||
|
narrowing). Federated arms: group the run's circuits by observed exit-signature
|
||||||
|
and set ``m_i`` = the number of **distinct realized entry nodes** in circuit
|
||||||
|
i's group — the consenting senders indistinguishable to the exit-adversary."""
|
||||||
|
if not specs:
|
||||||
|
return []
|
||||||
|
if all(_is_single_house(s) for s in specs):
|
||||||
|
return [s.matched_n for s in specs]
|
||||||
|
|
||||||
|
groups: dict = {}
|
||||||
|
for s in specs:
|
||||||
|
groups.setdefault(exit_signature(s), set()).add(entry_label(s))
|
||||||
|
return [len(groups[exit_signature(s)]) for s in specs]
|
||||||
|
|
||||||
|
|
||||||
|
def per_circuit_posteriors(specs: Sequence[CircuitSpec]) -> List[List[int]]:
|
||||||
|
"""Per-circuit uniform sender-posterior COUNT VECTORS ``[1]*m_i`` (max-entropy
|
||||||
|
over A_i) — the input rows for :func:`confirm.rq2_p1_delta_h`."""
|
||||||
|
return [[1] * m for m in observation_consistent_sizes(specs)]
|
||||||
|
|
||||||
|
|
||||||
|
def per_circuit_entropy(specs: Sequence[CircuitSpec]) -> List[float]:
|
||||||
|
"""Per-circuit Miller–Madow entropy ``H_i`` (bits) of the uniform posterior —
|
||||||
|
the §5-calibrated estimator applied to ``[1]*m_i``."""
|
||||||
|
return [miller_madow_entropy_bits([1] * m) for m in observation_consistent_sizes(specs)]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Willing-bridge concentration (RQ2-P3 mechanism).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def bridge_concentration(specs: Sequence[CircuitSpec]) -> List[Optional[float]]:
|
||||||
|
"""Per-circuit willing-bridge concentration ``c_i`` = the fraction of the run's
|
||||||
|
**bridge-bearing** circuits that route through the SAME willing bridge as
|
||||||
|
circuit i. ``None`` for a circuit that carries no bridge (excluded from the P3
|
||||||
|
funnel test, which is defined on the willing-bridge arm). Summing the top-3
|
||||||
|
distinct-bridge shares recovers the frozen §6 "top-k=3 bridge concentration"."""
|
||||||
|
bridged = [s for s in specs if bridge_label(s) is not None]
|
||||||
|
total = len(bridged)
|
||||||
|
if total == 0:
|
||||||
|
return [None for _ in specs]
|
||||||
|
counts: dict = {}
|
||||||
|
for s in bridged:
|
||||||
|
b = bridge_label(s)
|
||||||
|
counts[b] = counts.get(b, 0) + 1
|
||||||
|
out: List[Optional[float]] = []
|
||||||
|
for s in specs:
|
||||||
|
b = bridge_label(s)
|
||||||
|
out.append(None if b is None else counts[b] / total)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def top_k_bridge_concentration(specs: Sequence[CircuitSpec], k: int = 3) -> float:
|
||||||
|
"""Run-level "fraction of circuits through the top-k willing bridges" (frozen
|
||||||
|
§6, k=3) — reported alongside the per-circuit series for completeness."""
|
||||||
|
bridged = [s for s in specs if bridge_label(s) is not None]
|
||||||
|
total = len(bridged)
|
||||||
|
if total == 0:
|
||||||
|
return 0.0
|
||||||
|
counts: dict = {}
|
||||||
|
for s in bridged:
|
||||||
|
b = bridge_label(s)
|
||||||
|
counts[b] = counts.get(b, 0) + 1
|
||||||
|
top = sorted(counts.values(), reverse=True)[:k]
|
||||||
|
return sum(top) / total
|
||||||
|
|
||||||
|
|
||||||
|
def rq2_p3_pairs(specs: Sequence[CircuitSpec]) -> Tuple[List[float], List[float]]:
|
||||||
|
"""The parallel ``(concentration, per_circuit_h)`` series over the willing-bridge
|
||||||
|
circuits of a run — the two arguments of :func:`confirm.rq2_p3_funnel`. Circuits
|
||||||
|
with no bridge are dropped (concentration undefined there)."""
|
||||||
|
conc = bridge_concentration(specs)
|
||||||
|
ent = per_circuit_entropy(specs)
|
||||||
|
xs: List[float] = []
|
||||||
|
ys: List[float] = []
|
||||||
|
for c, h in zip(conc, ent):
|
||||||
|
if c is not None:
|
||||||
|
xs.append(c)
|
||||||
|
ys.append(h)
|
||||||
|
return xs, ys
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Offline reconstruction from the immutable raw records (per_circuit_seeds).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def reconstruct_run(cell, per_circuit_seeds: Sequence[int], *,
|
||||||
|
engine: str = "docker", hops: int = 3) -> List[CircuitSpec]:
|
||||||
|
"""Rebuild a run's ``C`` circuit specs OFFLINE from the immutable
|
||||||
|
``per_circuit_seeds`` the executor persisted: ``assemble`` is deterministic, so
|
||||||
|
each seed reproduces its circuit's path / house / bridge / consenting pool with
|
||||||
|
no live circuit. This is why the running battery is not wasted under the
|
||||||
|
ratified rule — RQ2 is recomputable from the sealed records."""
|
||||||
|
return [assemble(cell, int(cseed), engine=engine, hops=hops) for cseed in per_circuit_seeds]
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Real-data collectors — BLIND-GATED: do NOT call until the battery COMPLETES.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def _cells_by_id():
|
||||||
|
from cmd_chat.sor.battery import enumerate_cells
|
||||||
|
|
||||||
|
return {c.cell_id: c for c in enumerate_cells()}
|
||||||
|
|
||||||
|
|
||||||
|
def collect_rq2_p1_arms(data_dir, *, engine: str = "docker", hops: int = 3
|
||||||
|
) -> Tuple[List[List[int]], List[List[int]]]:
|
||||||
|
"""Aggregate the RQ2-P1 arms across the sealed battery: return
|
||||||
|
``(federated_vectors, single_house_vectors)`` — per-circuit posterior count
|
||||||
|
vectors for the two federated cells and the matched-N single-house cell,
|
||||||
|
reconstructed offline from each run's ``per_circuit_seeds``.
|
||||||
|
|
||||||
|
**BLIND-GATED (prereg §2).** Reads confirmatory records; must be called only
|
||||||
|
AFTER the full battery has completed. Not exercised by the synthetic tests."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
doc = json.loads((Path(data_dir) / "battery-results.json").read_text(encoding="utf-8"))
|
||||||
|
cells = _cells_by_id()
|
||||||
|
federated: List[List[int]] = []
|
||||||
|
single: List[List[int]] = []
|
||||||
|
for run in doc.get("runs", []):
|
||||||
|
cell = cells.get(run["cell_id"])
|
||||||
|
if cell is None or cell.rq != "RQ2":
|
||||||
|
continue
|
||||||
|
specs = reconstruct_run(cell, run.get("per_circuit_seeds", []), engine=engine, hops=hops)
|
||||||
|
vectors = per_circuit_posteriors(specs)
|
||||||
|
topo = cell.factors.get("topology")
|
||||||
|
if topo == "1house-N":
|
||||||
|
single.extend(vectors)
|
||||||
|
elif topo in ("bridge-federated", "directory-federated"):
|
||||||
|
federated.extend(vectors)
|
||||||
|
return federated, single
|
||||||
|
|
||||||
|
|
||||||
|
def collect_rq2_p3(data_dir, *, engine: str = "docker", hops: int = 3
|
||||||
|
) -> Tuple[List[float], List[float]]:
|
||||||
|
"""Aggregate the RQ2-P3 ``(concentration, per_circuit_h)`` series over the
|
||||||
|
willing-bridge (bridge-federated) circuits of the sealed battery.
|
||||||
|
|
||||||
|
**BLIND-GATED (prereg §2).** Reads confirmatory records; call only AFTER the
|
||||||
|
battery completes. Not exercised by the synthetic tests."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
doc = json.loads((Path(data_dir) / "battery-results.json").read_text(encoding="utf-8"))
|
||||||
|
cells = _cells_by_id()
|
||||||
|
conc: List[float] = []
|
||||||
|
ent: List[float] = []
|
||||||
|
for run in doc.get("runs", []):
|
||||||
|
cell = cells.get(run["cell_id"])
|
||||||
|
if cell is None or cell.rq != "RQ2":
|
||||||
|
continue
|
||||||
|
if cell.factors.get("topology") != "bridge-federated":
|
||||||
|
continue
|
||||||
|
specs = reconstruct_run(cell, run.get("per_circuit_seeds", []), engine=engine, hops=hops)
|
||||||
|
xs, ys = rq2_p3_pairs(specs)
|
||||||
|
conc.extend(xs)
|
||||||
|
ent.extend(ys)
|
||||||
|
return conc, ent
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# Stage-05 analysis clarification — RQ2 per-circuit adversary sender-posterior
|
# Stage-05 analysis clarification — RQ2 per-circuit adversary sender-posterior
|
||||||
|
|
||||||
**Status: PROPOSED — operator (Andre) ratification PENDING. NOT ratified; does
|
**Status: RATIFIED by operator (Andre) 2026-07-20, while BLIND to RQ2 data
|
||||||
NOT edit the frozen prereg** (`sor-consent-prereg.md`, SHA
|
(battery mid-collection, 0 RQ2 stats computed). Does NOT edit the frozen prereg**
|
||||||
|
(`sor-consent-prereg.md`, SHA
|
||||||
`f22331a72e0d0ccf38b787e63acabbe9d666456ec76076787a6d545c3193425b`).
|
`f22331a72e0d0ccf38b787e63acabbe9d666456ec76076787a6d545c3193425b`).
|
||||||
|
|
||||||
**Pre-specified BLIND to RQ2 outcomes — drafted 2026-07-20**, while the confirmatory
|
**Pre-specified BLIND to RQ2 outcomes — drafted 2026-07-20**, while the confirmatory
|
||||||
@@ -115,11 +116,14 @@ So under this rule RQ2-P1/P3 are computed once, post-battery, from the sealed da
|
|||||||
|
|
||||||
## Operator decision required
|
## Operator decision required
|
||||||
|
|
||||||
Ratify (or amend) the **observation-consistent-set + uniform-mass** construction
|
**RATIFIED** (operator Andre, 2026-07-20, while BLIND): the
|
||||||
above. On ratification this note flips to RATIFIED and the offline RQ2 loader
|
**observation-consistent-set + uniform-mass** construction above is adopted as
|
||||||
(mirroring `confirm_load.py`'s RQ1 path) computes RQ2-P1/P3 once from the sealed
|
the RQ2 per-circuit posterior. The offline RQ2 loader (mirroring
|
||||||
records. **Until then RQ2 stays BLIND — no RQ2 confirmatory statistic is computed,
|
`confirm_load.py`'s RQ1 path) computes RQ2-P1/P3 once from the sealed records.
|
||||||
inspected, or reported.**
|
Ratification unblocks the **CODE only** — RQ2 stays BLIND until the battery
|
||||||
|
completes: **no real RQ2 confirmatory statistic is computed, inspected, or
|
||||||
|
reported until all data has landed** (`battery-results.json` present, or 180
|
||||||
|
cell-runs each with `metrics.json`).
|
||||||
|
|
||||||
### Citations
|
### Citations
|
||||||
- **[Serjantov2002]** Serjantov, A., & Danezis, G. (2002). Towards an Information
|
- **[Serjantov2002]** Serjantov, A., & Danezis, G. (2002). Towards an Information
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""SS3 RQ2 loader — the RATIFIED per-circuit sender-posterior construction, on
|
||||||
|
SYNTHETIC specs/seeds ONLY.
|
||||||
|
|
||||||
|
These pin that the offline loader implements the ratified stage-05 rule
|
||||||
|
(observation-consistent anonymity set → uniform posterior → Miller–Madow H) and
|
||||||
|
produces the exact inputs confirm.rq2_p1_delta_h / rq2_p3_funnel consume — WITHOUT
|
||||||
|
reading any confirmatory record (prereg §2 blinding: ratification unblocks the
|
||||||
|
CODE, not the results). Specs are hand-built; the one reconstruction test drives
|
||||||
|
the deterministic assembler on chosen seeds (no confirmatory data dir).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from cmd_chat.sor.analysis import confirm, confirm_load_rq2 as rq2
|
||||||
|
from cmd_chat.sor.analysis.stats import miller_madow_entropy_bits
|
||||||
|
from cmd_chat.sor.assembler import CircuitSpec, HopSpec
|
||||||
|
from cmd_chat.sor.battery import derive_seed, enumerate_cells
|
||||||
|
|
||||||
|
|
||||||
|
def _spec(topology, entry, exit_house, bridge, *, matched_n=8):
|
||||||
|
"""A minimal 3-hop CircuitSpec: entry(houseA) → [bridge?] → exit(exit_house)."""
|
||||||
|
hops = [HopSpec(0, "entry", "houseA", entry, False)]
|
||||||
|
if bridge is not None:
|
||||||
|
hops.append(HopSpec(1, "bridge", "bridge", bridge, True))
|
||||||
|
else:
|
||||||
|
hops.append(HopSpec(1, "relay", "houseA", f"{entry}-r", False))
|
||||||
|
hops.append(HopSpec(2, "exit", exit_house, f"{exit_house}#x", False))
|
||||||
|
return CircuitSpec(
|
||||||
|
cell_id=f"RQ2/{topology}", rq="RQ2", seed=0, engine="docker",
|
||||||
|
topology=topology, bridge_present=bridge is not None, padding_applied=False,
|
||||||
|
matched_n=matched_n, houses=("houseA", exit_house), hops=tuple(hops),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_house_arm_uses_full_matched_n_pool():
|
||||||
|
# 1house-N: no federation narrows the pool → m_i = matched N for every circuit,
|
||||||
|
# regardless of which entry each circuit realized (ratified doc: A_i = all N).
|
||||||
|
specs = [_spec("1house-N", e, "houseA", None, matched_n=8) for e in ("a", "a", "b")]
|
||||||
|
assert rq2.observation_consistent_sizes(specs) == [8, 8, 8]
|
||||||
|
assert rq2.per_circuit_posteriors(specs) == [[1] * 8, [1] * 8, [1] * 8]
|
||||||
|
exp = miller_madow_entropy_bits([1] * 8)
|
||||||
|
assert all(abs(h - exp) < 1e-12 for h in rq2.per_circuit_entropy(specs))
|
||||||
|
|
||||||
|
|
||||||
|
def test_federated_anonymity_set_is_distinct_entries_per_exit_signature():
|
||||||
|
# Two exit-signature groups. Group brZ→houseB carries entries {a,a,b,c}=3
|
||||||
|
# distinct → m=3; group brY→houseB carries {d}=1 → m=1 (H=0).
|
||||||
|
specs = [
|
||||||
|
_spec("bridge-federated", "a", "houseB", "brZ"),
|
||||||
|
_spec("bridge-federated", "a", "houseB", "brZ"),
|
||||||
|
_spec("bridge-federated", "b", "houseB", "brZ"),
|
||||||
|
_spec("bridge-federated", "c", "houseB", "brZ"),
|
||||||
|
_spec("bridge-federated", "d", "houseB", "brY"),
|
||||||
|
]
|
||||||
|
assert rq2.observation_consistent_sizes(specs) == [3, 3, 3, 3, 1]
|
||||||
|
ent = rq2.per_circuit_entropy(specs)
|
||||||
|
assert abs(ent[0] - miller_madow_entropy_bits([1, 1, 1])) < 1e-12
|
||||||
|
assert ent[4] == 0.0 # singleton anonymity set → zero entropy
|
||||||
|
|
||||||
|
|
||||||
|
def test_bridge_concentration_is_per_circuit_load_share():
|
||||||
|
specs = [
|
||||||
|
_spec("bridge-federated", "a", "houseB", "brA"),
|
||||||
|
_spec("bridge-federated", "b", "houseB", "brA"),
|
||||||
|
_spec("bridge-federated", "c", "houseB", "brA"),
|
||||||
|
_spec("bridge-federated", "d", "houseB", "brB"),
|
||||||
|
]
|
||||||
|
assert rq2.bridge_concentration(specs) == [0.75, 0.75, 0.75, 0.25]
|
||||||
|
# top-3 distinct-bridge share over the run (frozen §6 k=3): (3 + 1)/4 = 1.0.
|
||||||
|
assert rq2.top_k_bridge_concentration(specs, k=3) == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_rq2_p3_pairs_drop_bridgeless_circuits():
|
||||||
|
specs = [
|
||||||
|
_spec("bridge-federated", "a", "houseB", "brA"),
|
||||||
|
_spec("bridge-federated", "b", "houseB", "brA"),
|
||||||
|
_spec("directory-federated", "c", "houseB", None), # no bridge → dropped
|
||||||
|
]
|
||||||
|
conc, ent = rq2.rq2_p3_pairs(specs)
|
||||||
|
assert len(conc) == 2 and len(ent) == 2 # only the two bridged circuits
|
||||||
|
assert conc == [1.0, 1.0] # both share brA (2/2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconstruct_run_is_deterministic_from_seeds():
|
||||||
|
# Drive the real assembler on chosen seeds (SYNTHETIC — no confirmatory dir).
|
||||||
|
cell = next(c for c in enumerate_cells()
|
||||||
|
if c.factors.get("topology") == "bridge-federated")
|
||||||
|
seeds = [derive_seed(cell.cell_id, i) for i in range(4)]
|
||||||
|
a = rq2.reconstruct_run(cell, seeds)
|
||||||
|
b = rq2.reconstruct_run(cell, seeds)
|
||||||
|
assert [s.fingerprint() for s in a] == [s.fingerprint() for s in b]
|
||||||
|
# Each reconstructed federated circuit spans >=2 houses (split knowledge).
|
||||||
|
assert all(s.span_houses() >= 2 for s in a)
|
||||||
|
# Sizes are computable and positive for every circuit.
|
||||||
|
assert all(m >= 1 for m in rq2.observation_consistent_sizes(a))
|
||||||
|
|
||||||
|
|
||||||
|
def test_feeds_confirm_rq2_p1_two_sided_shrink():
|
||||||
|
# Federated (small sets) vs matched-N single-house (full pool): ΔH < 0 here, so
|
||||||
|
# the frozen two-sided gate reports an honest shrink — exercises the wiring end
|
||||||
|
# to end (no confirmatory data; hand-built vectors).
|
||||||
|
federated = rq2.per_circuit_posteriors(
|
||||||
|
[_spec("bridge-federated", e, "houseB", "brZ") for e in ("a", "b")]
|
||||||
|
) # each group has 2 distinct entries → m=2
|
||||||
|
single = rq2.per_circuit_posteriors(
|
||||||
|
[_spec("1house-N", e, "houseA", None, matched_n=8) for e in ("a", "b")]
|
||||||
|
) # m=8
|
||||||
|
test = confirm.rq2_p1_delta_h(federated, single, seed=1, n_resamples=200)
|
||||||
|
assert test.name == "RQ2-P1" and test.effect == "ΔH"
|
||||||
|
assert test.ci.point < 0.0 and test.decision == "shrink"
|
||||||
Reference in New Issue
Block a user