diff --git a/prod/clean_arch/violet/cadence.py b/prod/clean_arch/violet/cadence.py new file mode 100644 index 0000000..21f9c85 --- /dev/null +++ b/prod/clean_arch/violet/cadence.py @@ -0,0 +1,170 @@ +"""VIOLET V3b: Cadence Control Plane — per-action quantization. + +Spec: prod/docs/VIOLET_SPEC__CADENCE_CONTROL_PLANE.md (BINDING). + +VIOLET is a reactor substrate (V0 clock/DeadlineScheduler); BLUE's scan-quantized +behaviour is a guest hosted on it. Scan cadence is a QUANTIZATION SETTING, not the +architecture. Every scan-governed action is *evaluated* every reactor tick (the +would-be action shadow-logged) and *actuated* only when its per-action quantum Q is +crossed. Each action's Q is: + + - UNIVERSAL — every action (entry, sizing, each exit reason, each input plane) + has one; nothing is hardcoded at scan. SL merely DEFAULTS tighter. + - INDEPENDENT — changing one action's Q never touches another's. + - CONTROL-PLANE — readable/writable at runtime (HZ-backed via refresh_from); code + defaults are the floor when the control plane is silent. + - LOOSENABLE — Q steps down per-action on its own schedule, each step gated on + the shadow deltas (evaluate-vs-actuate) this layer makes visible. + +Exit-priority (CATASTROPHIC/ADVSL > mechanical TP > discretionary v7) is INVARIANT to +Q and is enforced by the decision engine, not here — a faster Q never lets a +discretionary exit mask a mechanical one (the LINK -$1,248 bug). +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass +from typing import Callable, Dict, Optional + +# ── quantum constants (nanoseconds) ─────────────────────────────────────────── +INSTA_Q_NS = 0 # actuate every reactor tick (no quantization) +OBF_Q_NS = 1_000_000_000 # ~1s — OBF effective cadence (fastest service) +SCAN_Q_NS = 5_000_000_000 # 5s — NG7 scan cadence (the certification anchor) + + +class Action(str, enum.Enum): + """Every scan-governed VIOLET action that carries an independent cadence knob.""" + + CATASTROPHIC_SL = "catastrophic_sl" + ADVSL = "advsl" + TP = "tp" + V7_EXIT = "v7_exit" + ENTRY = "entry" + SIZING = "sizing" + CONSUME_SCAN = "consume_scan" + CONSUME_OBF = "consume_obf" + CONSUME_EXF = "consume_exf" + CONSUME_ESOF = "consume_esof" + CONSUME_MARAS = "consume_maras" + CONSUME_ACB = "consume_acb" + + +# Default Q per action (spec §3). SL-class defaults to insta (the safety deviation); +# everything else defaults to its source cadence — all loosenable independently. +_DEFAULT_Q_NS: Dict[Action, int] = { + Action.CATASTROPHIC_SL: INSTA_Q_NS, + Action.ADVSL: INSTA_Q_NS, + Action.TP: SCAN_Q_NS, + Action.V7_EXIT: SCAN_Q_NS, + Action.ENTRY: SCAN_Q_NS, + Action.SIZING: SCAN_Q_NS, + Action.CONSUME_SCAN: SCAN_Q_NS, + Action.CONSUME_OBF: OBF_Q_NS, + Action.CONSUME_EXF: SCAN_Q_NS, + Action.CONSUME_ESOF: SCAN_Q_NS, + Action.CONSUME_MARAS: SCAN_Q_NS, + Action.CONSUME_ACB: SCAN_Q_NS, +} + + +@dataclass +class CadenceKnob: + """One action's tunable cadence. Mutable — the control plane is its only mutator. + + ``q_ns`` is the actuation quantum: 0 ⇒ actuate every reactor tick (insta); else + actuate once ``q_ns`` has elapsed since the last actuation. ``evaluate_every_tick`` + keeps the would-be action computed (and shadow-logged) at full reactor speed + regardless of Q. + """ + + action: Action + q_ns: int + evaluate_every_tick: bool = True + enabled: bool = True + source: str = "default" # "default" | "control_plane" + + def __post_init__(self) -> None: + if int(self.q_ns) < 0: + raise ValueError(f"q_ns must be >= 0 (got {self.q_ns} for {self.action})") + self.q_ns = int(self.q_ns) + + +class CadenceControlPlane: + """Runtime registry of per-action cadence knobs (spec §4). + + Seeded from code defaults; ``refresh_from`` layers a runtime provider (HZ map / + env) on top, per action, leaving untouched actions at their current value. + """ + + def __init__(self) -> None: + self._knobs: Dict[Action, CadenceKnob] = { + a: CadenceKnob(action=a, q_ns=q) for a, q in _DEFAULT_Q_NS.items() + } + + def get(self, action: Action) -> CadenceKnob: + return self._knobs[action] + + def set( + self, action: Action, *, + q_ns: Optional[int] = None, + evaluate_every_tick: Optional[bool] = None, + enabled: Optional[bool] = None, + source: str = "control_plane", + ) -> CadenceKnob: + """Independently override one action. Other actions are never touched.""" + k = self._knobs[action] + new = CadenceKnob( + action=action, + q_ns=k.q_ns if q_ns is None else int(q_ns), + evaluate_every_tick=k.evaluate_every_tick if evaluate_every_tick is None else bool(evaluate_every_tick), + enabled=k.enabled if enabled is None else bool(enabled), + source=source, + ) + self._knobs[action] = new + return new + + def refresh_from(self, provider: Callable[[Action], Optional[dict]]) -> int: + """Pull runtime overrides from a provider (HZ map / env reader). + + ``provider(action)`` returns a dict of overrides or None (no override → keep + current value, i.e. the code-default floor). Returns the count of actions + updated. The control plane stays authoritative for live tuning. + """ + n = 0 + for action in Action: + ov = provider(action) + if not ov: + continue + self.set(action, **{k: v for k, v in ov.items() + if k in ("q_ns", "evaluate_every_tick", "enabled")}) + n += 1 + return n + + def due(self, action: Action, now_ns: int, last_actuation_ns: Optional[int]) -> bool: + """Q-boundary test: should this action ACTUATE now? + + Disabled ⇒ never. ``last_actuation_ns is None`` (never actuated) ⇒ yes. + q_ns == 0 (insta) ⇒ every tick. Else ⇒ once the quantum has elapsed. + """ + k = self._knobs[action] + if not k.enabled: + return False + if last_actuation_ns is None: + return True + if k.q_ns == 0: + return True + return (int(now_ns) - int(last_actuation_ns)) >= k.q_ns + + # Alias matching spec §4 caller idiom (evaluate always, then consult to actuate). + should_actuate = due + + def snapshot(self) -> Dict[str, dict]: + """Surfaced view for live inspection (spec §4).""" + return { + a.value: { + "q_ns": k.q_ns, "evaluate_every_tick": k.evaluate_every_tick, + "enabled": k.enabled, "source": k.source, + } + for a, k in self._knobs.items() + } diff --git a/prod/clean_arch/violet/test_violet_cadence.py b/prod/clean_arch/violet/test_violet_cadence.py new file mode 100644 index 0000000..53093bc --- /dev/null +++ b/prod/clean_arch/violet/test_violet_cadence.py @@ -0,0 +1,96 @@ +"""V3b: Cadence Control Plane — defaults, independence, control-plane, Q boundaries.""" + +from __future__ import annotations + +import sys + +sys.path.insert(0, "/mnt/dolphinng5_predict") + +import pytest +from hypothesis import given, settings, strategies as st + +from prod.clean_arch.violet.cadence import ( + INSTA_Q_NS, OBF_Q_NS, SCAN_Q_NS, Action, CadenceControlPlane, CadenceKnob, +) + + +def test_defaults_match_spec_table(): + cp = CadenceControlPlane() + assert cp.get(Action.CATASTROPHIC_SL).q_ns == INSTA_Q_NS + assert cp.get(Action.ADVSL).q_ns == INSTA_Q_NS + assert cp.get(Action.TP).q_ns == SCAN_Q_NS + assert cp.get(Action.CONSUME_OBF).q_ns == OBF_Q_NS + assert cp.get(Action.ENTRY).q_ns == SCAN_Q_NS + # SL-class defaults strictly tighter than TP (the safety deviation). + assert cp.get(Action.CATASTROPHIC_SL).q_ns < cp.get(Action.TP).q_ns + # every action is registered (universality) and evaluates every tick. + for a in Action: + assert cp.get(a).evaluate_every_tick is True + + +def test_set_is_independent_per_action(): + cp = CadenceControlPlane() + before = {a: cp.get(a).q_ns for a in Action} + cp.set(Action.TP, q_ns=1_000_000_000) + assert cp.get(Action.TP).q_ns == 1_000_000_000 + assert cp.get(Action.TP).source == "control_plane" + # nothing else moved + for a in Action: + if a is Action.TP: + continue + assert cp.get(a).q_ns == before[a] + assert cp.get(a).source == "default" + + +def test_control_plane_override_beats_default_absence_falls_back(): + cp = CadenceControlPlane() + overrides = {Action.TP: {"q_ns": 250_000_000}, Action.ENTRY: {"enabled": False}} + n = cp.refresh_from(lambda a: overrides.get(a)) + assert n == 2 + assert cp.get(Action.TP).q_ns == 250_000_000 # overridden + assert cp.get(Action.ENTRY).enabled is False + assert cp.get(Action.SIZING).q_ns == SCAN_Q_NS # untouched → default floor + + +def test_negative_q_rejected_at_construction(): + with pytest.raises(ValueError): + CadenceKnob(action=Action.TP, q_ns=-1) + + +def test_due_insta_actuates_every_tick(): + cp = CadenceControlPlane() + assert cp.due(Action.CATASTROPHIC_SL, now_ns=1000, last_actuation_ns=999) is True + # never-actuated always due + assert cp.due(Action.TP, now_ns=0, last_actuation_ns=None) is True + + +def test_due_scan_respects_quantum(): + cp = CadenceControlPlane() + last = 1_000_000_000 + assert cp.due(Action.TP, now_ns=last + SCAN_Q_NS - 1, last_actuation_ns=last) is False + assert cp.due(Action.TP, now_ns=last + SCAN_Q_NS, last_actuation_ns=last) is True + + +def test_disabled_action_never_due(): + cp = CadenceControlPlane() + cp.set(Action.V7_EXIT, enabled=False) + assert cp.due(Action.V7_EXIT, now_ns=10**12, last_actuation_ns=None) is False + + +@given( + q=st.integers(min_value=0, max_value=10_000_000_000), + elapsed=st.integers(min_value=0, max_value=20_000_000_000), +) +@settings(max_examples=100, deadline=None) +def test_due_boundary_property(q, elapsed): + cp = CadenceControlPlane() + cp.set(Action.ENTRY, q_ns=q) + last = 5_000_000_000 + due = cp.due(Action.ENTRY, now_ns=last + elapsed, last_actuation_ns=last) + assert due == (q == 0 or elapsed >= q) + + +def test_snapshot_surfaces_all_actions(): + snap = CadenceControlPlane().snapshot() + assert set(snap.keys()) == {a.value for a in Action} + assert all({"q_ns", "enabled", "source"} <= set(v) for v in snap.values()) diff --git a/prod/docs/VIOLET_SPEC__CADENCE_CONTROL_PLANE.md b/prod/docs/VIOLET_SPEC__CADENCE_CONTROL_PLANE.md new file mode 100644 index 0000000..1aa3e44 --- /dev/null +++ b/prod/docs/VIOLET_SPEC__CADENCE_CONTROL_PLANE.md @@ -0,0 +1,97 @@ +# VIOLET Spec — Cadence Control Plane (per-action quantization) + +**Status:** BINDING for V3b (`prod/clean_arch/violet/cadence.py`). Authored 2026-06-13 +from operator doctrine this session. Companion to the master charter +(memory `violet_subsecond_rebuild_plan`, cadence-quantizer section) and +`violet_v3_alpha_doctrine` (points #6, #8). This spec is the painstaking per-ACTION +quantization layer the charter only sketched as a principle. + +--- + +## 1. The inversion (why this layer exists) + +BLUE's clock **is** its architecture: every decision/action is denominated in 5–6 s +scans. VIOLET inverts this — the architecture is the **event-driven / clockless +(fastest-clock) reactor** (V0: `clock.py` mono_ns timebase + `DeadlineScheduler`). +BLUE's scan-quantized behaviour is a **guest** hosted on that reactor. The scan +cadence is a **quantization setting, not the architecture.** + +At first, every action is quantized at **Q = scan cadence** → bit-faithful BLUE +replication (warts and all). Over time each action's Q can be **loosened +independently** toward the reactor's faster clock. This is the certification-preserving +path: champion params are 5 s-bar-denominated (BIBLE §22.3), so loosening is a +deliberate, measured, per-action promotion — never a global flip. + +## 2. Core rules + +1. **Evaluate at fastest cadence; actuate at Q.** Every action is *evaluated* every + reactor tick (the would-be action is computed and **shadow-logged** — this is the + evidence trail). The action is only *actuated* when its quantization boundary Q is + crossed. Shadow deltas (would-be-at-fast vs actuated-at-Q) are the data that + justifies each loosening step. +2. **Adjustability is UNIVERSAL.** EVERY scan-governed activity carries its own + tunable Q — entry, sizing, each exit reason (TP, catastrophic SL, ADVSL, v7), + AND each input-plane consumption (scan, OBF, ExoF, EsoF, MARAS, ACB). No action is + hardcoded at scan. SL merely **defaults** to a tighter Q; mechanically it is the + same adjustable primitive as everything else. +3. **Per-action knobs are INDEPENDENTLY configurable.** Changing TP's Q must not + touch entry's Q. Each action is its own registry entry. +4. **Knobs are SURFACED IN A CONTROL PLANE.** The registry is readable and writable at + runtime for live inspection and tuning — not compile-time constants. HZ-backed at + runtime (mirroring how BLUE reads `DOLPHIN_FEATURES["live_tp_threshold"]`); code + defaults are the floor/fallback when the control plane is silent. +5. **Loosening is per-reason and measured.** Q steps down per action on its own + schedule (e.g. 6s→3s→1s→500ms→100ms→50ms), each promotion gated on the shadow + deltas from rule (1). SL-class first; TP later (TP at scan has desirable properties). + +## 3. Per-action default Q table (initial state — all loosenable) + +| Action | Class | Default Q | Loosening intent | +|---|---|---|---| +| Catastrophic SL | exit/safety | **tight VIOLET / insta-exec** | sub-second from early (the versioned safety deviation) | +| ADVSL | exit/safety | tight VIOLET (after min-bars gate) | sub-second early | +| TP (mechanical) | exit | **scan** | loosen *cautiously* (desirable at scan) | +| v7 discretionary exit | exit | scan | as-is first; per-reason later | +| Entry | entry | scan | cautious (champion params 5s-denominated) | +| Sizing (conviction) | sizing | scan | cautious (couples to entry) | +| Scan plane consume | input | scan (~5–6s native) | n/a (source cadence) | +| OBF plane consume | input | **~1s** (500ms purported / ~1s effective) | toward VIOLET clock — fastest service | +| ExoF / EsoF / MARAS / ACB consume | input | scan | adjustable too, though sources are orders slower | + +Exit priority is INVARIANT regardless of Q: CATASTROPHIC/ADVSL > mechanical TP > +discretionary (v7). A faster Q never lets a discretionary exit mask a mechanical one +(the LINK −$1,248 bug). + +## 4. Design (`cadence.py`) + +- **`Action`** — enum/identifier per row of §3 (CATASTROPHIC_SL, ADVSL, TP, V7_EXIT, + ENTRY, SIZING, CONSUME_SCAN, CONSUME_OBF, CONSUME_EXF, CONSUME_ESOF, CONSUME_MARAS, + CONSUME_ACB). +- **`CadenceKnob`** (V-TYPES `StrictModel`-style, but mutable via the control plane) — + per action: `q_ns` (actuation quantum; 0 ⇒ every reactor tick = "insta"), + `evaluate_every_tick: bool` (default True), `enabled: bool`, `source: "default"|"control_plane"`. +- **`CadenceControlPlane`** — the registry: `get(action) -> CadenceKnob`, + `set(action, **overrides)` (independent per action), `snapshot() -> dict` (surfaced + for inspection), `refresh_from(provider)` where `provider` reads the HZ map / env at + runtime; code defaults seed it. `due(action, now_ns, last_actuation_ns) -> bool` + (Q-boundary test, integrates with `DeadlineScheduler`). +- **Evaluate/actuate split helper** — `should_actuate(action, now_ns) -> bool`; + callers always evaluate, then consult this to actuate, and emit the shadow-delta + telemetry on the gap. + +## 5. Tests (V3b) + +- defaults match §3 table; SL defaults tighter than TP. +- `set` on one action leaves all others unchanged (independence). +- control-plane override beats default; absence falls back to default (floor). +- `due`/`should_actuate` honors Q boundaries (insta Q=0 actuates every tick; scan Q + actuates once per scan interval), property-tested with hypothesis. +- evaluate-always invariant: evaluation count ≥ actuation count for every action. +- exit-priority invariant unaffected by Q (a fast discretionary Q never preempts a + mechanical exit in the ordering). + +## 6. Related + +`violet_subsecond_rebuild_plan` (charter) · `violet_v3_alpha_doctrine` #6/#8 · +`clock.py`/`DeadlineScheduler` (V0 substrate) · `decision_engine.py` (V3c consumer) · +`prod/docs/TODO_TP_SCAN_CADENCE_BUGFIX.md` (TP-at-scan rationale).