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
+9 -4
View File
@@ -11,8 +11,13 @@ confirmatory-cell data — CLAUDE.md build discipline). Specifically:
known-unlinked pair.
These are pure functions over in-memory series/distributions — they read no
pcaps, spawn no engine, move no traffic, and touch no VM fabric. The
traffic-moving R7 pieces (``churn.py`` seeded VM spin/kill; the live selector
rebuild loop; the confirmatory battery that writes ``metrics.json``) are NOT
here — they are HELD pending R4/R6 + a live grid (see OVERSEER-STATUS.md).
pcaps, spawn no engine, move no traffic, and touch no VM fabric.
The rest of R7 is landed alongside: ``metrics.py`` (this package) aggregates the
four DV families into a schema-valid, write-once ``metrics.json``; the seeded
churn schedule + rebuild loop live in ``cmd_chat/sor/{churn,selector}.py``. The
live VM spin/kill against the isolated hackhouse fabric, and the full
pre-registered confirmatory battery, remain gated by the containment law + the
human freeze (see OVERSEER-STATUS.md) — the churn schedule is seeded *data* here,
not a real VM operation.
"""
+113
View File
@@ -0,0 +1,113 @@
"""R7 — metrics.json aggregator (the DV writer for a SOR run).
Reads the offline detector primitives (``detectors.py``) plus a churn/selector
:class:`~cmd_chat.sor.selector.SelectionResult` and writes an immutable, schema-
valid ``metrics.json`` into a run directory. It aggregates the four DV families
the study reports:
* RQ1 — bridge-correlation AUC (linkability of ingress↔egress flows);
* RQ2 — anonymity-set entropy (bits) of the observed sender distribution;
* RQ3 — throughput retention under churn + a rebuild-pattern classifier AUC.
The detectors are calibrated on synthetic fixtures only (never fit to
confirmatory-cell data — CLAUDE.md build discipline). This module computes and
serializes; it moves no traffic and spawns no engine. ``metrics.json`` is written
once and never edited in place (artifact immutability)."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
from cmd_chat.sor.analysis.detectors import (
auc,
bridge_correlation_auc,
shannon_entropy_bits,
)
from cmd_chat.sor.selector import SelectionResult
METRICS_SCHEMA = "sor-metrics/1"
def throughput_retention(result: SelectionResult) -> float:
"""Fraction of dropped circuits that were successfully rebuilt (a proxy for
throughput retained under churn). 1.0 when every drop was healed; lower when
drops were deferred for lack of a live pool. No drops -> full retention."""
if result.drops <= 0:
return 1.0
healed = result.drops - result.deferred
return max(0.0, min(1.0, healed / result.drops))
def rebuild_classifier_auc(
churned_gaps: Sequence[float], baseline_gaps: Sequence[float]
) -> float:
"""AUC separating a churned run's rebuild-interval signal from a low-churn
baseline's — the RQ3 rebuild-pattern classifier. ≈1 when the two regimes are
cleanly separable, ≈0.5 when indistinguishable. Calibrated on labeled control
signals, not fit to confirmatory data."""
# Shorter gaps (more frequent rebuilds) mark the churned regime; score churned
# as the positive class on the negated gap so larger score = more churn.
pos = [-g for g in churned_gaps]
neg = [-g for g in baseline_gaps]
return auc(pos, neg)
def compute_metrics(
*,
sender_counts: Dict[str, int],
ingress: Sequence[Sequence[float]],
egress: Sequence[Sequence[float]],
selection: SelectionResult,
churned_gaps: Optional[Sequence[float]] = None,
baseline_gaps: Optional[Sequence[float]] = None,
) -> Dict[str, Any]:
"""Aggregate the four DV families into a metrics dict (not yet written)."""
metrics: Dict[str, Any] = {
"schema": METRICS_SCHEMA,
"seed": selection.seed,
"selector_strategy": selection.strategy,
"hops": selection.hops,
"rq1_bridge_correlation_auc": bridge_correlation_auc(ingress, egress),
"rq2_anonymity_entropy_bits": shannon_entropy_bits(sender_counts),
"rq2_sender_count": len([c for c in sender_counts.values() if c > 0]),
"rq3_throughput_retention": throughput_retention(selection),
"rq3_drops": selection.drops,
"rq3_rebuilds": len(selection.rebuilds),
"rq3_deferred": selection.deferred,
"rq3_every_drop_rebuilt": selection.every_drop_rebuilt,
}
if churned_gaps is not None and baseline_gaps is not None:
metrics["rq3_rebuild_classifier_auc"] = rebuild_classifier_auc(
churned_gaps, baseline_gaps
)
return metrics
def _validate(metrics: Dict[str, Any]) -> None:
required = (
"schema",
"rq1_bridge_correlation_auc",
"rq2_anonymity_entropy_bits",
"rq3_throughput_retention",
"rq3_every_drop_rebuilt",
)
missing = [k for k in required if k not in metrics]
if missing:
raise ValueError(f"metrics schema: missing keys {missing}")
if metrics["schema"] != METRICS_SCHEMA:
raise ValueError(f"metrics schema: unexpected {metrics['schema']!r}")
def write_metrics(run_dir: Path, metrics: Dict[str, Any]) -> Path:
"""Write ``metrics.json`` into ``run_dir`` (created if needed) and return its
path. Refuses to overwrite an existing metrics.json (write-once artifact)."""
_validate(metrics)
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
path = run_dir / "metrics.json"
if path.exists():
raise FileExistsError(f"metrics.json already exists (immutable): {path}")
path.write_text(json.dumps(metrics, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return path