Files
hack-house/cmd_chat/sor/selector.py
T
leetcrypt 12fd2537e3 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>
2026-07-19 17:18:44 -07:00

172 lines
6.8 KiB
Python

"""R7 — Path selector that rebuilds circuits under churn.
The selector consumes a churn schedule (``churn.py``) and, whenever a live circuit
loses a node to a kill, **rebuilds** it from the currently-live pool. Three
strategies, all deterministic and offline:
* ``static`` — canonical-order pick (stable, no randomness);
* ``random`` — seeded PATH-stream pick from the live pool;
* ``agent`` — a *local* stability heuristic (prefers nodes that have churned
least). The paid frontier-model agent arm (GOAL autonomy envelope (c)) is
**human-gated and intentionally NOT wired here** — this strategy spends
nothing and calls no external model; it is the offline stand-in used to
validate the rebuild loop.
The R7 acceptance predicate this satisfies: under a fixed churn seed the selector
rebuilds every dropped circuit (as long as the live pool can supply ``hops``
nodes). Rebuild activity is emitted as R3 ``rebuild_start``/``rebuild_done`` +
``churn_kill``/``churn_spawn`` events when an :class:`EventLog` is supplied.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from cmd_chat.sor.churn import ChurnEvent
from cmd_chat.sor.config import Domain, SorRng
from cmd_chat.sor.events import EventLog
STRATEGIES = ("static", "random", "agent")
@dataclass
class Rebuild:
"""One rebuild triggered by a drop: the step, the node that dropped, and the
replacement circuit (node ids in order)."""
t: int
dropped: str
circuit: List[str]
@dataclass
class SelectionResult:
"""Outcome of replaying a churn schedule under a selector strategy."""
strategy: str
seed: int
hops: int
initial_circuit: List[str]
rebuilds: List[Rebuild] = field(default_factory=list)
drops: int = 0
deferred: int = 0 # drops that could not be rebuilt (pool < hops at that step)
events_sha256: Optional[str] = None
@property
def every_drop_rebuilt(self) -> bool:
"""True iff every drop that broke the circuit was met by a rebuild — the
R7 acceptance predicate (no deferred/unhealed drops)."""
return self.deferred == 0 and len(self.rebuilds) == self.drops
class Selector:
"""Picks a ``hops``-length circuit from a live pool per its strategy. Pure and
deterministic; the same (seed, strategy, live pool, churn count) always yields
the same circuit."""
def __init__(self, strategy: str, seed: int, hops: int) -> None:
if strategy not in STRATEGIES:
raise ValueError(f"unknown selector strategy {strategy!r} (of {STRATEGIES})")
self.strategy = strategy
self.seed = seed
self.hops = hops
self._draws = 0 # advances the random stream across rebuilds
self._kill_count: Dict[str, int] = {} # node -> times killed (agent signal)
def note_kill(self, node: str) -> None:
self._kill_count[node] = self._kill_count.get(node, 0) + 1
def build(self, live: List[str]) -> Optional[List[str]]:
"""Return a ``hops``-length circuit from ``live``, or ``None`` if the pool
is too small (fewer than ``hops`` live nodes)."""
if len(live) < self.hops:
return None
if self.strategy == "static":
return sorted(live)[: self.hops]
if self.strategy == "random":
pool = sorted(live)
s = SorRng(self.seed).stream(Domain.SELECTOR)
for _ in range(self._draws): # replay to current position (determinism)
s.next_u64()
picked: List[str] = []
work = list(pool)
for _ in range(self.hops):
j = s.next_below(len(work))
picked.append(work.pop(j))
self._draws += 1
return picked
# agent: local stability heuristic — fewest kills first, ties by id. No
# external model, no spend (the paid arm is human-gated, not wired here).
ranked = sorted(live, key=lambda n: (self._kill_count.get(n, 0), n))
return ranked[: self.hops]
def run_selection(
seed: int,
nodes: List[str],
hops: int,
schedule: List[ChurnEvent],
strategy: str = "static",
log: Optional[EventLog] = None,
) -> SelectionResult:
"""Replay ``schedule`` over ``nodes`` under ``strategy`` and rebuild the circuit
whenever a kill drops one of its nodes. Deterministic from the inputs.
Returns a :class:`SelectionResult`; ``result.every_drop_rebuilt`` is the R7
acceptance predicate. If ``log`` is given, emits R3 churn/rebuild events."""
sel = Selector(strategy, seed, hops)
live = {n: True for n in nodes}
live_list = [n for n in nodes if live[n]]
initial = sel.build(live_list) or []
circuit: List[str] = list(initial)
result = SelectionResult(strategy, seed, hops, list(initial))
for ev in schedule:
if ev.kind == "kill":
live[ev.node] = False
sel.note_kill(ev.node)
if log is not None:
log.emit("churn_kill", node_fp=_fp(ev.node), hop_index=ev.t)
if ev.node in circuit:
# The live circuit lost a hop -> must rebuild.
result.drops += 1
if log is not None:
log.emit("rebuild_start", circuit_id=f"t{ev.t}", hop_index=ev.t)
rebuilt = sel.build([n for n in nodes if live[n]])
if rebuilt is None:
result.deferred += 1 # pool too small right now
circuit = []
else:
circuit = rebuilt
result.rebuilds.append(Rebuild(ev.t, ev.node, list(rebuilt)))
if log is not None:
log.emit("rebuild_done", circuit_id=f"t{ev.t}",
hop_index=len(rebuilt))
else: # spawn
live[ev.node] = True
if log is not None:
log.emit("churn_spawn", node_fp=_fp(ev.node), hop_index=ev.t)
if not circuit:
# A deferred drop can now be healed once the pool recovers.
rebuilt = sel.build([n for n in nodes if live[n]])
if rebuilt is not None:
circuit = rebuilt
result.deferred = max(0, result.deferred - 1)
result.rebuilds.append(Rebuild(ev.t, ev.node, list(rebuilt)))
if log is not None:
log.emit("rebuild_done", circuit_id=f"t{ev.t}",
hop_index=len(rebuilt))
if log is not None:
result.events_sha256 = log.close()
return result
def _fp(node: str) -> str:
"""A short, stable fingerprint of a node id for event metadata (never the id
verbatim in case ids ever carry structure)."""
import hashlib
return hashlib.sha256(node.encode("utf-8")).hexdigest()[:8]