R7 (live halves): seeded churn + rebuild selector + metrics.json

churn.py emits a seed-deterministic kill/spawn schedule (pure data — no real VM
spin/kill; the live fabric half stays gated by the containment law). selector.py
consumes the schedule and rebuilds a circuit whenever a kill drops one of its
hops, across static | random | agent strategies; the paid frontier-model agent
arm (GOAL envelope (c)) is human-gated and NOT wired — the agent strategy here is
a local stability heuristic that spends nothing. analysis/metrics.py aggregates
the four DV families (RQ1 correlation AUC, RQ2 entropy bits, RQ3 throughput
retention + rebuild-classifier AUC) into a schema-valid, write-once metrics.json.
Acceptance check green: under a fixed churn seed the selector rebuilds every
dropped circuit (every_drop_rebuilt, all strategies) and metrics.json is
produced. Python R7 selector suite 11 passed; full SOR suite 92 passed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-07-19 17:18:44 -07:00
parent 21263da8ca
commit 12fd2537e3
5 changed files with 502 additions and 4 deletions
+138
View File
@@ -0,0 +1,138 @@
"""R7 (live halves) — churn schedule + selector rebuild + metrics.json.
Acceptance predicate (roadmap R7, the traffic-side half): under a fixed churn
seed the selector rebuilds every dropped circuit, and metrics.json is produced.
(The offline detector calibration halves — correlator AUC≈1/0.5 and entropy
log2(N) — are covered in test_sor_analysis.py.)
All deterministic and offline: the churn schedule is seeded data (no real VM
spin/kill), the selector replays it, and the metrics writer aggregates the DVs.
The paid frontier-model ``agent`` arm is human-gated and NOT exercised here — the
``agent`` strategy under test is the local stability heuristic (no spend).
"""
import json
import pytest
from cmd_chat.sor.analysis.detectors import synthetic_bridge_fixture
from cmd_chat.sor.analysis.metrics import compute_metrics, write_metrics
from cmd_chat.sor.churn import ChurnEvent, churn_schedule, live_nodes_at
from cmd_chat.sor.events import EventLog
from cmd_chat.sor.selector import STRATEGIES, run_selection
NODES = [f"n{i}" for i in range(8)]
# --------------------------------------------------------------------------- #
# Churn schedule — seeded + deterministic.
# --------------------------------------------------------------------------- #
def test_churn_schedule_is_seed_deterministic():
a = churn_schedule(2024, NODES, steps=20)
b = churn_schedule(2024, NODES, steps=20)
assert a == b
assert churn_schedule(2025, NODES, steps=20) != a
def test_churn_schedule_has_kills_and_spawns():
sched = churn_schedule(7, NODES, steps=40, kill_prob_pct=40)
kinds = {ev.kind for ev in sched}
assert "kill" in kinds and "spawn" in kinds
# A killed node is not live until it is spawned again.
first_kill = next(ev for ev in sched if ev.kind == "kill")
assert first_kill.node not in live_nodes_at(NODES, sched, first_kill.t)
def test_empty_inputs_yield_empty_schedule():
assert churn_schedule(1, [], steps=10) == []
assert churn_schedule(1, NODES, steps=0) == []
# --------------------------------------------------------------------------- #
# Selector — rebuilds every dropped circuit under a fixed churn seed.
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("strategy", STRATEGIES)
def test_selector_rebuilds_every_dropped_circuit(strategy):
# Pool (8) comfortably exceeds hops (3), so every drop can be healed.
seed = 0xBADC0DE
sched = churn_schedule(seed, NODES, steps=60, kill_prob_pct=35)
result = run_selection(seed, NODES, hops=3, schedule=sched, strategy=strategy)
assert len(result.initial_circuit) == 3
assert result.drops > 0 # the churn actually perturbed the circuit
assert result.every_drop_rebuilt # THE R7 acceptance predicate
assert result.deferred == 0
assert len(result.rebuilds) == result.drops
# Every rebuilt circuit is a full, distinct-node, currently-plausible path.
for rb in result.rebuilds:
assert len(rb.circuit) == 3
assert len(set(rb.circuit)) == 3
def test_selector_is_deterministic():
seed = 42
sched = churn_schedule(seed, NODES, steps=30)
r1 = run_selection(seed, NODES, hops=3, schedule=sched, strategy="random")
r2 = run_selection(seed, NODES, hops=3, schedule=sched, strategy="random")
assert [rb.circuit for rb in r1.rebuilds] == [rb.circuit for rb in r2.rebuilds]
def test_unknown_strategy_refused():
with pytest.raises(ValueError):
run_selection(1, NODES, hops=3, schedule=[], strategy="frontier-gpt")
def test_selection_emits_rebuild_events(tmp_path):
seed = 0xBADC0DE
sched = churn_schedule(seed, NODES, steps=40, kill_prob_pct=35)
log = EventLog(tmp_path, "sel-run", seed=seed)
result = run_selection(seed, NODES, hops=3, schedule=sched,
strategy="static", log=log)
assert result.events_sha256 and len(result.events_sha256) == 64
lines = (tmp_path / "events.jsonl").read_text().strip().splitlines()
events = [json.loads(ln)["event"] for ln in lines]
assert "churn_kill" in events
assert events.count("rebuild_done") == len(result.rebuilds)
# --------------------------------------------------------------------------- #
# metrics.json — produced + schema-valid.
# --------------------------------------------------------------------------- #
def test_metrics_json_is_produced(tmp_path):
seed = 0xBADC0DE
sched = churn_schedule(seed, NODES, steps=60, kill_prob_pct=35)
result = run_selection(seed, NODES, hops=3, schedule=sched, strategy="static")
ingress, egress = synthetic_bridge_fixture(0xC0FFEE, linked=True) # AUC ~1
metrics = compute_metrics(
sender_counts={f"s{i}": 1 for i in range(8)}, # 8 equiprobable -> 3 bits
ingress=ingress,
egress=egress,
selection=result,
churned_gaps=[1.0, 1.5, 1.0, 2.0],
baseline_gaps=[10.0, 12.0, 11.0, 13.0],
)
path = write_metrics(tmp_path / "run", metrics)
assert path.exists()
loaded = json.loads(path.read_text())
assert loaded["schema"] == "sor-metrics/1"
assert loaded["rq2_anonymity_entropy_bits"] == pytest.approx(3.0)
assert loaded["rq1_bridge_correlation_auc"] == pytest.approx(1.0)
assert loaded["rq3_every_drop_rebuilt"] is True
assert 0.0 <= loaded["rq3_throughput_retention"] <= 1.0
assert loaded["rq3_rebuild_classifier_auc"] == pytest.approx(1.0)
def test_metrics_json_is_write_once(tmp_path):
result = run_selection(1, NODES, hops=3, schedule=[], strategy="static")
metrics = compute_metrics(
sender_counts={"a": 1, "b": 1},
ingress=[[1, 2]],
egress=[[1, 2]],
selection=result,
)
run = tmp_path / "run"
write_metrics(run, metrics)
with pytest.raises(FileExistsError): # immutable artifact
write_metrics(run, metrics)