12fd2537e3
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>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""R7 — Seeded churn schedule (deterministic node kill/spawn stream).
|
|
|
|
The churn generator produces a **schedule** — a reproducible list of kill/spawn
|
|
events drawn from the R1 ``Domain.CHURN`` stream — that models nodes dropping out
|
|
of and rejoining the grid over time. It is pure data: this module spins and kills
|
|
no real VM (that live half runs against the isolated hackhouse VM fabric and is
|
|
gated by the same containment law as the R4 forwarder). Producing the schedule
|
|
here, deterministically from the seed, is what lets the R7 acceptance check assert
|
|
that a fixed churn seed drives the selector to rebuild *every* dropped circuit —
|
|
verifiable entirely offline.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import List
|
|
|
|
from cmd_chat.sor.config import Domain, SorRng
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ChurnEvent:
|
|
"""One scheduled grid event at logical step ``t``. ``kind`` is ``"kill"`` or
|
|
``"spawn"``; ``node`` is the affected node id."""
|
|
|
|
t: int
|
|
kind: str # "kill" | "spawn"
|
|
node: str
|
|
|
|
|
|
def churn_schedule(
|
|
seed: int,
|
|
nodes: List[str],
|
|
steps: int,
|
|
kill_prob_pct: int = 30,
|
|
) -> List[ChurnEvent]:
|
|
"""Deterministically build a churn schedule over ``nodes`` for ``steps`` logical
|
|
steps, drawing from the seed's CHURN stream alone (so the same seed yields the
|
|
same schedule — the R7 determinism the selector check relies on).
|
|
|
|
At each step every currently-live node may be killed with probability
|
|
``kill_prob_pct``%, and every currently-dead node is respawned with the same
|
|
probability. Events are emitted in a stable (step, node) order."""
|
|
if not nodes or steps <= 0:
|
|
return []
|
|
s = SorRng(seed).stream(Domain.CHURN)
|
|
live = {n: True for n in nodes}
|
|
events: List[ChurnEvent] = []
|
|
for t in range(steps):
|
|
for n in nodes: # stable order -> stable schedule
|
|
roll = s.next_below(100)
|
|
if live[n]:
|
|
if roll < kill_prob_pct:
|
|
live[n] = False
|
|
events.append(ChurnEvent(t, "kill", n))
|
|
else:
|
|
if roll < kill_prob_pct:
|
|
live[n] = True
|
|
events.append(ChurnEvent(t, "spawn", n))
|
|
return events
|
|
|
|
|
|
def live_nodes_at(nodes: List[str], schedule: List[ChurnEvent], t: int) -> List[str]:
|
|
"""The set of live nodes at (through the end of) step ``t``, replaying the
|
|
schedule from the all-live initial state. Deterministic."""
|
|
live = {n: True for n in nodes}
|
|
for ev in schedule:
|
|
if ev.t > t:
|
|
break
|
|
live[ev.node] = ev.kind == "spawn"
|
|
return [n for n in nodes if live[n]]
|