kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
491 lines (458 loc) • 30.8 kB
text/typescript
/**
* # session/harness/plan-fixture — freeze one authored Plan once, fan it out to N watchers (ADR-0032 §7)
*
* ADR-0032 §7 describes — in prose only — the paradigm the cascade exists to serve: a Fable strategist
* authors the day's Plan+Brief+Mandate+View **once** (the few expensive frontier judgments), that authored
* frame is **content-hashed and frozen** (`plan_fixture_sha`), and then **dozens/hundreds of small/fast
* watchers** run against the byte-identical frozen plan+frame. This module is that paradigm in code, with
* ZERO determinism-core change — it composes primitives that already exist:
*
* - **freeze-once** — {@link capturePlanFixture} snapshots the strategist's OPEN turn
* ({@link CapturedTurns}[{@link OPEN_WAKE_KEY}], the armed Plan) plus its {@link Mandate}/{@link Brief}/View
* context, canonicalizes it (sorted keys, dropped `undefined` — the SAME discipline as the receipt/Blotter
* content-address) and content-hashes it via {@link sha256} → a stable {@link PlanFixture.planFixtureSha}.
* - **replay-many, zero model calls** — {@link recordedStrategistOf} rehydrates a fixture into a strategist
* tier backed by the EXISTING {@link recordedAgent}: the frozen OPEN turn is re-emitted byte-identically on
* every run with no provider call (ADR-0032 §6, "recordedAgent replays the cascade byte-identically").
* - **N-watcher fan-out** — {@link fanWatchers} builds one {@link cascadeAgent} per watcher config over the
* recorded strategist and the SAME frozen frame (tape+wakes+fill+mandate), runs each through the EXISTING
* `runSimulateSession`, and collects grade + {@link attributeCascade attribution} into per-ConfigId grid
* columns via the EXISTING {@link buildGrid}. One authored plan → N deterministic columns.
*
* ## The determinism claim the sha buys
* `plan_fixture_sha` is a total function of the fixture's DATA (the OPEN turn + mandate + brief + view + the
* strategist config), never its construction order or the wall clock. So: the SAME authored plan reloads to
* the SAME sha (a cache hit is a byte-identity, not a re-author); ANY changed byte in the frozen plan yields a
* NEW sha (no silent contamination — a stale/edited fixture can never masquerade as the frozen one). The
* loader ({@link readPlanFixture}) re-derives the sha on read and refuses a fixture whose stored sha does not
* match its content (fail-closed, RUNTIME §8).
*
* ## Scope (owner v1)
* The seed fixture is authored from a KNOWN-VALID Plan document (the `@ spot` live-authoring grammar bug is
* OUT OF SCOPE — see bd issues), so the fan-out is exercised independently of live authoring. A rehydrated
* strategist replays only its OPEN turn; it captured no re-brief, so on a watcher escalation the fan runs in
* `frozenStrategist` mode ({@link cascadeAgent}): the over-reach is REFUSED and the frozen plan is HELD ARMED
* (fail-closed to a PASS) rather than torn down, so the fan measures watcher management against a live armed
* plan instead of collapsing to $0 (docs/results/author-and-fan/, PR #8). This is confined to the recorded
* fan — the LIVE cascade still disarms/re-briefs exactly as before, and the admission Gate is untouched.
*/
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import {
OPEN_WAKE_KEY,
recordedAgent,
fixedPlanAgent,
BACKTEST_CONFIG,
type Agent,
type AgentConfig,
type AgentTurn,
type CapturedTurn,
type CapturedTurns,
type SupersedeAction,
type WakeKey,
} from "../agent.ts";
import { runSimulateSession, type SimulateResult } from "../simulate.ts";
import { buildGrid, type Grid } from "../grid.ts";
import { cellConfigId } from "../config.ts";
import { cascadeAgent, cascadeConfigOf, attributeCascade, type Attribution, type CascadeStep } from "./cascade.ts";
import { sha256 } from "../../crypto/sha256.ts";
import { canonicalizeJson } from "../../canonical/json.ts";
import { assertGradedMandate } from "../../frame/types.ts";
import { assertBriefHashHonest } from "../../frame/brief.ts";
import type { Blotter } from "../../blotter/index.ts";
import type { BusEvent } from "../../bus/types.ts";
import type { Brief, Mandate } from "../../frame/types.ts";
import type { FillModelName } from "../sim.ts";
import type { TimeOfDay } from "../day.ts";
// ─────────────────────────────────────────────────────────────────────────────
// The frozen plan fixture — freeze-once (ADR-0032 §7)
// ─────────────────────────────────────────────────────────────────────────────
/** The DATA that is frozen and content-hashed — everything the fan-out replays, WITHOUT the sha itself.
* `openTurn` is the strategist's captured OPEN turn ({@link CapturedTurns}[{@link OPEN_WAKE_KEY}] — the armed
* Plan/journal); `config` is the strategist's advertised {@link AgentConfig} (the tier identity a rehydrated
* {@link recordedAgent} carries); `mandate`/`brief`/`view` are the frame context authored alongside the Plan
* (ADR-0026 — the hard Mandate, the soft Brief, the View lens). Absent optionals are dropped by the
* canonicalizer, so a fixture that carries no Brief hashes identically whether the field is absent or omitted. */
export interface PlanFixtureContent {
/** The strategist's captured OPEN turn — the armed Plan (a `supersede` document) + its journal. */
readonly openTurn: AgentTurn;
/** The strategist tier's advertised config — the identity a rehydrated {@link recordedAgent} replays under. */
readonly config: AgentConfig;
/** The hard {@link Mandate} authored for the frame (ADR-0026). */
readonly mandate?: Mandate;
/** The soft {@link Brief} authored for the frame (ADR-0026). */
readonly brief?: Brief;
/** The View lens document the strategist authored the plan under (a Kestrel `VIEW` statement), when forced. */
readonly view?: string;
}
/** A frozen, content-addressed plan fixture: the {@link PlanFixtureContent} plus its stable
* `planFixtureSha` (the ADR-0032 §7 `plan_fixture_sha`). A cache KEY — the same authored plan always mints
* the same sha; any changed byte mints a new one. */
export interface PlanFixture extends PlanFixtureContent {
/** `sha256` over the canonical JSON of the {@link PlanFixtureContent} (ADR-0032 §7 `plan_fixture_sha`). */
readonly planFixtureSha: string;
}
/** The exact bytes {@link planFixtureSha} hashes — the ONE {@link canonicalizeJson canonical byte order}
* (canonical/json: sorted keys, dropped `undefined`, identical to the receipt / Blotter canonicalizer) over
* ONLY the frozen content (never the sha), so a fixture's bytes depend only on its DATA. */
function canonicalContent(content: PlanFixtureContent): string {
return canonicalizeJson({ openTurn: content.openTurn, config: content.config, mandate: content.mandate, brief: content.brief, view: content.view });
}
/**
* The ADR-0032 §7 `plan_fixture_sha` — `sha256` over the canonical JSON of the frozen {@link
* PlanFixtureContent}. PURE and order-insensitive: the same authored plan (same OPEN turn + mandate + brief +
* view + config) in ANY key order folds to the SAME 64-hex sha; ANY changed byte yields a different sha. No
* wall clock, no RNG.
*/
export function planFixtureSha(content: PlanFixtureContent): string {
return sha256(canonicalContent(content));
}
/**
* Freeze an authored plan: bind the {@link PlanFixtureContent} to its {@link planFixtureSha}.
*
* ## Fail-closed at the HASH-COMMIT boundary (kestrel-voy9)
* `plan_fixture_sha` is the content-address that binds "this performance came from this thesis" into
* grade provenance (ADR-0032 §7), and the deterministic grade driver ({@link runSimulateSession})
* never renders — so honesty is enforced HERE, where the mandate + brief COMMIT, not at a render seam
* the grade never reaches:
* - {@link assertGradedMandate} — a frozen graded plan MUST declare a complete {@link Mandate}
* (objective + `1R=$X>0` + success criterion + bounded-risk rule); a missing/incomplete mandate
* can never mint a stable sha.
* - {@link assertBriefHashHonest} — a supplied Brief whose `hash` LIES about its `text` (a raw
* `{text,hash}` literal that bypassed {@link makeBrief}) can never mint a sha that binds the wrong
* thesis; the mismatch is refused before the content is hashed.
* Pure; deterministic. Throws {@link KernelHonestyError} on either violation.
*/
export function capturePlanFixture(content: PlanFixtureContent): PlanFixture {
assertGradedMandate(content.mandate);
if (content.brief !== undefined) assertBriefHashHonest(content.brief);
return { ...content, planFixtureSha: planFixtureSha(content) };
}
/** Additional frame context to fold into a fixture captured from a run (the {@link Mandate}/{@link Brief}/View
* the run was authored under — a {@link SimulateResult} does not carry them back). */
export interface PlanFixtureFromRunOptions {
readonly mandate?: Mandate;
readonly brief?: Brief;
readonly view?: string;
}
/**
* Freeze the strategist's OPEN turn out of a completed {@link SimulateResult} — the freeze-once step after a
* ONE-TIME authoring run. Reads {@link SimulateResult.captured}[{@link OPEN_WAKE_KEY}] (the armed Plan) and
* {@link SimulateResult.config} (the strategist identity), folds in the supplied frame context, and
* content-hashes. Fail-closed: a result with no captured OPEN turn throws (a strategist that stood down at
* open authored no plan to freeze).
*/
export function planFixtureFromRun(res: SimulateResult, opts: PlanFixtureFromRunOptions = {}): PlanFixture {
// The fixture freezes the TURN alone (never the capture's deliberation-cost wrapper, ADR-0040):
// `planFixtureSha` is a content address of the authored plan, so a clocked authoring run and a
// latency-blind one freeze byte-identical fixtures for the same authored document.
const openTurn = res.captured.get(OPEN_WAKE_KEY)?.turn;
if (openTurn === undefined) {
throw new Error(`planFixtureFromRun: run captured no OPEN turn (key ${OPEN_WAKE_KEY}) — nothing to freeze`);
}
return capturePlanFixture({
openTurn,
config: res.config,
...(opts.mandate !== undefined ? { mandate: opts.mandate } : {}),
...(opts.brief !== undefined ? { brief: opts.brief } : {}),
...(opts.view !== undefined ? { view: opts.view } : {}),
});
}
/** Pull the frozen Plan SOURCE document out of a fixture's OPEN turn (the `supersede` document), or `null`
* when the OPEN turn armed nothing (a stand-down). The frozen-Plan counterfactual arm ({@link fanWatchers})
* replays THIS document alone through {@link fixedPlanAgent}. */
export function fixturePlanDoc(fixture: PlanFixture): string | null {
const sup = fixture.openTurn.actions.find((a): a is SupersedeAction => a.kind === "supersede");
return sup?.document ?? null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Rehydrate a fixture into a strategist tier — replay-many, ZERO model calls
// ─────────────────────────────────────────────────────────────────────────────
/**
* Rehydrate a frozen fixture into a strategist {@link Agent} backed by the EXISTING {@link recordedAgent}: its
* `open` re-emits the frozen OPEN turn byte-identically with NO provider call; its `decide` (only reached on a
* watcher escalation) fails closed to a stand-down (the frozen plan captured no re-brief turn). Because this
* tier IS a recording, {@link fanWatchers} runs the cascade in `frozenStrategist` mode so that fail-closed
* stand-down HOLDS the frozen plan armed rather than disarming it (see {@link cascadeAgent}). This is the tier
* every fan-out watcher runs against.
*/
export function recordedStrategistOf(fixture: PlanFixture): Agent {
// COSTLESS by construction: the fixture froze the authored turn, not a deliberation cost, so the
// rehydrated strategist replays only latency-blind (a clock-honest replay refuses at open, ADR-0040 §7).
const turns: CapturedTurns = new Map<WakeKey, CapturedTurn>([[OPEN_WAKE_KEY, { turn: fixture.openTurn }]]);
return recordedAgent(fixture.config, turns);
}
// ─────────────────────────────────────────────────────────────────────────────
// Persist / load — the fixtures dir (the cache)
// ─────────────────────────────────────────────────────────────────────────────
/** Serialize a fixture to pretty JSON (the on-disk cache form). Pure. */
export function serializePlanFixture(fixture: PlanFixture): string {
return JSON.stringify(fixture, null, 2) + "\n";
}
/** Write a fixture to `path` (creating parent dirs) — persist to the fixtures dir. */
export function writePlanFixture(fixture: PlanFixture, path: string): void {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, serializePlanFixture(fixture), "utf8");
}
/**
* Load a fixture from `path` and RE-VERIFY its content hash. Fail-closed: a fixture whose stored
* `planFixtureSha` does not match a fresh {@link planFixtureSha} of its content is REFUSED (a contaminated /
* hand-edited fixture can never masquerade as the frozen plan). A clean reload returns byte-identical content
* and the SAME sha — the cache-hit guarantee.
*
* BRIEF-HASH honesty on read (kestrel-voy9): the outer sha only proves the bytes are self-consistent, NOT
* that a supplied `brief.hash` tells the truth about `brief.text` — a hand-forged fixture can carry a matching
* outer sha over a LYING brief hash (it would content-address, and thus bind into grade provenance, the wrong
* thesis). {@link assertBriefHashHonest} re-derives and REFUSES it before the fixture can be replayed.
*/
export function readPlanFixture(path: string): PlanFixture {
const raw = JSON.parse(readFileSync(path, "utf8")) as PlanFixture;
const { planFixtureSha: stored, ...content } = raw;
const recomputed = planFixtureSha(content);
if (recomputed !== stored) {
throw new Error(
`readPlanFixture: plan_fixture_sha mismatch — stored ${stored} != recomputed ${recomputed} ` +
`(the fixture at ${path} is contaminated; refusing to replay a plan that is not the frozen one)`,
);
}
if (raw.brief !== undefined) assertBriefHashHonest(raw.brief);
return raw;
}
// ─────────────────────────────────────────────────────────────────────────────
// fanWatchers — one frozen plan → N watchers → N deterministic grid columns
// ─────────────────────────────────────────────────────────────────────────────
/** One watcher in the fan-out: its manage-only {@link Agent} (recorded / stub / prompted) and an optional
* human label for the column (defaults to the watcher config's own `label`). */
export interface WatcherSpec {
readonly watcher: Agent;
/** Optional display label for this column; the grid ConfigId is derived from the cascade config regardless. */
readonly label?: string;
}
/** The frozen frame every watcher runs against — the byte-identical tape + cadence + fill + mandate + brief
* (ADR-0032 §7: "the identical frozen plan+frame"). Mirrors the `runSimulateSession` knobs the cascade tracer
* uses, so the ONLY variable across columns is the watcher tier. */
export interface FanFrame {
readonly events: readonly BusEvent[];
readonly wakes?: readonly TimeOfDay[];
readonly fillModel: FillModelName;
readonly rUsd: number;
readonly mandate?: Mandate;
readonly brief?: Brief;
/** The buy-and-hold / null baseline EV for the `strategist_alpha` leg; defaults to the frozen-plan EV
* (⇒ `strategistAlpha = 0`, an honest null when no benchmark is supplied). */
readonly benchmarkEv?: number;
/**
* Opt in to the PER-WAKE mark-to-model of held option legs (the theta cell — kestrel theta-cell seam b,
* {@link import("../../fill/mark-to-model.ts").markHeldLegsPerWake}). `false`/absent (every existing
* cell) ⇒ held options are marked only at the terminal intrinsic settle, exactly as today (pinned
* floors unchanged, byte-identical). `true` (the FOMC-OPTIONS theta cell) ⇒ a held straddle's value
* decays between wakes as theta bleeds, so standing down on a bleeding position is natively costly.
*/
readonly markToModel?: boolean;
}
/** The per-column participation summary the fan surfaces so a run can MEASURE PASS-vs-act-vs-held rather
* than INFER it from P&L (kestrel harness-observability, Bug 2). The four buckets PARTITION every invoked
* ordinal (open + each wake) exactly once — a held/escalated turn is counted DISTINCTLY, never pooled into
* the PASS bucket. Derived from the cascade routing {@link CascadeStep} log + the captured turns. */
export interface ColumnSummary {
/** Non-escalated ordinals whose terminal turn EMITTED at least one action (`actions.length > 0`). */
readonly actedTurns: number;
/** Non-escalated ordinals whose terminal turn was a PASS (empty actions — a genuine stand-pat). */
readonly passTurns: number;
/** Ordinals where the watcher escalated and the frozen plan was HELD ARMED (over-reach refused). */
readonly escalationHeldTurns: number;
/** Ordinals where the watcher escalated and the strategist RE-BRIEFED (its turn crossed to the Bus). */
readonly escalationRebriefTurns: number;
}
/** One fan-out column: the cascade run for one watcher against the frozen plan, keyed by its cell ConfigId. */
export interface WatcherColumn {
/** The cell config-axis id ({@link cellConfigId}) of this cascade column — its distinct grid key. */
readonly configId: string;
/** The cascade config's label (`cascade(strategist»watcher)`) — legible column name. */
readonly label: string;
/** The graded Blotter of this watcher's cascade run. */
readonly blotter: Blotter;
/** The captured cascade tape (the byte-identical replay input for this column). */
readonly captured: CapturedTurns;
/** The three-level attribution over the frozen-plan counterfactual (ADR-0032 §6). */
readonly attribution: Attribution;
/** The cascade routing audit for this column — one {@link CascadeStep} per invoked ordinal (open + each
* wake), including any escalation-held / escalation-rebrief events (kestrel harness-observability, Bug 2). */
readonly steps: readonly CascadeStep[];
/** The PASS/act/held participation summary derived from {@link steps} + {@link captured} (Bug 2). */
readonly summary: ColumnSummary;
}
/** The result of a fan-out: the frozen plan's sha, the frozen-plan EV (the shared counterfactual baseline),
* one {@link WatcherColumn} per watcher, and the aggregated comparison {@link Grid} (one cell per watcher). */
export interface FanResult {
/** The frozen plan's `plan_fixture_sha` — the cache key every column shares. */
readonly planFixtureSha: string;
/** `EV(frozen strategist Plan, replayed pure-algo, no watcher)` — the armed Plan alone (the shared baseline). */
readonly frozenPlanEv: number;
readonly columns: readonly WatcherColumn[];
/** The {@link buildGrid} of all cascade columns — N distinct cells (one per watcher ConfigId). */
readonly grid: Grid;
}
/**
* OPT-IN CLOCK-HONEST fan mode (ADR-0040 / clock-honest wakes §7, bd kestrel-yb75). Supplied to
* {@link fanWatchers}, it makes EACH watcher column's cascade run CLOCKED: the real Simulate driver measures
* the wall-clock deliberation cost of every Seat turn at its `now` seam and records `measuredMs` into that
* column's {@link CapturedTurns} (the persisted replay corpus tracer-1 lifted, PR #223). ABSENT (the
* default) ⇒ the fan runs LATENCY-BLIND, byte-identical to before this seam existed.
*
* The eligibility gate stays SOVEREIGN: this mode does not bypass the driver's own
* {@link import("../simulate.ts").clockHonestIneligibility} refusal at open — an ineligible/undeclared
* `dataRung` (minute/session/absent/off-lattice) still REFUSES the run loudly. And the shared frozen-plan
* counterfactual ({@link FanResult.frozenPlanEv}) is UNTOUCHED: it has no Seat deliberating (pure-algo), so
* it stays latency-blind and deterministic — only the watcher columns are clocked.
*/
export interface ClockHonestFan {
/** The network/infra latency allowance added to EVERY measured Seat turn (integer ms ≥ 0). The driver
* has NO default — a clock-honest run without it is refused at open (never a designed default); `0` is
* legal and declares a colocated seat. A ConfigId axis (a clocked column never pools with an unclocked). */
readonly latencyBufferMs: number;
/** The tape's DECLARED observability rung — threaded to the driver, which VERIFIES it fail-closed
* (`tick`/`second` eligible; `minute`/`session`/absent/off-lattice REFUSED at open). Declared, never
* trusted here — the sovereign gate lives in the driver. */
readonly dataRung: string;
/** The injectable wall clock the driver measures with. Defaults to {@link monotonicNow}(`Date.now`).
* Whatever is supplied is wrapped in {@link monotonicNow} so a broken clock FAILS CLOSED. */
readonly now?: () => number;
}
/**
* A CLOCK-HONEST MEASUREMENT DEFECT — broken timing (a dead/backward clock) or a capture that failed the
* clock-honest replayability belt (PR #226 review, finding C). Distinct from a Seat/model failure so a
* downstream attrition/coverage read can bucket "the measurement machinery failed" separately from "the
* model failed" instead of regex-matching free-text `error` strings. Thrown by {@link monotonicNow} and by
* the roster's fail-closed capture belt; `instanceof` survives propagation through the real driver. */
export class ClockHonestDefectError extends Error {}
/**
* Wrap a wall clock so the clock-honest measurement path FAILS CLOSED on broken timing (bd kestrel-yb75).
* A reading that is NON-FINITE (`NaN`/`Infinity` — a dead clock) or that STEPS BACKWARD (a non-monotonic
* clock — an NTP slew) THROWS instead of yielding a bogus `measuredMs`: a backward step would round to `0`
* and silently claim a colocated seat, and a `NaN` would serialize to `null` under a clock-honest label —
* exactly the "never silently record measuredMs=0 or a fabricated cost" hazard §7 forbids. The throw
* propagates out of the real driver so the column is marked CRASHED (never a clock-honest capture carrying
* fabricated cost). A well-behaved clock (`Date.now` in normal operation; a constant injected clock, which
* measures a legitimate `0`) is unaffected. Pure factory; the returned closure carries the last reading.
*/
export function monotonicNow(base: () => number): () => number {
let last = Number.NEGATIVE_INFINITY;
return () => {
const v = base();
if (!Number.isFinite(v)) {
throw new ClockHonestDefectError(
`clock-honest measurement: wall clock returned a non-finite reading ${JSON.stringify(v)} — a broken clock cannot ground a measured deliberation cost (fail closed, bd kestrel-yb75)`,
);
}
if (v < last) {
throw new ClockHonestDefectError(
`clock-honest measurement: wall clock stepped BACKWARD (${v} < ${last}) — a non-monotonic clock would round to a bogus measuredMs=0 and silently claim a colocated seat (fail closed, bd kestrel-yb75)`,
);
}
last = v;
return v;
};
}
/**
* FAN OUT one frozen plan to N watchers (ADR-0032 §7). Rehydrates the fixture into a recorded strategist
* (ZERO model calls), runs the frozen-Plan counterfactual ONCE (the shared `frozenPlanEv`), then for EACH
* watcher builds `cascadeAgent({ strategist: recordedStrategist, watcher })` and runs it through the EXISTING
* `runSimulateSession` against the byte-identical frozen frame. Collects grade + {@link attributeCascade
* attribution} per watcher and aggregates all columns via {@link buildGrid} into N distinct grid cells (each
* watcher label mints a distinct cascade ConfigId — the ADR-0013 controlled-division discipline). Deterministic
* across the fan (no wall clock, no RNG in the driver): the same fixture + watchers yields byte-identical columns.
*/
/** Derive the {@link ColumnSummary} from a column's cascade routing {@link CascadeStep} log + its captured
* turns. The four buckets PARTITION every invoked ordinal exactly once: an escalation-held / escalation-
* rebrief step is counted in its OWN bucket (never pooled into PASS/act); a non-escalated step is classified
* by whether its captured terminal turn EMITTED an action. Pure — no clock, no RNG. */
function summarizeColumn(steps: readonly CascadeStep[], captured: CapturedTurns): ColumnSummary {
let actedTurns = 0;
let passTurns = 0;
let escalationHeldTurns = 0;
let escalationRebriefTurns = 0;
for (const step of steps) {
if (step.kind === "escalation-hold-armed") {
escalationHeldTurns++;
continue;
}
if (step.kind === "escalation-rebrief") {
escalationRebriefTurns++;
continue;
}
const turn = captured.get(step.key)?.turn;
if ((turn?.actions.length ?? 0) > 0) actedTurns++;
else passTurns++;
}
return { actedTurns, passTurns, escalationHeldTurns, escalationRebriefTurns };
}
export async function fanWatchers(fixture: PlanFixture, watchers: readonly WatcherSpec[], frame: FanFrame, clock?: ClockHonestFan): Promise<FanResult> {
const strategist = recordedStrategistOf(fixture);
// The frozen-Plan counterfactual — the armed Plan alone (pure-algo, no watcher), run ONCE. The shared
// baseline for every column's watcher_alpha (ADR-0032 §6). A fixture that armed nothing → NaN EV (honest).
// The frozen frame knobs, spread WITHOUT explicit `undefined` (exactOptionalPropertyTypes discipline).
const wakesOpt = frame.wakes !== undefined ? { wakes: frame.wakes } : {};
const mandateOpt = frame.mandate !== undefined ? { mandate: frame.mandate } : {};
const briefOpt = frame.brief !== undefined ? { brief: frame.brief } : {};
// THE THETA GATE (theta-cell seam b): thread the frame's opt-in into the sim so held option legs are
// marked-to-model per wake and the bleed is folded into `totals.floor` — the frozen-plan baseline AND
// every watcher column grade on the same theta-graded floor. Absent (every cell but FOMC-OPTIONS) ⇒ no
// spread ⇒ byte-identical to before the flag was wired (exactOptionalPropertyTypes discipline).
const markToModelOpt = frame.markToModel === true ? { markToModel: true as const } : {};
const planDoc = fixturePlanDoc(fixture);
const frozen =
planDoc !== null
? await runSimulateSession({ events: frame.events, agent: fixedPlanAgent(planDoc, BACKTEST_CONFIG), fillModel: frame.fillModel, rUsd: frame.rUsd, ...wakesOpt, ...markToModelOpt })
: null;
const frozenPlanEv = frozen?.blotter.totals.floor ?? NaN;
const benchmark = frame.benchmarkEv ?? frozenPlanEv;
const columns: WatcherColumn[] = [];
const blotters: Blotter[] = [];
for (const spec of watchers) {
// `frozenStrategist: true` — this fan's strategist tier is a `recordedStrategistOf` recording with NO
// captured re-brief. So a watcher escalation must NOT let the strategist's fail-closed stand-down
// DISARM the frozen plan (which would collapse the whole fan to $0); the over-reach is refused and the
// armed plan is HELD (fail-closed to a PASS). SCOPED to this recorded fan — the LIVE cascade never sets
// it (see cascade.ts CascadeOptions.frozenStrategist); the admission Gate is untouched.
// Wire a routing log sink through the cascade so each column can REPORT its escalation events (esp. the
// frozenStrategist HELD_ARMED_TURN holds) instead of discarding them, and SURFACE a PASS/act/held summary
// (kestrel harness-observability, Bug 2). Above the determinism line — audit evidence, never a graded input.
const steps: CascadeStep[] = [];
// CLOCK-HONEST (bd kestrel-yb75, opt-in): stamp the cascade config's `clockHonest`/`latencyBufferMs`
// ConfigId axes (so a clocked column never pools with an unclocked one) and thread the DECLARED
// `dataRung` + a MONOTONIC-GUARDED clock into the driver. The driver verifies eligibility fail-closed at
// open (the sovereign gate) and mints `measuredMs` at its one `now` seam. A FRESH monotonic wrapper per
// column (each column is its own measurement session). Absent ⇒ no config override, no dataRung/now
// spread ⇒ byte-identical latency-blind behavior (exactOptionalPropertyTypes discipline).
const clockedConfigOpt =
clock !== undefined
? { config: { ...cascadeConfigOf(strategist.config, spec.watcher.config), clockHonest: true as const, latencyBufferMs: clock.latencyBufferMs } }
: {};
const clockOpt = clock !== undefined ? { dataRung: clock.dataRung, now: monotonicNow(clock.now ?? Date.now) } : {};
const cascade = cascadeAgent({ strategist, watcher: spec.watcher, frozenStrategist: true, log: steps, ...clockedConfigOpt });
const res = await runSimulateSession({
events: frame.events,
agent: cascade,
fillModel: frame.fillModel,
rUsd: frame.rUsd,
...wakesOpt,
...mandateOpt,
...briefOpt,
...markToModelOpt,
...clockOpt,
});
const attribution = attributeCascade({ benchmark, frozenPlan: frozenPlanEv, cascade: res.blotter.totals.floor });
columns.push({
configId: cellConfigId(res.config),
label: spec.label ?? res.config.label,
blotter: res.blotter,
captured: res.captured,
attribution,
steps,
summary: summarizeColumn(steps, res.captured),
});
blotters.push(res.blotter);
}
return { planFixtureSha: fixture.planFixtureSha, frozenPlanEv, columns, grid: buildGrid(blotters) };
}