R7/RQ3: executor-wiring stone — offline selector DVs + calibration gate
Wire the RQ3 churn-resilience companion (severable follow-on; RQ3 frozen in its
own prereg) as SYNTHETIC/OFFLINE build + calibration only — no confirmatory
battery run.
- battery: enumerate_rq3_cells()/rq3_schedule() add selector in {static(control),
random, agent} at 1house/bridge-off under pinned churn kp=30/steps=20, kept
SEPARATE so the frozen 6-cell lead lattice stays byte-identical.
- executor: run_rq3_cell_run collects offline selector DVs (throughput-retention,
drops/rebuilds, rebuild-interval gaps); added-latency is live-only (offline path
records None, never fabricated); run_rq3_battery(live=False) hard-raises.
- analysis/rq3_calibration: DRY offline gate. Churn-bites (1589 drops/1576
rebuilds) + rebuild-classifier calibration green with real teeth — churned(kp30)
vs baseline(kp5) AUC 0.926 separable, baseline-vs-baseline null 0.518 blind.
Classifier scored on the PER-RUN mean inter-rebuild gap (the confirmatory
grouping unit); the frozen instrument (rebuild_interval_gaps,
rebuild_classifier_auc) is untouched, no fit to confirmatory data.
HARD HOLD: no RQ3 confirmatory battery (live added-latency is operator+grid-gated).
Lead prereg SHA f22331a72e… untouched; containment intact (synthetic/offline, $0
local Ollama, frontier arm inert); worktree-only.
Tests: tests/test_sor_rq3_wiring.py 9 passed; full SOR suite 194 passed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+211
-2
@@ -35,18 +35,21 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import statistics
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from cmd_chat.sor import battery as sor_battery
|
||||
from cmd_chat.sor.analysis.detectors import bridge_correlation_auc, shannon_entropy_bits
|
||||
from cmd_chat.sor.analysis.metrics import compute_metrics, write_metrics
|
||||
from cmd_chat.sor.analysis.metrics import compute_metrics, throughput_retention, write_metrics
|
||||
from cmd_chat.sor.assembler import assemble
|
||||
from cmd_chat.sor.churn import churn_schedule
|
||||
from cmd_chat.sor.config import Domain, SorRng
|
||||
from cmd_chat.sor.forwarder import CircuitError, assert_isolated, run_circuit_fixture
|
||||
from cmd_chat.sor.provenance import Node, RunManifest, write_manifest
|
||||
from cmd_chat.sor.selector import SelectionResult
|
||||
from cmd_chat.sor.selector import SelectionResult, run_selection
|
||||
|
||||
DEFAULT_BINS = 32
|
||||
|
||||
@@ -331,3 +334,209 @@ def run_battery(
|
||||
path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
doc["_results_path"] = str(path)
|
||||
return doc
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# RQ3 — churn-resilient selector collection.
|
||||
#
|
||||
# The selector arm's DVs split cleanly into:
|
||||
# * OFFLINE (deterministic, no engine/traffic): throughput-retention, drops,
|
||||
# rebuilds, and the rebuild-interval-gap signal for the RQ3-P2 classifier —
|
||||
# all computed from the pure ``run_selection`` replay of the pinned churn
|
||||
# schedule. These need no live circuit and are collected here directly.
|
||||
# * LIVE (operator-GO-gated): the RQ3-P1-latency added-latency DV, which is a
|
||||
# real end-to-end wall-clock measurement of the assembled circuit standing up
|
||||
# on the isolated-docker grid. It is measured ONLY in the ``live=True`` path;
|
||||
# the offline path records ``added_latency_ms = None`` and NEVER fabricates it.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def rebuild_interval_gaps(result: SelectionResult) -> List[float]:
|
||||
"""The rebuild-interval-gap signal: sorted differences between successive
|
||||
rebuild steps. More churn → more frequent rebuilds → smaller gaps, so this is
|
||||
the feature the RQ3-P2 rebuild-pattern classifier separates on. Fewer than two
|
||||
rebuilds → no interval → empty (a real null observation, not fabricated)."""
|
||||
ts = sorted(rb.t for rb in result.rebuilds)
|
||||
return [float(b - a) for a, b in zip(ts, ts[1:])]
|
||||
|
||||
|
||||
def _rq3_pool(cell, size: int = 8) -> List[str]:
|
||||
"""The 1-house consenting-node pool the selector rebuilds over. Stable, labelled
|
||||
ids (house-local) — the selection substrate, not a live circuit."""
|
||||
house = cell.factors.get("topology", "1house")
|
||||
return [f"{house}/node{ix:02d}" for ix in range(size)]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RQ3RunReport:
|
||||
cell_id: str
|
||||
rq: str
|
||||
run_index: int
|
||||
seed: int
|
||||
strategy: str
|
||||
hops: int
|
||||
kill_prob_pct: int
|
||||
steps: int
|
||||
drops: int
|
||||
rebuilds: int
|
||||
deferred: int
|
||||
every_drop_rebuilt: bool
|
||||
throughput_retention: float
|
||||
rebuild_gaps: List[float]
|
||||
added_latency_ms: Optional[float] # measured only in the live path; else None
|
||||
run_dir: str
|
||||
|
||||
|
||||
def run_rq3_cell_run(
|
||||
cell,
|
||||
run_index: int,
|
||||
out_root: Path,
|
||||
*,
|
||||
engine: str = "docker",
|
||||
pool_size: int = 8,
|
||||
hops: int = 3,
|
||||
c_circuits: int = 0,
|
||||
payload_size: int = 4096,
|
||||
live: bool = False,
|
||||
) -> RQ3RunReport:
|
||||
"""Collect one RQ3 (cell, run): replay the pinned churn schedule under the cell's
|
||||
selector strategy and record the OFFLINE selector DVs (retention, drops, rebuilds,
|
||||
rebuild-interval gaps). If ``live`` is True, additionally stand up ``c_circuits``
|
||||
isolated-docker circuits and measure the per-run **median end-to-end latency** (the
|
||||
RQ3-P1-latency sample); otherwise ``added_latency_ms`` is left ``None`` (never
|
||||
fabricated). Writes a write-once ``rq3-run.json`` sidecar. Deterministic offline."""
|
||||
kp = int(cell.factors["churn_kill_prob_pct"])
|
||||
steps = int(cell.factors["churn_steps"])
|
||||
strategy = cell.factors.get("selector", "static")
|
||||
seed = sor_battery.derive_seed(cell.cell_id, run_index)
|
||||
run_dir = Path(out_root) / f"rq3-{cell.cell_id.replace('/', '_')}-r{run_index}"
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
nodes = _rq3_pool(cell, pool_size)
|
||||
schedule = churn_schedule(seed, nodes, steps, kill_prob_pct=kp)
|
||||
result = run_selection(seed, nodes, hops, schedule, strategy=strategy)
|
||||
gaps = rebuild_interval_gaps(result)
|
||||
|
||||
added_latency_ms: Optional[float] = None
|
||||
if live:
|
||||
# RQ3-P1-latency: a REAL end-to-end measurement, isolated-docker only. Held
|
||||
# behind the operator GO; never runs in the offline calibration/synthetic path.
|
||||
assert_isolated(engine)
|
||||
if shutil.which("docker") is None:
|
||||
raise ExecutorError("docker control plane not found — cannot measure live RQ3 latency")
|
||||
if c_circuits <= 0:
|
||||
raise ExecutorError("live RQ3 latency needs c_circuits > 0")
|
||||
samples: List[float] = []
|
||||
for c in range(c_circuits):
|
||||
cseed = _circuit_seed(seed, c)
|
||||
t0 = time.perf_counter()
|
||||
res = run_circuit_fixture(
|
||||
cseed, engine=engine, hops=hops, out_root=run_dir / "circuits",
|
||||
payload_size=payload_size,
|
||||
)
|
||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||
if not res.delivered:
|
||||
raise ExecutorError(f"RQ3 latency circuit {c} did not deliver for {cell.cell_id}")
|
||||
samples.append(dt_ms)
|
||||
added_latency_ms = statistics.median(samples)
|
||||
|
||||
doc = {
|
||||
"schema": "sor-rq3-run/1",
|
||||
"cell_id": cell.cell_id,
|
||||
"rq": cell.rq,
|
||||
"run_index": run_index,
|
||||
"seed": seed,
|
||||
"selector_strategy": result.strategy,
|
||||
"hops": hops,
|
||||
"pool_size": pool_size,
|
||||
"kill_prob_pct": kp,
|
||||
"steps": steps,
|
||||
"drops": result.drops,
|
||||
"rebuilds": len(result.rebuilds),
|
||||
"deferred": result.deferred,
|
||||
"every_drop_rebuilt": result.every_drop_rebuilt,
|
||||
"throughput_retention": throughput_retention(result),
|
||||
"rebuild_gaps": gaps,
|
||||
"added_latency_ms": added_latency_ms,
|
||||
"measured_from": "live-docker-e2e" if live else "offline-selection-replay",
|
||||
}
|
||||
path = run_dir / "rq3-run.json"
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
return RQ3RunReport(
|
||||
cell_id=cell.cell_id, rq=cell.rq, run_index=run_index, seed=seed,
|
||||
strategy=result.strategy, hops=hops, kill_prob_pct=kp, steps=steps,
|
||||
drops=result.drops, rebuilds=len(result.rebuilds), deferred=result.deferred,
|
||||
every_drop_rebuilt=result.every_drop_rebuilt,
|
||||
throughput_retention=throughput_retention(result), rebuild_gaps=gaps,
|
||||
added_latency_ms=added_latency_ms, run_dir=str(run_dir),
|
||||
)
|
||||
|
||||
|
||||
def run_rq3_battery(
|
||||
out_root: Path,
|
||||
*,
|
||||
engine: str = "docker",
|
||||
order_seed: int = sor_battery.S0,
|
||||
r_runs: int,
|
||||
c_circuits: int,
|
||||
pool_size: int = 8,
|
||||
hops: int = 3,
|
||||
live: bool = False,
|
||||
) -> Dict:
|
||||
"""Drive the RQ3 interleaved schedule. Like :func:`run_battery`, this CONFIRMATORY
|
||||
path requires ``live=True``: the pre-registered RQ3 report includes the
|
||||
RQ3-P1-latency added-latency DV, a real end-to-end measurement — so a ``live=False``
|
||||
call is refused rather than emit a battery missing (or fabricating) that DV. The
|
||||
offline selector DVs are exercised via :func:`run_rq3_cell_run` (and the calibration
|
||||
gate) directly; this launcher is the operator-gated live collection."""
|
||||
if not live:
|
||||
raise ExecutorError(
|
||||
"run_rq3_battery(live=False): the confirmatory RQ3 battery includes the "
|
||||
"RQ3-P1-latency end-to-end measurement, collected ONLY from real isolated-"
|
||||
"docker circuits. Pass live=True (operator-gated) to collect; the offline "
|
||||
"selector DVs are available via run_rq3_cell_run / the calibration gate."
|
||||
)
|
||||
assert_isolated(engine)
|
||||
out_root = Path(out_root)
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
schedule = sor_battery.rq3_schedule(order_seed, r=r_runs)
|
||||
by_cell = {c.cell_id: c for c in sor_battery.enumerate_rq3_cells()}
|
||||
schedule = [pr for pr in schedule if pr.cell_id in by_cell]
|
||||
|
||||
reports: List[RQ3RunReport] = []
|
||||
for pr in schedule:
|
||||
reports.append(run_rq3_cell_run(
|
||||
by_cell[pr.cell_id], pr.run_index, out_root, engine=engine,
|
||||
pool_size=pool_size, hops=hops, c_circuits=c_circuits, live=True,
|
||||
))
|
||||
|
||||
agg: Dict[str, Dict] = {}
|
||||
for rep in reports:
|
||||
a = agg.setdefault(rep.cell_id, {
|
||||
"strategy": rep.strategy, "runs": 0,
|
||||
"throughput_retention": [], "added_latency_ms": [],
|
||||
})
|
||||
a["runs"] += 1
|
||||
a["throughput_retention"].append(rep.throughput_retention)
|
||||
if rep.added_latency_ms is not None:
|
||||
a["added_latency_ms"].append(rep.added_latency_ms)
|
||||
|
||||
doc = {
|
||||
"schema": "sor-rq3-battery-results/1",
|
||||
"engine": engine,
|
||||
"order_seed": order_seed,
|
||||
"r_runs": r_runs,
|
||||
"c_circuits": c_circuits,
|
||||
"pool_size": pool_size,
|
||||
"hops": hops,
|
||||
"measured_from": "live-docker-e2e",
|
||||
"n_runs": len(reports),
|
||||
"cells": agg,
|
||||
"runs": [vars(r) for r in reports],
|
||||
}
|
||||
path = out_root / "rq3-battery-results.json"
|
||||
if not path.exists():
|
||||
path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
doc["_results_path"] = str(path)
|
||||
return doc
|
||||
|
||||
Reference in New Issue
Block a user