/**
 * # session/sim — the sim Session driver + graded report (RUNTIME §7)
 *
 * The one object that owns a `sim` run, wired end to end over the shipped substrate. It reads a
 * recorded/synthetic bus **as its clock** (`now` = each event's `ts`, RUNTIME §0), drives the
 * canonical market state and the {@link PlanEngine} trigger sweep, routes the engine's orders
 * through the **Gate** into a {@link SimFillEngine} (the judge, RUNTIME §6), reassesses resting
 * orders on every BOOK event (and resting SPOT orders on every SPOT tick carrying an NBBO,
 * ADR-0017), feeds the resulting fills **back** into the engine's org facts and onto the emitted
 * bus, cash-settles held inventory off the final spot — options at intrinsic, spot inventory
 * mark-to-market (ADR-0017) — and returns a graded {@link EpisodeReport}.
 *
 * One code path, one gate (RUNTIME §7): swapping the gate adapter is what turns this into paper
 * or live; nothing else changes. Every mode crosses the same fill model, so grades compare on
 * fills, not on fill assumptions (ARCHITECTURE §4).
 *
 * ## The per-event pass (no look-ahead — state at event N is a function of events ≤ N)
 * For each bus event, in order:
 * 1. **clock** — the gate's `now` is pinned to the event's `ts` (the Gate seam is `now`-less;
 *    the driver owns the injected clock, RUNTIME §0).
 * 2. **canonical state** — {@link CanonicalState.applyEvent} folds SPOT/HEARTBEAT into the
 *    signal instrument's one causal coordinate (the market-fact substrate the triggers read).
 * 3. **fill reassessment (BOOK only, BEFORE the sweep)** — the market acts first: every leg of
 *    the book reassesses the matching resting orders through {@link SimFillEngine.onBook}; a
 *    strict-cross floor fill is emitted **and fed back** into the engine (`engine.onEvent(fill)`)
 *    so inventory, basis, and TP maintenance advance — all **before** the engine reacts to that
 *    book. This is the load-bearing ordering (RUNTIME §7): a plan can never cancel/reprice an
 *    order the same book would have filled.
 * 4. **engine sweep** — {@link PlanEngine.onEvent} arms/fires/manages; its PLAN + WAKE land on
 *    the emitted bus, its order submits/cancels go through the {@link SimGate} into the fill
 *    engine (each `place`/`cancel` ORDER event drained onto the emitted bus at submission time).
 *    Orders placed *by this sweep* rest for the **next** book (fills are reassessed on book
 *    events, one book at a time).
 *
 * At session end the fill engines **settle** at the final exec spot: filled option inventory
 * settles at intrinsic, filled spot inventory marks to the final spot (ADR-0017 — no expiry, no
 * intrinsic), still-resting orders expire worthless-unfilled ($0 floor). The report carries the
 * floor outcome and the E[$] under `pFill` side by side (RUNTIME §6).
 *
 * ## Determinism (RUNTIME §0/§7 — a test, not a hope)
 * Everything on the path is pure and injected-time; the emitted stream is stamped with a
 * monotonic `seq` in emission order and serialized canonically. `determinism_hash` is the
 * sha256 of that serialization: two runs on the same inputs produce the identical hash, and a
 * zero-documents run produces a stable pass-through hash.
 *
 * ## Fail-closed (RUNTIME §8, all loud)
 * No META header → refuse (a well-formed bus opens with exactly one META). Unknown fill model →
 * refuse to grade (never default silently). No instruments → refuse. Bus corruption surfaces
 * from {@link readBus} as a loud, line-located error.
 */

import { writeFileSync } from "node:fs";

import type {
  BookEvent,
  BookState,
  BusEvent,
  ExperimentalEnvelope,
  Fidelity,
  FillModelStamp,
  InstanceIdentity,
  MetaEvent,
  Mode,
  NewBusEvent,
  OptionQuote,
  OrderEvent,
  PlanOutcome,
  PlanState,
  Right,
  SampledQualificationClaim,
} from "../bus/index.ts";
import { BUS_SCHEMA, canonicalEnvelope, foldBook, groupPlanInstances, readBus, serializeBus } from "../bus/index.ts";
import {
  CanonicalSeriesProvider,
  CanonicalState,
  FakeOrgFacts,
  UNKNOWN,
  type SeriesProvider,
  type TriState,
} from "../series/index.ts";
import { PlanEngine, armRefusalError, type Gate, type InstrumentSpec, type OrderIntent, type OrderRole } from "../engine/index.ts";
import type { FillEvent } from "../lang/index.ts";
import { expiryTauYears, etMinuteOfDay, type FairTauProvider } from "./clock.ts";
import { requireMeta, resolveSpecs, specFromMeta } from "./specs.ts";
import {
  MakerFairCalV1,
  MakerFairV1,
  SETTLE_MARK_STALE_AFTER_MS,
  SimFillEngine,
  SpotFillEngine,
  StrictCrossV1,
  type EpisodeSigmoidParams,
  type FillModel,
  type OrderOutcome,
  type SettleMark,
  type SpotSettleReport,
} from "../fill/index.ts";
import { executionFair } from "../fair/index.ts";
import { type KestrelNode, type Module, type PlanStatement } from "../lang/index.ts";
import { canonicalPlansText } from "../canonical/plans.ts";
import { project, type Blotter } from "../blotter/index.ts";
import { makeGate } from "../adapters/broker.ts";
import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.ts";
import { nakedShortTaint, sampledQualified, selectHeadline, type FilledLeg, type HeadlinePnl } from "../grade/headline.ts";
import type { AgentConfig } from "./agent.ts";
import type { CarriedLot } from "../fill/index.ts";
import type { SessionCarry } from "./carry.ts";
import { sha256 } from "../crypto/sha256.ts";
import { roundToGrid } from "../protocol/grade.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Public surface
// ─────────────────────────────────────────────────────────────────────────────

/** The two shipped, named fill models (RUNTIME §6). Any other name is refused (fail-closed). */
export type FillModelName = "strict-cross-v1" | "maker-fair-v1";

/**
 * The machine-readable reason code a session de-arms under when the exec book's time-to-expiry is
 * unknowable (kestrel-wcnd) — it prefixes the CONTROL `disarm` note, which the Blotter projects as
 * a reason-carrying de-arm record (`disarms[].reason`, kestrel-2pl). Exported so a consumer (and
 * the guard fixture) matches the CODE rather than scraping prose.
 */
export const FAIR_TAU_UNKNOWN_CODE = "fair-tau-unknown" as const;

/** Options for {@link runSimSession}. Exactly one of `busPath`/`events` supplies the bus.
 * `documents` are already-parsed Kestrel nodes (a bare statement or a full module — both are
 * normalized to a module and armed). Instrument contract facts are not on the v1 bus META, so
 * they are derived from the asset class (tick `0.01`, strike step `1`, multiplier `100` for an
 * option-underlier else `1`, role carried from META) unless `instruments` overrides them.
 * `fairTauYears` injects time-to-expiry so `@fair` (and the maker-fair hazard) can be built;
 * **absent, it defaults to the CONTRACT-expiry tau** ({@link expiryTauYears}: `max(0, 16:00 ET on
 * the book's OWN expiry − now)`, RUNTIME §4) — so `@fair` is buildable by default, decays toward
 * the contract's expiry (not this session's close, kestrel-wcnd), and goes UNKNOWN (fail-closed,
 * see {@link SessionCore.step}) when a book names no resolvable expiry. */
export interface RunSimOptions {
  readonly busPath?: string;
  readonly events?: readonly BusEvent[];
  readonly documents: readonly KestrelNode[];
  readonly fillModel: FillModelName;
  readonly rUsd: number;
  readonly out?: string;
  readonly instruments?: readonly InstrumentSpec[];
  readonly fairTauYears?: FairTauProvider;
  /** Calibrated maker-fair fill params (the `episode-sigmoid-u-v1` form). When present with
   * `fillModel: "maker-fair-v1"`, the session grades under the calibrated curve instead of the
   * uncalibrated default hazard (RUNTIME §6). Ignored by `strict-cross-v1`. */
  readonly makerFairParams?: EpisodeSigmoidParams;
  /** An optional agent config (kestrel-5zl.6). When supplied AND it declares an authoring identity, its
   * a57.14 experimental envelope is captured (`envelopeForConfig`) and stamped on the graded META. A
   * config-less run (the default) stamps no envelope — byte-identical to before (backward-compatible). */
  readonly config?: AgentConfig;
}

/** One transition in a plan's lifecycle trace (RUNTIME §5), reconstructed from the emitted PLAN
 * stream. */
export interface PlanLifecycleStep {
  readonly ts: number;
  readonly state: PlanState;
  readonly outcome?: PlanOutcome;
  readonly reason?: string;
}

/** A plan's lifecycle trace + its terminal state (the outcome when `done`, else the last
 * lifecycle state). ONE trace is one exact Plan **instance** (kestrel-22j.15): a legal same-name replacement
 * is its OWN trace beside its predecessor, never collapsed into it. */
export interface PlanTrace {
  /** The authored plan NAME — the Lineage aggregation key (a name may legally recur; NOT the instance id). */
  readonly plan: string;
  /** The exact Plan-instance identity this trace belongs to (kestrel-22j.15) — the id the engine minted,
   * carried on the emitted PLAN stream. Distinct per instance, so two same-name instances are two traces. */
  readonly plan_instance: string;
  readonly lifecycle: readonly PlanLifecycleStep[];
  readonly final_state: PlanState | PlanOutcome;
}

/** One gate order, joined across the intent (resolution annotation, role), the engine child
 * (esc stage), and the fill engine outcome (`pFill`, floor fill, settle P&L). */
export interface OrderReport {
  readonly ref: string;
  readonly plan: string;
  /** The authoring plan's exact INSTANCE identity (kestrel-22j.15) — carried from the order intent so an
   * order joins its exact instance, not merely its recurring name. */
  readonly plan_instance: string;
  readonly role: OrderRole;
  readonly instrument: string;
  readonly side: "buy" | "sell";
  readonly qty: number;
  /** The option strike; ABSENT for a spot/equity order (ADR-0017 — no fictional strike). */
  readonly strike?: number;
  /** The option right; ABSENT for a spot/equity order (ADR-0017). */
  readonly right?: Right;
  readonly px: number;
  /** The price-resolution annotation (`fair=fallback(mid)`, …) — a silent mid is forbidden,
   * RUNTIME §4. */
  readonly annotation: string;
  /** The esc stage this resting order occupied (RUNTIME §4/§5) — the ladder rung, NOT a count of
   * reprices (the honest session-wide reprice tally is `session.reprice_count`, F6). */
  readonly esc_stages: number;
  /** Expected fill probability `1 − survival` (`1` on a floor fill). */
  readonly pFill: number;
  /** Did the strict-cross floor fire? */
  readonly filled: boolean;
  /** The floor fill price, else `null`. */
  readonly fillPx: number | null;
  /** Realized $ under the floor (0 unless floor-filled). */
  readonly floorPnl: number;
  /** E[$] under `pFill`. */
  readonly expectedPnl: number;
  /** Present + `true` when the order was cancelled mid-session (esc-replaced, TTL/INVALIDATE
   * pulled, or CANCEL-IF) rather than filled or settled. */
  readonly cancelled?: boolean;
}

/** The graded sim report (RUNTIME §7). Stamps its judge (`fill_model`) and carries the
 * determinism hash of the emitted event stream. */
export interface EpisodeReport {
  readonly session: {
    readonly date: string;
    readonly instruments: readonly string[];
    readonly mode: "sim";
    readonly fill_model: string;
    readonly bus_events: number;
    /** sha256 of the emitted PLAN/WAKE/ORDER stream — pins the engine's OUTPUT (RUNTIME §0/§7). */
    readonly determinism_hash: string;
    /** sha256 of the raw INPUT bus (serialized events) — pins what the run was graded against, so
     * determinism_hash (output) and bus_sha256 (input) together bind the whole computation (F6). */
    readonly bus_sha256: string;
    /** Count of actual cancel/replace reprice submissions across the session (peg + esc), from the
     * engine — the honest reprice tally (F6; `orders[].esc_stages` is the ladder rung, not a count). */
    readonly reprice_count: number;
    /** The **last bus event `ts`** — the session clock's final value (settle time). Injected, never
     * wall-clock (RUNTIME §0); the natural deterministic `now` for recording this run into the
     * ledger (ADR-0006). Equals `firstTs` for an empty bus. */
    readonly settle_ts: number;
  };
  readonly plans: readonly PlanTrace[];
  readonly orders: readonly OrderReport[];
  /** The three-channel P&L contract (kestrel-9gu.12): floor + expected always, sampled on a seeded run,
   * and the HEADLINE selected by the fail-closed qualification gate (floor until 9gu.8.6 passes). */
  readonly totals: {
    /** Realized $ under the deterministic strict-cross floor — the honest lower bound. */
    readonly realized_floor_usd: number;
    /** E[$] under the survival product — the analytic bound beside the realization. */
    readonly expected_usd: number;
    /** The seeded causal realization (one ensemble replicate, ADR-0016). Present ONLY on a `fillSeed`
     * run — an un-run channel is never conflated with a zero outcome (kestrel-9gu.12). */
    readonly sampled_usd?: number;
    readonly premium_spent: number;
    /** The settle mark's provenance + fail-closed staleness verdict + annotated recovery
     * (kestrel-xwf): the spot the book cash-settled against (`px` — the parity-recovered value when
     * `source: "parity"`), WHEN the primary mark's value was last established on the input tape
     * (`as_of_ts`; a frozen feed re-printing the same px does not refresh it), its age vs the settle
     * instant, and the `stale` verdict under the declared threshold. `stale: true` means the totals
     * above are graded against a mark the tape stopped supporting — NON-BANKABLE (a parity recovery
     * annotates, it does not pardon), never a silently fresh-looking number (the taint also rides the
     * Blotter's `totals.headline`). `source`/`mark_uncertain`/`note` are the recovery annotation:
     * `"parity"` = recovered honestly from a two-sided closing book; `"stale"` = refused —
     * `mark_uncertain: true`, the last mark retained. Explicit `null`s (never omitted keys) so the
     * report's shape is fixed. */
    readonly settle_mark: {
      readonly px: number;
      readonly as_of_ts: number | null;
      readonly last_observed_ts: number | null;
      readonly age_ms: number | null;
      readonly stale_after_ms: number;
      readonly stale: boolean;
      readonly source: "market" | "parity" | "stale";
      readonly mark_uncertain: boolean;
      readonly note: string | null;
    };
    /** WHICH channel is the headline + the gate verdict that selected it + the xwf settle taint
     * ({@link HeadlinePnl}, kestrel-9gu.12). `floor` today; `sampled` once the 9gu.8.6 qualification is
     * on the record (never with extrapolated-support sampled realizations — bankableEv stays). The
     * settle-mark staleness verdict above feeds this headline's taint (a stale mark — including a
     * parity-recovered one — taints whichever channel leads). */
    readonly headline: HeadlinePnl;
  };
  readonly emitted: Readonly<Record<string, number>>;
}

/**
 * @deprecated Renamed to {@link EpisodeReport} (kestrel-markets-jcn0): the
 * deterministic execution unit is an **Episode**, not a "Kestrel Session"
 * ("session" is now reserved for the stateful TerminalSession seat,
 * PLAT-ADR-0052). This alias keeps `engine.session.SessionReport` type-importers
 * compiling across the rename; it will be removed after the 0.8.0 public cutover.
 * The on-the-wire report shape (its `session` payload field, `sessionId` keys) is
 * unchanged — this is a type-name-only rename, wire contract untouched.
 */
export type SessionReport = EpisodeReport;

// ─────────────────────────────────────────────────────────────────────────────
// Internals
// ─────────────────────────────────────────────────────────────────────────────

/** The fill-model factory — the ONE place a name maps to an implementation. An unknown name is
 * refused loudly: never grade under a fill model you did not name (RUNTIME §6/§8). */
function makeFillModel(name: FillModelName, makerFairParams?: EpisodeSigmoidParams): FillModel {
  switch (name) {
    case "strict-cross-v1":
      if (makerFairParams !== undefined) {
        throw new Error("session: --hazard-params applies only to maker-fair-v1 (fail-closed, RUNTIME §8)");
      }
      return new StrictCrossV1();
    case "maker-fair-v1":
      // Calibrated curve when params are supplied; the uncalibrated default hazard otherwise.
      return makerFairParams !== undefined ? new MakerFairCalV1(makerFairParams) : new MakerFairV1();
    default:
      throw new Error(`session: unknown fill model ${JSON.stringify(name)} (fail-closed, RUNTIME §8)`);
  }
}

/** Normalize a parsed Kestrel node into a module (a bare statement becomes a one-statement
 * module) so it can be armed. */
function toModule(node: KestrelNode): Module {
  return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] };
}

// ─────────────────────────────────────────────────────────────────────────────
// Graded-bus judge self-description (ADR-0011 part B — the two-bus rule)
//
// A GRADED bus self-describes its judge on its opening META: the fill model that produced its fills,
// the Instance identity, and the Fidelity it may claim. These are session INPUTS (known at open), so
// the driver stamps them onto the META. An INPUT tape stays fill-model-agnostic (no stamp). Pure and
// deterministic — no wall clock, no RNG.
// ─────────────────────────────────────────────────────────────────────────────

// sha256 (the portable, synchronous digest — Bun-native fast path, Worker-portable pure-JS fallback,
// kestrel-alw.17) is imported from ../crypto/sha256.ts so a graded bus hashes byte-identically under
// `bun test`, the CLI, and a Cloudflare Worker isolate. No wall clock, no RNG.

/** The canonical parse-print text of an armed document set — the byte-stable printer projection
 * (ADR-0001), documents joined by a blank line (matching the CLI's canonical plans text / the ledger's
 * `plans_sha256` input). Its sha256 is {@link InstanceIdentity.version}. A `Module` is a `KestrelNode`,
 * so this is the {@link canonicalPlansText} owner (canonical/plans) applied to an armed module set. */
export function canonicalPlansTextOf(modules: readonly Module[]): string {
  return canonicalPlansText(modules);
}

/** The fill-model judge stamp for a GRADED bus's META. `name`/`version` are the model's identity;
 * `calibration_sha` is a stable digest of the calibration descriptor; `self_limitation` is what the judge
 * DECLARES it could not observe (a57.4) — stamped verbatim so the projector never re-guesses it from `name`
 * (the mislabel of the calibrated judge, kestrel-o32) ({@link FillModelStamp}). */
export function fillModelStampOf(model: FillModel): FillModelStamp {
  const cal = model.calibration;
  const calibration_sha = sha256(
    cal !== undefined ? JSON.stringify({ version: cal.version, calibrated: cal.calibrated }) : "none",
  );
  return {
    name: model.name,
    version: model.version,
    calibration_sha,
    self_limitation: model.self_limitation.map((l) => ({ code: l.code, note: l.note })),
  };
}

/** The Instance identity for a GRADED bus's META (ADR-0011 recommendation 1, ADR-0006 names-are-data):
 * `lineage` = the authored NAME (the Pod's when the document set runs a Pod, else the first authored
 * plan/book statement's), `version` = `plans_sha256`, `mode` = the session mode, `pod` present ONLY for
 * an actual Pod document. A zero-document grade authors nothing ⇒ an empty `lineage`. */
export function instanceIdentityOf(modules: readonly Module[], mode: Mode): InstanceIdentity {
  const version = sha256(canonicalPlansTextOf(modules));
  let pod: string | undefined;
  let firstNamed: string | undefined;
  for (const m of modules) {
    for (const st of m.statements) {
      if (st.kind === "pod" && pod === undefined) pod = st.name;
      if ((st.kind === "plan" || st.kind === "book" || st.kind === "pod") && firstNamed === undefined) {
        firstNamed = st.name;
      }
    }
  }
  const lineage = pod ?? firstNamed ?? "";
  return { lineage, version, mode, ...(pod !== undefined ? { pod } : {}) };
}

/** Fidelity level for a mode (CONTEXT: Fidelity): sim/paper are `modeled`, live is `realized`. */
export function fidelityOf(mode: Mode): Fidelity {
  return mode === "live" ? "realized" : "modeled";
}

/** Stamp a GRADED bus's opening META: the input session header + the judge self-description
 * (`fill_model` + `instance` + `fidelity`) and, when supplied, the author's a57.14 experimental
 * `envelope` (the config's captured identity — kestrel-5zl.6) plus the FULL `config_id`
 * (`sha256(canonical AgentConfig)` — the sampling-axis-complete identity the grid CellKey keys on,
 * m9i.2 owner tweak). Advertises the current {@link BUS_SCHEMA} (v6 — a graded bus is the ONLY stamping
 * site that follows the bump; input tapes keep their pinned literal, clock-honest wakes §1) and, for a
 * verified clock-honest session, the `clock_honest: true` attestation (ADR-0040 — `true` or ABSENT,
 * never `false`). Keeps the input META's `seq`/`ts`. Pure. Only a GRADED bus carries these; an INPUT
 * tape's META stays judge-less (the two-bus rule, ADR-0011). The `envelope`/`config_id` keys are OMITTED
 * entirely when absent — a config-less graded bus is byte-identical to before (backward-compatible;
 * a57.14 leaves it `certified`). */
export function stampGradedMeta(
  inputMeta: MetaEvent,
  judge: {
    fillModel: FillModelStamp;
    instance: InstanceIdentity;
    fidelity: Fidelity;
    envelope?: ExperimentalEnvelope;
    configId?: string;
    cellConfigId?: string;
    sampledQualification?: SampledQualificationClaim;
    /** The clock-honest attestation (ADR-0040 / kestrel-w7la.1): pass `true` ONLY when the session ran
     * clocked semantics AND the data-floor verification held. The stamp is `true` or ABSENT — never
     * `false` (`false` is reserved platform-side as a revocation value; clock-honest wakes §5). */
    clockHonest?: true;
  },
): MetaEvent {
  return {
    seq: inputMeta.seq,
    ts: inputMeta.ts,
    stream: "META",
    type: "session",
    session_date: inputMeta.session_date,
    instruments: inputMeta.instruments,
    mode: inputMeta.mode,
    bus_schema: BUS_SCHEMA,
    fill_model: judge.fillModel,
    instance: judge.instance,
    fidelity: judge.fidelity,
    ...(judge.envelope !== undefined ? { envelope: judge.envelope } : {}),
    ...(judge.configId !== undefined ? { config_id: judge.configId } : {}),
    ...(judge.cellConfigId !== undefined ? { cell_config_id: judge.cellConfigId } : {}),
    // The sampled-channel qualification claim (kestrel-9gu.12) — the headline gate's bus-derivable
    // input. Omitted entirely when absent, so every unqualified graded bus is byte-identical to before.
    ...(judge.sampledQualification !== undefined ? { sampled_qualification: judge.sampledQualification } : {}),
    // The clock-honest attestation (ADR-0040): `true` or ABSENT, never `false` (clock-honest wakes §5).
    ...(judge.clockHonest === true ? { clock_honest: true } : {}),
  };
}

/** Assemble the canonical GRADED bus for a session: the judge-stamped opening META (seq 0) followed by
 * the engine's emitted reactions (PLAN/WAKE/ORDER/TELEMETRY, incl. the settle-outcome records),
 * re-stamped `seq` 1..N so the stream is gap-free from 0 and opens with exactly one META — a well-formed
 * bus {@link readBus} accepts. This is the self-describing artifact the Blotter projector (slice 2) will
 * consume; slice 1 uses it to PROVE totals{floor,expected} + orders[].support re-derive from bus bytes
 * with no engine-state scrape. Pure. */
export function assembleGradedBus(gradedMeta: MetaEvent, emitted: readonly BusEvent[]): BusEvent[] {
  let seq = 0;
  const out: BusEvent[] = [{ ...gradedMeta, seq: seq++ }];
  // Seq-space REFERENCES re-stamp with the seqs they name (clock-honest wakes §1): a v6 deliberation
  // record's `wake_seq` points at its WAKE checkpoint's seq in the EMITTED stream, so assembling the
  // graded bus (which re-stamps every seq to make room for the META at 0) must remap it through the
  // same old→new map — otherwise the reader's record-vs-checkpoint identity would verify against the
  // wrong event. The map is explicit (never an assumed "+1") so the invariant survives any future
  // re-stamping shape.
  const newSeqOf = new Map<number, number>();
  for (const e of emitted) {
    const stamped = seq++;
    newSeqOf.set(e.seq, stamped);
    if (e.stream === "WAKE" && e.type === "deliberation") {
      const remapped = newSeqOf.get(e.wake_seq);
      if (remapped === undefined) {
        throw new Error(
          `session: deliberation record (emitted seq ${e.seq}) names wake_seq ${e.wake_seq}, which is not an earlier emitted event (fail-closed, clock-honest wakes §1)`,
        );
      }
      out.push({ ...e, seq: stamped, wake_seq: remapped } as BusEvent);
    } else {
      out.push({ ...e, seq: stamped } as BusEvent);
    }
  }
  return out;
}

/**
 * Session-scoped own-fill telemetry — the `fillEvent` provider hook (RUNTIME §3). It observes the
 * ORDER events the fill engine drains onto the emitted bus and answers `filled`/`rejected`/
 * `cancelled` fill-lifecycle triggers as a **definite `true` once seen** (else definite `false`,
 * since it IS wired). Deliberately session-scoped and all-or-nothing: per-plan / per-leg /
 * `partial-fill` / `unfilled` semantics are ABSENT-with-reason — single-leg fills are never
 * partial (documented in SURFACES.md).
 *
 * A LEG QUALIFIER never reaches here: `ComposedProvider.fillEvent` (src/engine/plans.ts) refuses it
 * upstream with a logged de-arm reason (kestrel-s3h8), precisely BECAUSE this latch is session-wide —
 * answering `filled leg 1` from it would fire a plan on a stranger's fill. Do not "support" the
 * qualifier here by ignoring it; scope it for real (kestrel-qbwo.2) or leave the refusal standing.
 */
class FillTelemetry {
  #filled = false;
  #rejected = false;
  #cancelled = false;

  /** Fold one drained bus event; only ORDER events matter. */
  observe(ev: BusEvent): void {
    if (ev.stream !== "ORDER") return;
    if (ev.type === "fill") this.#filled = true;
    else if (ev.type === "reject") this.#rejected = true;
    else if (ev.type === "cancel" && ev.reason === "cancelled") this.#cancelled = true;
  }

  /** Answer a fill-lifecycle trigger. */
  assess(ev: FillEvent): TriState {
    switch (ev.event) {
      case "filled":
        return this.#filled;
      case "rejected":
        return this.#rejected;
      case "cancelled":
        return this.#cancelled;
      default:
        return UNKNOWN; // partial-fill / unfilled: not modeled for single-leg fills
    }
  }
}

/** The append-only emitted stream: stamps a monotonic `seq` in emission order and serializes
 * canonically (the determinism substrate, RUNTIME §0/§1). */
class EmittedStream {
  readonly events: BusEvent[] = [];
  #seq = 0;

  /** Stamp the next `seq` and land the event in CANONICAL field order. The canonicalization is
   * {@link canonicalEnvelope} — the SAME owner {@link import("../bus/write.ts").BusWriter} stamps
   * through, never a second copy of the order — because a bare `{ seq: this.#seq++, ...ev }` would
   * preserve the CALLER's key order verbatim: an emitter spreading a payload that carries
   * `ts`/`stream`/`type` mid-object then serializes a non-canonical record onto the graded bus
   * (`serializeEvent` is `JSON.stringify`, insertion-order-dependent). That was kestrel-pta5 —
   * `theta_bleed` put `ts` after `type` — patched at the one call site with the class left open;
   * kestrel-byfl closes the class here, so no present or future emitter can reintroduce it. */
  emit(ev: NewBusEvent): BusEvent {
    const stamped = canonicalEnvelope(this.#seq++, ev);
    this.events.push(stamped);
    return stamped;
  }

  hash(): string {
    return sha256(serializeBus(this.events));
  }
}

/** The sim Gate: rests engine order intents in the {@link SimFillEngine} — or, for a SPOT/equity
 * intent (no strike/right, ADR-0017 / kestrel-20f.14), in the instrument-keyed
 * {@link SpotFillEngine} — and cancels them, pinning the injected clock (`now`) the driver sets
 * each event. Every `place`/`cancel` drains the fill engines' fresh ORDER events onto the
 * emitted bus at submission time. */
export class SimGate implements Gate {
  now = 0;
  readonly intents: OrderIntent[] = [];

  constructor(
    private readonly fill: SimFillEngine,
    private readonly spotFill: SpotFillEngine,
    /** Whether spot orders may grade in this session — only the strict-cross family has a spot
     * judge today; a spot order under `maker-fair-v1` is REFUSED loudly (no spot hazard model
     * exists — never grade under a judge that cannot assess the instrument, RUNTIME §8). */
    private readonly spotAllowed: boolean,
    private readonly drain: () => void,
  ) {}

  submit(intent: OrderIntent): string {
    this.intents.push(intent);
    // Route on the instrument kind (ADR-0017): an option intent carries strike+right; a spot
    // intent carries neither — it rests in the SpotFillEngine keyed on the instrument alone.
    if (intent.strike === undefined || intent.right === undefined) {
      if (!this.spotAllowed) {
        throw new Error(
          "session: a spot/equity order grades only under strict-cross-v1 — no spot hazard model exists for maker-fair-v1 (fail-closed, RUNTIME §8; ADR-0017)",
        );
      }
      const ref = this.spotFill.place(
        {
          ref: intent.ref,
          plan: intent.plan,
          plan_instance: intent.plan_instance,
          instrument: intent.instrument,
          side: intent.side,
          qty: intent.qty,
          px: intent.px,
        },
        this.now,
      );
      this.drain();
      return ref;
    }
    const ref = this.fill.place(
      {
        ref: intent.ref,
        plan: intent.plan,
        plan_instance: intent.plan_instance,
        instrument: intent.instrument,
        side: intent.side,
        qty: intent.qty,
        px: intent.px,
        strike: intent.strike,
        right: intent.right,
        // Thread the directional-guard evidence (kestrel-9gu.6): the engine derived the leg's
        // moneyness + covered-state onto the intent, and the FillOrder must carry them so the
        // maker-fair directional guard runs end to end (the far-OTM SELL cap / covered-wing exemption
        // / BUY unlock). Omitting them is exactly the drop that let a Session's far-OTM standalone
        // SELL bypass the cap.
        ...(intent.moneyness !== undefined ? { moneyness: intent.moneyness } : {}),
        ...(intent.covered !== undefined ? { covered: intent.covered } : {}),
      },
      this.now,
    );
    this.drain();
    return ref;
  }

  cancel(ref: string): boolean {
    // Returns whether a resting order was actually removed. The engine's own cancels ignore the return
    // (a double-cancel / already-filled ref is a harmless no-op); an AGENT `cancelOrder` reads it to
    // fail-closed refuse a ref that matches no resting order (kestrel-5zl.5). Widening void→boolean still
    // satisfies the `Gate.cancel(): void` interface (a boolean return is assignable to a void one).
    // Try the options book first, then the spot book (a ref lives in exactly one — the engine's
    // monotonic ids never collide across the two).
    const removed = this.fill.cancel(ref, this.now) || this.spotFill.cancel(ref, this.now);
    this.drain();
    return removed;
  }
}

/** Deterministic report/grader $-grid rounding so dollars/prices are byte-stable in the report (RUNTIME
 * §0). Re-homed onto the ONE grid the grader owns ({@link ../protocol/grade.ts REPORT_GRID} / `roundToGrid`,
 * kestrel-z473.6) — never a local `1e8` — so an outsider's recomputation is byte-identical. */
const round = roundToGrid;

// ─────────────────────────────────────────────────────────────────────────────
// Closing put-call-parity recovery candidate (kestrel-xwf annotated recovery)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Derive a closing settle spot from **put-call parity** on a two-sided book: at (near-)expiry the
 * forward ≈ spot, so `C − P = S − K ⇒ S = K + (C_mid − P_mid)` for the strike nearest `nearSpot` that
 * quotes BOTH a two-sided call and a two-sided put. Returns the parity spot + the strike used, or
 * `null` when no strike has both sides two-sided (parity not derivable → fail-closed to a taint).
 *
 * Mids are used ONLY as a settle-mark of last resort when the primary feed died — never as an
 * execution anchor (the never-a-mid-anchor rule governs FILL prices, RUNTIME §4) — and the result is
 * always flagged as recovered-from-stale, never silently banked as a live mark.
 *
 * The caller supplies the CLOSING legs: {@link SessionCore} feeds this only quotes whose values are
 * FRESH at the settle instant (carried on a BOOK event within the declared settle-mark staleness
 * threshold, {@link SETTLE_MARK_STALE_AFTER_MS}) — a two-sided pair surviving in the FOLDED book
 * from hours before the option feed died is NOT a closing strike, and "recovering" from its stale
 * mids would launder one dead feed through another (the DEGRADED-DATA cell's dark-to-close shape).
 */
export function closingParitySpot(
  book: Pick<BookState, "legs"> | undefined,
  nearSpot: number,
): { spot: number; strike: number } | null {
  if (book === undefined) return null;
  const mid = (q: OptionQuote): number | null =>
    q.bid !== null && q.ask !== null && q.ask >= q.bid ? (q.bid + q.ask) / 2 : null;
  const calls = new Map<number, number>();
  const puts = new Map<number, number>();
  for (const leg of book.legs) {
    const m = mid(leg);
    if (m === null) continue;
    (leg.right === "C" ? calls : puts).set(leg.strike, m);
  }
  let best: { spot: number; strike: number } | null = null;
  let bestDist = Infinity;
  for (const [k, c] of calls) {
    const p = puts.get(k);
    if (p === undefined) continue;
    const dist = Math.abs(k - nearSpot);
    if (dist < bestDist) {
      bestDist = dist;
      best = { spot: k + c - p, strike: k };
    }
  }
  return best;
}

// ─────────────────────────────────────────────────────────────────────────────
// SessionCore — the reusable sim substrate (the per-event pass, single-sourced)
// ─────────────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────────────
// The LIVE-gate seam (OSS-ADR-0053) — how a non-sim progression (paper/live) plugs its own Gate
// and venue-fill feedback into the ONE per-event pass, so sim and paper differ ONLY by the adapters
// at these seams. Absent (every sim/day/simulate caller) ⇒ the core builds its reference SimGate over
// its own fill engine and its per-event pass is byte-identical to before.
// ─────────────────────────────────────────────────────────────────────────────

/** The wired core internals a {@link SessionCoreOptions.liveGate} factory is handed to build its Gate.
 * It reads canonical state / the folded books the core already owns (never a second copy) and appends
 * the venue's own ORDER events onto the session's single live bus. */
export interface LiveGateWiring {
  readonly state: CanonicalState;
  readonly execSpec: InstrumentSpec;
  readonly signalSpec: InstrumentSpec;
  /** The core's folded option books (per instrument) — the same map a Frame renders, read by a paper
   * gate's price-anchor wall so the driver keeps no second book of its own. */
  readonly books: ReadonlyMap<string, BookState>;
  /** Append ONE venue-emitted ORDER event onto the session's single live bus (stamps `seq`, stripping
   * any venue-local one), returning the stamped event. A `fill` is additionally queued to be fed back
   * into the engine AFTER the sweep (the broker fills-after-sweep ordering — the venue acts, the engine
   * reacts next). */
  appendVenueEvent(ev: BusEvent | NewBusEvent): BusEvent;
}

/** What a {@link SessionCoreOptions.liveGate} factory returns — the injected Gate plus the two thin
 * hooks the live progression needs the core's per-event pass to call. Every field but `gate` is
 * optional; the sim path uses none of this. */
export interface LiveGateBinding {
  /** The gate orders route through (the L0-clamped paper/live gate) — REPLACES the reference SimGate.
   * Its `now` is pinned by the core each event exactly as `SimGate.now` is (RUNTIME §0). */
  readonly gate: Gate & { now: number };
  /** Wrap the core's canonical series provider before the engine reads it — the paper path wraps it in
   * the feed-death staleness gate so a plan cannot fire off a dead line. Absent ⇒ the bare provider. */
  wrapSeriesProvider?(base: SeriesProvider): SeriesProvider;
  /** Fold a venue fill into the driver's expected-position ledger (the reconciliation input), called
   * per fill BEFORE it is fed back to the engine. */
  onVenueFill?(fill: OrderEvent): void;
  /** Run the engine sweep (+ the post-sweep venue-fill feedback) inside the driver's own error boundary
   * — the paper path maps a wall's `ClampRefused`/`IbkrOrderRefused` throw into a typed session
   * STAND_DOWN here. Absent ⇒ the sweep runs bare and any throw propagates. */
  guardSweep?(apply: () => void): void;
}

/** What a {@link SessionCore} is constructed from — the pieces both the one-shot
 * {@link runSimSession} and the stepped {@link import("./day.ts").runDaySession} agree on. */
export interface SessionCoreOptions {
  readonly meta: MetaEvent;
  readonly specs: readonly InstrumentSpec[];
  readonly fillModel: FillModelName;
  readonly rUsd: number;
  readonly fairTauYears: FairTauProvider;
  readonly makerFairParams?: EpisodeSigmoidParams;
  /** The initial injected clock (the first bus event's `ts`). */
  readonly firstTs: number;
  /** The author's a57.14 experimental envelope, captured from the run's config (kestrel-5zl.6), stamped on
   * the graded META at open. Absent for a config-less run (a NO-LLM Backtest / day session) ⇒ no envelope. */
  readonly envelope?: ExperimentalEnvelope;
  /** The FULL `ConfigId` of the run's config (`deriveConfigId(canonicalizeConfig(config))`, kestrel-5zl.6),
   * stamped on the graded META alongside `envelope` (m9i.2 owner tweak — the seeded RUN identity feeding
   * `SimRunId`). Supplied under the SAME condition as `envelope`; absent ⇒ no stamp. */
  readonly configId?: string;
  /** The **cell config axis** (`cellConfigId(config)` — the full ConfigId with the `fillSeed` REPLICATE axis
   * stripped, kestrel-9gu.8.1 / ADR-0016 §3), stamped as `meta.cell_config_id`. Supplied ONLY when it DIVERGES
   * from {@link configId} (the config carried a `fillSeed`); absent for a seedless run, whose cell axis equals
   * `config_id` (so the graded bus stays byte-identical). The config axis the grid `cellOf` keys the cell on:
   * seed-only replicates share it. */
  readonly cellConfigId?: string;
  /** The prior session's {@link SessionCarry} (the multi-session horizon design, kestrel-5zl.16). When present with non-empty
   * `positions`, this session OPENS with that carried inventory seeded into the fill engine at its
   * carry-mark basis (emitting an opening `place`+`fill` per lot). **Absent — or an `emptyCarry()` —
   * seeds nothing, so every existing single-session caller (and a length-1 sequence) is byte-identical
   * to today's flat open.** The standing-document re-arm is the driver's concern, not the core's. */
  readonly openingCarry?: SessionCarry;
  /** The SamplingConfig fill-realization seed (kestrel-9gu.8.1/.8.3, ADR-0016). Present ⇒ the fill engine
   * draws CAUSAL sampled fills off a `fillSeed`-keyed per-episode substream; absent ⇒ the sampled channel is
   * OFF (fail-closed — floor + expected only). Derived from the run's config via `fillSeedOf`. */
  readonly fillSeed?: number;
  /** The owner-recorded sampled-channel qualification claim (kestrel-9gu.8.6 → 9gu.12), derived from the
   * run's config via `sampledQualificationOf`. Present ⇒ stamped on the graded META and judged by the
   * headline gate. REFUSED LOUDLY (RUNTIME §8) when structurally contradictory: a claim on an unseeded run
   * (nothing to qualify) or under an uncalibrated judge (never an uncalibrated headline). Absent — every
   * run today — ⇒ the gate fails closed and strict-cross floor stays the headline (9gu.8 AC#7). */
  readonly sampledQualification?: SampledQualificationClaim;
  /** The complete regime-scope vocabulary of the tape (kestrel-ocyf), pre-scanned from the full bus by
   * callers that hold it up front (`runSimSession` / `runSimulateSession`). Threaded to the engine so
   * `armDocument` can issue the DEFINITIVE dead-on-arrival notice for a regime gate whose scope is
   * never written on this tape (vs the conditional not-yet-written notice when the future is unknown). */
  readonly tapeRegimeScopes?: readonly string[];
  /** The clock-honest attestation input (ADR-0040 / kestrel-w7la.1): the driver passes `true` ONLY when
   * the session ran clocked semantics AND the §4 data-floor verification held (it refuses at open
   * otherwise, so a constructed core with this flag is an attested one). Stamped as the graded META's
   * `clock_honest: true`; absent ⇒ latency-blind ⇒ no stamp, byte-identical META. */
  readonly clockHonest?: true;
  /** The LIVE-gate seam (OSS-ADR-0053): a factory that, given the wired core internals, returns the
   * gate a NON-sim progression (paper/live) routes orders through, plus its venue-fill hooks. Present
   * ONLY for the paper/live drivers; ABSENT for every sim/day/simulate caller — and when absent the
   * core builds its reference SimGate over its own fill engine and its per-event pass is byte-identical
   * to before (the sim corpus never sees this field). The factory is called ONCE at construction, after
   * canonical state + the emitted stream exist. */
  readonly liveGate?: (wiring: LiveGateWiring) => LiveGateBinding;
}

/**
 * The sim runtime wired end to end — the ONE place the load-bearing per-event pass lives
 * (RUNTIME §7). Owns the fill engine (the judge), the emitted stream (determinism substrate),
 * the Gate, canonical market state, and the {@link PlanEngine}. Documents are armed
 * incrementally ({@link SessionCore.arm}); {@link SessionCore.step} advances one bus event with
 * **fills-before-sweep** ordering; {@link SessionCore.settle} cash-settles and drains. Both the
 * one-shot driver and the stepped day runner drive the identical core, so their emitted streams —
 * and therefore their determinism hashes — are produced by exactly the same code (no duplicated
 * event loop to drift).
 */
export class SessionCore {
  readonly meta: MetaEvent;
  readonly emitted = new EmittedStream();
  readonly fill: SimFillEngine;
  /** The instrument-keyed resting-order engine for SPOT/equity orders (ADR-0017,
   * kestrel-20f.14) — the spot sibling of {@link fill}; the Gate routes an intent with no
   * strike/right here. Idle (zero events, zero settle outcomes) on an options-only session. */
  readonly spotFill: SpotFillEngine;
  readonly gate: SimGate;
  readonly engine: PlanEngine;
  readonly state: CanonicalState;
  readonly execSpec: InstrumentSpec;
  readonly signalSpec: InstrumentSpec;

  /** The GATE the engine routes orders through (RUNTIME §7) — the reference {@link gate} SimGate for a
   * sim session, or the injected {@link LiveGateBinding.gate} for a paper/live one (OSS-ADR-0053). The
   * ONE thing that differs between the modes; every event the driver pins its `now`. */
  readonly #activeGate: Gate & { now: number };
  /** The injected live-progression hooks (OSS-ADR-0053), or `undefined` for a sim session — the
   * discriminator the per-event pass reads to switch on the live one-bus + venue-fill feedback. */
  readonly #liveBinding: LiveGateBinding | undefined;
  /** Venue fills queued by {@link LiveGateWiring.appendVenueEvent} during a sweep, fed back into the
   * engine AFTER it (the broker fills-after-sweep ordering). Always empty on a sim session. */
  readonly #pendingVenueFills: OrderEvent[] = [];

  readonly #fairTauYears: FairTauProvider;
  /** kestrel-wcnd: latched once the τ-fail-closed de-arm has fired, so the reason is logged ONCE
   * per session (it cannot change — the exec book's expiry is a property of the tape). */
  #fairTauDisarmed = false;
  /** Whether `@fair` is the anchor this session GRADES against — i.e. `maker-fair-v1`, whose fill
   * hazard is a function of `|px − fair|` (RUNTIME §6). `strict-cross-v1` ignores `@fair` entirely,
   * so an unknown τ moves no decision it makes (kestrel-wcnd, see {@link #failClosedOnUnknownTau}). */
  readonly #fairIsGraded: boolean;
  readonly #fillTelemetry = new FillTelemetry();
  readonly #books = new Map<string, BookState>();
  /** The fill model object (the judge), kept for the GRADED bus's META self-description (ADR-0011 B). */
  readonly #fillModelObj: FillModel;
  /** The author's captured experimental envelope (a57.14 / 5zl.6), or `undefined` for a config-less run. */
  readonly #envelope: ExperimentalEnvelope | undefined;
  /** The run's FULL ConfigId (m9i.2 owner tweak), or `undefined` for a config-less run. */
  readonly #configId: string | undefined;
  /** The run's cell config axis (fillSeed stripped — ADR-0016 §3), or `undefined` when it equals `#configId`
   * (a seedless run) or there is no config. Stamped as `meta.cell_config_id`; the axis `cellOf` keys on. */
  readonly #cellConfigId: string | undefined;
  /** The sampled-channel qualification claim (kestrel-9gu.12), or `undefined` — the fail-closed default
   * under which the headline gate resolves to the strict-cross floor (9gu.8 AC#7). */
  readonly #sampledQualification: SampledQualificationClaim | undefined;
  /** The clock-honest attestation (ADR-0040): `true` for a verified clocked session, else `undefined`. */
  readonly #clockHonest: true | undefined;
  /** The armed document set, in arm order — the source of the Instance identity (lineage/version). */
  readonly #modules: Module[] = [];
  /** The last AUTHORED standing module (the View/Wake/Plan book) — the multi-session carry document
   * (the multi-session horizon design). Excludes synthesized one-shot `placeOrder` arms (they are transient order actions, not
   * the authored book), so a carried document never re-fires a stale order next session. */
  #standingModuleAuthored: Module | undefined;
  #drainedCount = 0;
  #spotDrainedCount = 0;
  #settleSpot: number | undefined;
  #settleTs: number;
  #settle: ReturnType<SimFillEngine["settle"]> | undefined;
  // Settle-mark provenance (kestrel-xwf): WHEN the current #settleSpot VALUE was established
  // (`asOf` — a frozen feed re-printing the same px does not refresh it; the 2025-04-09 failure
  // mode), and the last spot-bearing event of any kind (`lastObserved` — feed-death detection).
  // Tracked as injected-clock `ts` only, never input `seq` (seq shifts under JOURNAL interleaving
  // and emitted bytes must not depend on it — the a57.11 invariant). The SPOT settle reads the
  // same `asOf` as its mark watermark (ADR-0017 staleMark provenance).
  #settleSpotAsOfTs: number | null = null;
  #settleSpotLastObservedTs: number | null = null;
  /** The resolved settle mark (kestrel-xwf) — provenance + verdict + annotated recovery, after {@link settle}. */
  #settleMark: SettleMark | undefined;
  #spotSettle: SpotSettleReport | undefined;
  /** kestrel-xwf parity-candidate freshness: per exec-symbol leg (`strike|right`), the last two-sided
   * quote CARRIED on a BOOK event and the `ts` it was carried. A leg carried with a dark side is
   * DELETED (the market said the quote is gone). At settle, only legs fresh within the declared
   * settle-mark staleness threshold qualify as a CLOSING strike for the parity recovery — a
   * two-sided pair surviving only in the fold from before a feed death is not a closing quote. */
  readonly #parityQuotes = new Map<string, { readonly leg: OptionQuote; readonly ts: number }>();

  constructor(opts: SessionCoreOptions) {
    this.meta = opts.meta;
    this.execSpec = opts.specs.find((s) => s.role === "exec") ?? opts.specs[0]!;
    this.signalSpec = opts.specs.find((s) => s.role === "signal") ?? opts.specs[0]!;
    this.#fairTauYears = opts.fairTauYears;
    this.#fairIsGraded = opts.fillModel === "maker-fair-v1";
    this.#settleTs = opts.firstTs;

    const model = makeFillModel(opts.fillModel, opts.makerFairParams);
    this.#fillModelObj = model;
    this.#envelope = opts.envelope;
    this.#configId = opts.configId;
    this.#cellConfigId = opts.cellConfigId;
    this.#clockHonest = opts.clockHonest;
    // The sampled-channel qualification claim (kestrel-9gu.12) is REFUSED LOUDLY when structurally
    // contradictory (RUNTIME §8): qualification without a seeded run has nothing it could qualify, and
    // qualification under an uncalibrated judge would license an uncalibrated headline — the exact thing
    // the gate exists to forbid (9gu.8 AC#7; ADR-0016 §5). Absent (every run today) ⇒ fail-closed floor.
    if (opts.sampledQualification !== undefined) {
      if (opts.fillSeed === undefined) {
        throw new Error(
          "session: sampledQualification claimed with the sampled channel OFF (no fillSeed) — nothing to qualify (fail-closed, kestrel-9gu.12)",
        );
      }
      if (model.calibration?.calibrated !== true) {
        throw new Error(
          "session: sampledQualification claimed under an uncalibrated judge — never an uncalibrated headline (fail-closed, kestrel-9gu.12 / 9gu.8 AC#7)",
        );
      }
    }
    this.#sampledQualification = opts.sampledQualification;
    // Arm the seeded sampled fill channel when the config declared a fillSeed (ADR-0016 / kestrel-9gu.8.3);
    // absent ⇒ the sampler is off (fail-closed — the deterministic floor + expected rails only).
    this.fill = new SimFillEngine({
      model,
      multiplier: this.execSpec.multiplier,
      ...(opts.fillSeed !== undefined ? { sampler: { runSeed: opts.fillSeed } } : {}),
    });
    // The spot sibling (ADR-0017): equity multiplier is the exec spec's (1 for an equity — the
    // asset-class default of specFromMeta). Only the strict-cross family has a spot judge today.
    this.spotFill = new SpotFillEngine({ multiplier: this.execSpec.multiplier });
    // One Session path, only the gate differs (kestrel-7o2.4): the SIM driver selects its gate through
    // the mode-keyed factory. `sim` returns the identical SimGate over the identical ingredients — a pure
    // refactor, byte-identical to the hardwired `new SimGate(...)` this replaced.
    this.gate = makeGate("sim", {
      sim: {
        fill: this.fill,
        spotFill: this.spotFill,
        spotAllowed: opts.fillModel === "strict-cross-v1",
        drain: () => void this.#drain(),
      },
    });

    this.state = new CanonicalState({ instrument: this.signalSpec.symbol });

    // The LIVE-gate seam (OSS-ADR-0053): if a paper/live driver injected a `liveGate` factory, build its
    // gate now (canonical state + the folded books + the emitted stream all exist) and route the engine
    // through it instead of the reference SimGate. The SimGate above is still built — it stays INERT (no
    // order ever rests in its fill engine) so `settle`/`buildReport` keep working untouched, and so the
    // sim path is byte-identical (this whole branch is skipped when `liveGate` is absent). The venue's
    // own ORDER events land on the session's single live bus through `appendVenueEvent`; a fill is queued
    // for the post-sweep feedback the per-event pass drains.
    this.#liveBinding = opts.liveGate?.({
      state: this.state,
      execSpec: this.execSpec,
      signalSpec: this.signalSpec,
      books: this.#books,
      appendVenueEvent: (ev) => {
        const { seq: _venueSeq, ...rest } = ev as (BusEvent | NewBusEvent) & { seq?: number };
        const stamped = this.emitted.emit(rest as NewBusEvent);
        if (stamped.stream === "ORDER" && stamped.type === "fill") this.#pendingVenueFills.push(stamped as OrderEvent);
        return stamped;
      },
    });
    this.#activeGate = this.#liveBinding?.gate ?? this.gate;

    const baseProvider = new CanonicalSeriesProvider(this.state, new FakeOrgFacts(), {
      timeOfDayMinutes: (now) => etMinuteOfDay(now),
      fillEvent: (ev) => this.#fillTelemetry.assess(ev),
    });
    // The paper path wraps the provider in the feed-death staleness gate (RUNTIME §3/§8) so a plan
    // cannot fire off a dead line; sim leaves it bare (byte-identical).
    const provider = this.#liveBinding?.wrapSeriesProvider?.(baseProvider) ?? baseProvider;
    this.engine = new PlanEngine({
      instruments: [...opts.specs],
      seriesProvider: provider,
      gate: this.#activeGate,
      busWriter: { emit: (ev) => void this.emitted.emit(ev) },
      rUsd: opts.rUsd,
      now: opts.firstTs,
      fairTauYears: opts.fairTauYears,
      ...(opts.tapeRegimeScopes !== undefined ? { tapeRegimeScopes: opts.tapeRegimeScopes } : {}),
    });

    // Multi-session open (the multi-session horizon design, kestrel-5zl.16.2): seed the carried inventory as this session's
    // opening book. Each carried lot re-opens at its carry-mark basis, emitting an opening `place`+`fill`
    // (drained onto the graded stream) so the Blotter projects the position and `settle` marks it. Refs are
    // ordinal-namespaced so a carried lot can never collide with a fresh `agent-order-N` this session. An
    // absent / empty carry seeds nothing — the existing flat-open path, byte-identical.
    const carry = opts.openingCarry;
    if (carry !== undefined && carry.positions.length > 0) {
      const sessionOrdinal = carry.priorOrdinal + 1;
      this.gate.now = opts.firstTs;
      carry.positions.forEach((p, i) => {
        this.fill.seedFilled(
          {
            ref: `carry:${sessionOrdinal}:${i}`,
            ...(p.plan !== undefined ? { plan: p.plan } : {}),
            ...(p.plan_instance !== undefined ? { plan_instance: p.plan_instance } : {}),
            instrument: p.instrument,
            side: p.side,
            qty: p.qty,
            px: p.basis,
            strike: p.strike,
            right: p.right,
          },
          p.basis,
          opts.firstTs,
        );
      });
      this.#drain(); // carry the seeded place/fill ORDER events onto the emitted stream (+ own-fill telemetry)
    }
  }

  /** Carry both fill engines' fresh ORDER events onto the emitted stream (stamping `seq`) and fold
   * each into own-fill telemetry. Returns the stamped events (the caller feeds fills back). A gate
   * operation touches exactly one engine, so the options-then-spot drain order never interleaves
   * one call's events (deterministic; an options-only session drains zero spot events). */
  #drain(): BusEvent[] {
    const fresh = [
      ...this.fill.events.slice(this.#drainedCount),
      ...this.spotFill.events.slice(this.#spotDrainedCount),
    ];
    this.#drainedCount = this.fill.events.length;
    this.#spotDrainedCount = this.spotFill.events.length;
    return fresh.map((e) => {
      const stamped = this.emitted.emit(e);
      this.#fillTelemetry.observe(stamped);
      return stamped;
    });
  }

  /** Arm one already-normalized document module (RUNTIME §5). Retained (in arm order) as the source
   * of the GRADED bus's Instance identity (lineage = the authored name; version = plans_sha256). */
  arm(mod: Module, opts?: { synthesizedOrder?: boolean }): void {
    // Arm FIRST, record the lineage only on success: a document the engine refuses to arm must NOT enter
    // the Instance identity, so a fail-closed driver (which catches the throw) never stamps a phantom,
    // never-armed module onto the graded META. Two refusal shapes (OSS-ADR-0045): a `plan-name-in-use`
    // (F4) THROWS from armDocument directly; a per-statement `unknown-series` refusal comes back as DATA
    // on the report — this session driver refuses the WHOLE document (aggregating every coded refusal, so
    // nothing is silently dropped), matching the prior whole-document fail-loud boundary.
    const report = this.engine.armDocument(mod, opts);
    if (report.refusals.length > 0) throw armRefusalError(report.refusals);
    this.#modules.push(mod);
    // The AUTHORED book is the standing document that carries across sessions (the multi-session horizon
    // design, kestrel-5zl.16.2). A synthesized one-shot `placeOrder` arm is a transient order action, NOT
    // the authored book — it must never become the carried document (or it would re-fire a stale order next
    // session). Recorded only on a SUCCESSFUL arm (after armDocument), so a refused document never carries.
    if (opts?.synthesizedOrder !== true) this.#standingModuleAuthored = mod;
  }

  /** Cancel a resting order by its real Gate ref on behalf of an agent `cancelOrder` (kestrel-5zl.5 / #3c).
   * Pins the injected clock and routes through the engine so the engine dump (the wake snapshot's
   * `resting[]` source) and the emitted bus (the ORDER `cancel`) stay consistent in the SAME snapshot —
   * never one wake late. Returns whether a resting order was actually removed (fail-closed refuse on a
   * ref that matches nothing currently resting). */
  cancelOrder(ref: string, now: number): boolean {
    this.gate.now = now;
    return this.engine.cancelOrderByRef(ref, now);
  }

  /** FLATTEN an instrument on behalf of an agent `flatten` action (bd if0 / 75n). Pins the injected clock
   * and routes through the engine, which — for every managing plan holding the instrument — cancels the
   * plan's resting orders (neutralizing the rolling TP re-post) AND crosses a COVERED close through the
   * SAME Gate a fired Plan uses (never-naked holds; SELL floored at intrinsic). The gate drains its ORDER
   * cancel/place onto the emitted stream in the same step, so the wake snapshot's `resting[]` (engine dump)
   * and `engineLog` (bus projection) agree within one snapshot. Returns the closed contracts + plans
   * flattened (the driver's audit; a flat book returns `{0,0}` — a clean no-op). */
  flatten(instrument: string, now: number): { closedQty: number; plansFlattened: number } {
    this.gate.now = now;
    return this.engine.flatten(instrument, now);
  }

  /** The judge-stamped opening META of this session's GRADED bus (ADR-0011 part B): the input header
   * plus `fill_model` + `instance` + `fidelity`, self-describing the judge that produced these fills.
   * A pure derivation of the session inputs (fill model, armed documents, mode) — known at open. */
  get gradedMeta(): MetaEvent {
    return stampGradedMeta(this.meta, {
      fillModel: fillModelStampOf(this.#fillModelObj),
      instance: instanceIdentityOf(this.#modules, this.meta.mode),
      fidelity: fidelityOf(this.meta.mode),
      ...(this.#envelope !== undefined ? { envelope: this.#envelope } : {}),
      ...(this.#configId !== undefined ? { configId: this.#configId } : {}),
      ...(this.#cellConfigId !== undefined ? { cellConfigId: this.#cellConfigId } : {}),
      ...(this.#sampledQualification !== undefined ? { sampledQualification: this.#sampledQualification } : {}),
      ...(this.#clockHonest === true ? { clockHonest: true as const } : {}),
    });
  }

  /** The canonical GRADED bus: the judge-stamped META (seq 0) + every emitted engine reaction (incl.
   * the settle-outcome telemetry), re-stamped gap-free from 0 — the self-describing artifact a Blotter
   * projector consumes, and the bus slice 1's enabling-property proof re-derives totals + support from. */
  gradedBus(): BusEvent[] {
    return assembleGradedBus(this.gradedMeta, this.emitted.events);
  }

  /**
   * Project this session's canonical GRADED bus into its {@link Blotter} (kestrel-a57.1 slice 2,
   * ADR-0011). A pure projection of {@link gradedBus} — the Bus is the truth, the Blotter its
   * regenerable projection (ADR-0010); no engine-state scrape. The existing {@link EpisodeReport}
   * (see {@link buildReport}) and the CLI/Ledger stand UNCHANGED beside it — the report→Blotter
   * unification is a later cleanup. Call at finalize (after {@link settle}), as the driver does.
   */
  blotter(): Blotter {
    return project(this.gradedBus());
  }

  /** Supersede the standing document set at `now` (RUNTIME §5, "document supersession"): de-arm
   * un-fired plans, halt acquisition + cancel unfilled children, inventory + TP/EXIT ride. The
   * caller {@link arm}s the replacement immediately after. */
  supersede(now: number): void {
    this.gate.now = now;
    this.engine.supersede(now);
    this.#drain(); // any cancels the supersession pulled land on the emitted stream now
  }

  /**
   * FAIL CLOSED on an unknowable τ (kestrel-wcnd). Reached when the exec instrument's BOOK names no
   * resolvable `expiry` (absent / unparseable / an unresolvable tag like `1dte` or `weekly`), so
   * {@link expiryTauYears} returns `null` and `@fair` is UNBUILDABLE for this book.
   *
   * The predecessor could not reach this state at all: it read τ off the SESSION DATE, so a book
   * with no expiry — or any expiry — silently got a same-day τ. That silent assumption is the bug;
   * an unknown tenor now de-arms with a machine-readable reason ({@link FAIR_TAU_UNKNOWN_CODE} on a
   * CONTROL `disarm`, which the Blotter projects as a reason-carrying de-arm record, kestrel-2pl).
   *
   * Scope is deliberately exact, not maximal:
   * - **Only under `maker-fair-v1`**, the model whose graded fill hazard IS a function of `@fair`
   *   (RUNTIME §6). With no `@fair` it degrades to `pFill: 0` / `kind: "no-fair"` for the whole
   *   session — a *silent* zero-fill floor that reads like a legitimate "nothing filled" grade.
   *   `strict-cross-v1` never reads `@fair`, so an unknown τ moves nothing it decides and de-arming
   *   a session it can still grade honestly would be refusal, not fail-closure. (The AUTHOR-side
   *   `@fair` anchor keeps its own ladder either way: `resolveFair` falls back to an ANNOTATED book
   *   value that always names itself — never a silent mid, RUNTIME §4.)
   * - **Only the exec instrument's book** — the one `@fair` is priced off. A signal-side chain
   *   prices nothing.
   * - **Once per session** — the tape's expiry cannot change, so a second record would be noise.
   *
   * The reason is CLASSIFIED, never echoed: it names `expiry-absent` / `expiry-unresolvable`, never
   * the raw token. An expiry DATE is calendar identity, and CONTROL notes are projected onto author
   * surfaces — echoing `2024-06-21` would re-leak the date the frame seam strips (`assertDateBlind`).
   */
  #failClosedOnUnknownTau(ev: BookEvent): void {
    if (this.#fairTauDisarmed) return;
    if (!this.#fairIsGraded) return;
    if (ev.instrument !== this.execSpec.symbol || ev.legs.length === 0) return;
    this.#fairTauDisarmed = true;
    const why = ev.expiry === undefined ? "expiry-absent" : "expiry-unresolvable";
    this.emit({
      ts: ev.ts,
      stream: "CONTROL",
      type: "disarm",
      note: `${FAIR_TAU_UNKNOWN_CODE}: ${why} on the exec book — time-to-expiry is unknowable, so @fair is UNKNOWN under maker-fair-v1; de-armed (never a same-day-expiry assumption)`,
    });
    this.supersede(ev.ts);
  }

  /** Emit a raw (seq-less) bus event onto the emitted stream — the day driver's CONTROL/WAKE
   * handshake records (never ORDER; those come from the gate/fill engine). */
  emit(ev: NewBusEvent): BusEvent {
    return this.emitted.emit(ev);
  }

  /**
   * Advance one bus event with the load-bearing ordering (RUNTIME §7): pin the clock, fold
   * canonical state, then — on a BOOK event — reassess resting orders for fills (the market acts
   * FIRST, fills fed back into the engine) BEFORE the engine sweep reacts to that same book.
   */
  step(ev: BusEvent): void {
    // JOURNAL is author metadata, NOT an engine input (a57.11); TELEMETRY is engine OUTPUT and
    // observational only (a57.9) — neither is ever an engine input. The per-event pass never reacts
    // to them: returning before touching the clock, canonical state, or the engine is what makes
    // the determinism invariant hold by construction — "same input events in ⇒ same engine events
    // out", regardless of any JOURNAL/TELEMETRY records interleaved on the bus (e.g. when a prior
    // emitted bus is replayed as input). It also means a record appended after the last market
    // event cannot move the settle clock.
    if (ev.stream === "JOURNAL" || ev.stream === "TELEMETRY") return;

    const live = this.#liveBinding;
    // LIVE ONE-BUS (OSS-ADR-0053): a paper/live session has no separately-recorded input tape — the
    // venue's own tape events and the engine's emissions share ONE Bus in arrival order. So the input
    // event is appended to the emitted stream FIRST, before the sweep interleaves the engine's own
    // reactions. A sim session keeps the two-bus separation (input replayed, output graded apart), so
    // this is skipped and the emitted stream stays engine-output-only — byte-identical.
    if (live !== undefined) this.#appendInput(ev);

    this.#activeGate.now = ev.ts;
    this.#settleTs = ev.ts;
    this.state.applyEvent(ev);

    if (ev.stream === "TICK" && ev.type === "BOOK") {
      this.#observeSpot(ev.underlier_px, ev.ts);
      this.#books.set(ev.instrument, foldBook(ev, this.#books.get(ev.instrument)));
      // Track parity-candidate freshness (kestrel-xwf): a two-sided leg CARRIED on this event is a
      // live closing quote as of `ev.ts`; a carried dark side retires it. Un-carried legs keep their
      // last freshness — and age out of the closing window naturally at settle.
      if (ev.instrument === this.execSpec.symbol) {
        for (const leg of ev.legs) {
          const key = `${leg.strike}|${leg.right}`;
          if (leg.bid !== null && leg.ask !== null) this.#parityQuotes.set(key, { leg, ts: ev.ts });
          else this.#parityQuotes.delete(key);
        }
      }
      // kestrel-wcnd: τ comes from the CONTRACT's own expiry, carried on THIS book event — never
      // from the session date. `null` ⇒ the expiry names no resolvable date ⇒ `@fair` is UNKNOWN
      // for this book, and the session fails closed (below) instead of assuming same-day.
      const tau = this.#fairTauYears(ev.ts, ev.expiry);
      if (tau === null) this.#failClosedOnUnknownTau(ev);
      reassessBook(ev, this.fill, () => this.#drain(), this.engine, tau);
    }

    // SPOT-tick reassessment (ADR-0017 / kestrel-20f.14): the spot instrument's own NBBO rides the
    // SPOT tick's additive bid/ask, so resting SPOT orders reassess HERE — the market acts first,
    // fills fed back into the engine BEFORE its sweep reacts to this same tick (the identical
    // fills-before-sweep ordering the BOOK path keeps). An options tape carries no bid/ask, so the
    // quote is dark, nothing crosses, and the emitted stream is byte-identical to before. The
    // observation also folds the settle mark + its provenance (kestrel-xwf).
    if (ev.stream === "TICK" && ev.type === "SPOT") {
      this.#observeSpot(ev.px, ev.ts);
      this.spotFill.onQuote(ev.instrument, { bid: ev.bid ?? null, ask: ev.ask ?? null, last: ev.px }, ev.ts);
      for (const s of this.#drain()) {
        if (s.stream === "ORDER" && s.type === "fill") this.engine.onEvent(s);
      }
    }

    // THE ENGINE SWEEP (RUNTIME §7). On a sim session this is a bare `engine.onEvent(ev)`, byte-identical
    // to before. On a live session it also drains the venue fills queued DURING the sweep back into the
    // engine (the broker fills-AFTER-sweep ordering — the venue acted, the engine reacts next), and the
    // whole thing runs inside the driver's error boundary so a wall's refusal (ClampRefused /
    // IbkrOrderRefused) becomes a typed session STAND_DOWN instead of an unmapped throw.
    const runSweep = (): void => {
      this.engine.onEvent(ev);
      if (live !== undefined) {
        for (let f = this.#pendingVenueFills.shift(); f !== undefined; f = this.#pendingVenueFills.shift()) {
          live.onVenueFill?.(f);
          this.engine.onEvent(f);
        }
      }
    };
    if (live?.guardSweep !== undefined) live.guardSweep(runSweep);
    else runSweep();
  }

  /** Append one INPUT tape event onto the session's single live bus (OSS-ADR-0053), stripping the
   * venue's own `seq` so the session stamps its own monotonic one — two `seq` spaces on one bus is not
   * a bus. Live sessions only; a sim session never calls this. */
  #appendInput(ev: BusEvent): BusEvent {
    const { seq: _venueSeq, ...rest } = ev as BusEvent & { seq?: number };
    return this.emitted.emit(rest as NewBusEvent);
  }

  /** Fold one spot observation into the settle mark + its provenance (kestrel-xwf). The as-of
   * refreshes ONLY when the observed VALUE changes: a frozen feed that keeps re-printing the same px
   * (2025-04-09: 174.13 from 13:01 through the close) must not launder an hours-old mark into a
   * fresh-looking one. Every observation refreshes `lastObserved` (feed-death disambiguation). A
   * genuinely motionless px past the threshold therefore also grades stale — fail-closed: taint for
   * a human, never a silent fresh number (RUNTIME §8). */
  #observeSpot(px: number, ts: number): void {
    this.#settleSpotLastObservedTs = ts;
    if (this.#settleSpot === px && this.#settleSpotAsOfTs !== null) return; // frozen re-print: as-of stays
    this.#settleSpot = px;
    this.#settleSpotAsOfTs = ts;
  }

  /** Cash-settle at the final exec spot (RUNTIME §6): held OPTION inventory → intrinsic; held
   * SPOT inventory → mark-to-final-spot (ADR-0017 — no expiry, no intrinsic; the mark carries the
   * kestrel-xwf as-of watermark, so a stale final print is flagged, never silently marked to);
   * resting → $0. Threads the settle mark's provenance so the settle spot never grades without its
   * as-of/staleness verdict (kestrel-xwf). Drains the expired-unfilled cancels + settle telemetry
   * onto the emitted stream. Call once,
   * after the last step.
   *
   * `opts.carry` (the multi-session horizon design, kestrel-5zl.16.3) — a NON-final session of a multi-session Instance —
   * marks held OPTION inventory to close and lets it SURVIVE into {@link carriedInventory} (the emitted bus is
   * byte-identical to an intrinsic settle; only the bus-silent carried hand-off differs). Absent / `false`
   * — the final or a standalone session — is today's terminal intrinsic settle exactly (the position dies),
   * so a length-1 sequence is byte-for-byte unchanged. SPOT inventory carry (the overnight equity swing) is
   * NOT built yet (kestrel-20f.8): a carry settle that would strand a net spot position REFUSES loudly
   * rather than silently terminal-marking it (fail-closed, RUNTIME §8); a flat/closed spot book carries fine.
   *
   * kestrel-xwf: the mark's provenance is threaded to the engine, which grades it fail-closed (stale =
   * unstated as-of OR older than the declared threshold) and — when STALE — applies the closing
   * put-call-parity candidate derived here from the last folded two-sided book of the exec symbol
   * (the ANNOTATED recovery: `source: "parity"`, the book settles against the recovered spot) or
   * honestly refuses (`source: "stale"`, mark-uncertain, the last mark retained). The unified
   * `settle_mark` TELEMETRY record rides the graded bus whenever at least one order settled. */
  settle(opts?: { readonly carry?: boolean }): ReturnType<SimFillEngine["settle"]> {
    // Fail-closed-LOUD close (kestrel-j43g): a still-armed, never-fired plan whose entry WHEN spent the
    // whole session UNRESOLVABLE — it names a series this tape never wrote, or it never once reached a
    // definite verdict — de-arms HERE with the classified reason, instead of ending the session `armed`
    // with no receipt on any surface. A plan with no `ttl` never reaches the y6vo ttl drop site, so this
    // is the only close-out it can get. Deterministic: the injected settle instant, never a clock
    // (RUNTIME §0). A definite-but-never-crossed plan is legitimately waiting and is left untouched, so
    // an ordinary idle session stays byte-identical. Emitted BEFORE the fill settle so the plan's
    // terminal step precedes the settle telemetry, and drained by the `#drain()` below.
    this.engine.deArmUnresolvedAtClose(this.#settleTs);
    // The parity CANDIDATE (kestrel-xwf annotated recovery) — derived from the CLOSING legs only:
    // quotes carried two-sided on a BOOK event within the declared staleness threshold of the settle
    // instant (a pair surviving only in the fold from before a feed death is not a closing strike).
    // The engine applies the candidate only on a STALE verdict.
    const closingLegs = [...this.#parityQuotes.values()]
      .filter((q) => this.#settleTs - q.ts <= SETTLE_MARK_STALE_AFTER_MS)
      .map((q) => q.leg);
    const parity = closingParitySpot(closingLegs.length > 0 ? { legs: closingLegs } : undefined, this.#settleSpot ?? 0);
    this.#settle = this.fill.settle(this.#settleSpot ?? 0, this.#settleTs, {
      ...(opts?.carry !== undefined ? { carry: opts.carry } : {}),
      mark: {
        asOfTs: this.#settleSpotAsOfTs,
        lastObservedTs: this.#settleSpotLastObservedTs,
        ...(parity !== null ? { parity: { spot: round(parity.spot), strike: parity.strike } } : {}),
      },
    });
    this.#settleMark = this.#settle.settleMark;
    // Spot inventory marks to the same RESOLVED settle spot, carrying the SAME kestrel-xwf
    // value-established as-of as its watermark (a frozen re-print never refreshes it) — `staleMark`
    // flags a mark whose source predates the settle instant (ADR-0017 provenance).
    this.#spotSettle = this.spotFill.settle(
      this.#settleMark.px,
      this.#settleTs,
      this.#settleSpotAsOfTs ?? this.#settleTs,
    );
    if (opts?.carry === true) {
      const net = new Map<string, number>();
      for (const o of this.#spotSettle.outcomes) {
        if (!o.floorFilled) continue;
        net.set(o.instrument, (net.get(o.instrument) ?? 0) + (o.side === "buy" ? o.qty : -o.qty));
      }
      for (const [instrument, qty] of net) {
        if (qty !== 0) {
          throw new Error(
            `session: carry settle with a net spot position (${qty} ${instrument}) — spot overnight carry is not built (kestrel-20f.8); fail-closed, RUNTIME §8`,
          );
        }
      }
    }
    this.#drain();
    return this.#settle;
  }

  /** The carry-mark lots a `settle({ carry: true })` marked-and-kept — this session's contribution to the
   * next session's opening book (the multi-session horizon design). Empty after a final / intrinsic settle. */
  carriedInventory(): readonly CarriedLot[] {
    return this.fill.carriedInventory();
  }

  /** The spot settle report (mark-to-final-spot, ADR-0017), after {@link settle}. Empty outcomes
   * on an options-only session. */
  get spotSettle(): SpotSettleReport | undefined {
    return this.#spotSettle;
  }

  /** The standing AUTHORED module at session end — the last authored View/Wake/Plan book (the carried
   * standing document, re-armed fresh next session), or `undefined` if this session authored no book
   * (synthesized one-shot orders do not count). */
  get standingModule(): Module | undefined {
    return this.#standingModuleAuthored;
  }

  /** Every armed plan STATEMENT the core holds, in arm order (kestrel-wa0j.29) — the typed ASTs the sim
   * driver captures the ENFORCED terms from for the `armed-plan` pane (the watcher's role-keyed percept:
   * it manages plans whose document text it otherwise never sees). A read-only projection of the armed
   * module set — it holds the SAME ASTs the engine armed, never a re-parse. */
  get armedPlanStatements(): readonly PlanStatement[] {
    const out: PlanStatement[] = [];
    for (const mod of this.#modules) for (const s of mod.statements) if (s.kind === "plan") out.push(s);
    return out;
  }

  /** The resolved settle-mark provenance + verdict + annotated recovery (kestrel-xwf), available
   * after {@link settle} (the engine's {@link SettleMark}, unchanged). */
  get settleMark(): SettleMark | undefined {
    return this.#settleMark;
  }

  /** Build the graded report (after {@link settle}). `busEventCount` and `busSha256` pin the
   * input bus alongside the emitted-stream determinism hash (F6). */
  buildReport(busEventCount: number, busSha256: string): EpisodeReport {
    if (this.#settle === undefined) throw new Error("session: buildReport before settle");
    return buildReport(
      this.meta,
      busEventCount,
      this.emitted,
      this.gate,
      this.#settle,
      busSha256,
      this.#settleTs,
      this.#spotSettle,
      this.#sampledQualification,
    );
  }

  /** The latest folded option book per instrument — the chain a Frame renders. */
  get books(): ReadonlyMap<string, BookState> {
    return this.#books;
  }
  /** The current injected clock (the last stepped event's `ts`, or `firstTs` before any step). */
  get settleTs(): number {
    return this.#settleTs;
  }
}

// `requireMeta`/`resolveSpecs`/`specFromMeta` — the driver's PURE header rules (META → instruments) —
// live in `./specs.ts` (factored light for the CLI's `validate --arm` context derivation, kestrel-elu9)
// and are re-exported here unchanged so every existing consumer (simulate/paper/session barrel) keeps
// importing them from the sim driver. NOTE: one `import` + an export LIST, deliberately — pairing an
// `import` with a separate `export … from` of the same names makes `bun build --splitting` emit a
// chunk with duplicate export statements, which node rejects at load ("Duplicate export of
// 'requireMeta'"), killing every bundled verb.
export { requireMeta, resolveSpecs, specFromMeta };

/** The sha256 of the serialized input bus — pins WHAT a run was graded against (F6). */
export function busSha256Of(events: readonly BusEvent[]): string {
  return sha256(serializeBus(events));
}

/** The tape's complete regime-scope vocabulary (kestrel-ocyf): every scope any REGIME event on the
 * bus writes, de-duplicated. Callers holding the full bus pass this to the engine so `armDocument`
 * can prove a regime gate dead on arrival (scope outside this set ⇒ it can never arm on this tape). */
export function tapeRegimeScopesOf(events: readonly BusEvent[]): string[] {
  const scopes = new Set<string>();
  for (const e of events) if (e.stream === "REGIME") scopes.add(e.scope);
  return [...scopes];
}

// ─────────────────────────────────────────────────────────────────────────────
// runSimSession
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Drive one `sim` session end to end and return its graded {@link EpisodeReport} (RUNTIME §7).
 * Pure with respect to its inputs (given `events`, no I/O at all); with `busPath` it reads the
 * bus once. If `out` is given, the report is also written there as pretty JSON. Fails closed on
 * a missing META, an unknown fill model, or no instruments (RUNTIME §8). A thin driver over
 * {@link SessionCore}: arm all documents up front, step every event, settle, build.
 */
export function runSimSession(opts: RunSimOptions): EpisodeReport {
  if (opts.busPath === undefined && opts.events === undefined) {
    throw new Error("session: runSimSession needs either `busPath` or `events`");
  }
  const events: BusEvent[] = opts.events !== undefined ? [...opts.events] : [...readBus(opts.busPath!)];

  const meta = requireMeta(events);
  const specs = resolveSpecs(meta, opts.instruments);

  // Dynamic tau by default (RUNTIME §4): `@fair` (and the maker-fair hazard) decay toward
  // intrinsic across the session off the session clock — `max(0, 16:00 ET on session_date − now)`.
  // An explicit `fairTauYears` (e.g. the CLI's `--tau-hours` constant) still overrides it.
  const fairTauYears = opts.fairTauYears ?? expiryTauYears(meta.session_date);

  // Capture the a57.14 envelope only when a config is supplied AND declares an authoring identity
  // (kestrel-5zl.6). A config-less run — every existing runSimSession caller — stamps nothing, so its
  // graded bus stays byte-identical (backward-compatible; the projector leaves it `certified`).
  // The FULL ConfigId rides alongside under the SAME condition (m9i.2 owner tweak): the grid CellKey
  // keys on it, so temperature-only config variants land in DISTINCT cells.
  const envelope = opts.config !== undefined ? envelopeForConfig(opts.config) : undefined;
  const configId = envelope !== undefined ? deriveConfigId(canonicalizeConfig(opts.config!)) : undefined;
  // The cell config axis (fillSeed stripped — ADR-0016 §3): stamped ONLY when it DIVERGES from configId
  // (the config carried a fillSeed) — a seedless run's cell axis equals configId, so no stamp and the graded
  // bus stays byte-identical. This keeps seed-only variants pooled as replicates in ONE cell while configId
  // keeps the seeded RUN identity (SimRunId).
  const cellConfigId = envelope !== undefined ? cellConfigIdOf(opts.config!) : undefined;
  const cellConfigStamp = cellConfigId !== undefined && cellConfigId !== configId ? cellConfigId : undefined;
  // Arm the seeded sampled fill channel iff the config declares a fillSeed (kestrel-9gu.8.3); a config-less
  // run — and any config that omits the seed — leaves the sampler OFF (fail-closed, byte-identical).
  const fillSeed = opts.config !== undefined ? fillSeedOf(opts.config) : undefined;
  // The sampled-channel qualification claim (kestrel-9gu.12): read fail-closed off the config; absent —
  // every config today, until the 9gu.8.6 study passes — the headline gate resolves to the floor.
  const sampledQualification = opts.config !== undefined ? sampledQualificationOf(opts.config) : undefined;

  const core = new SessionCore({
    meta,
    specs,
    fillModel: opts.fillModel,
    rUsd: opts.rUsd,
    fairTauYears,
    firstTs: events[0]?.ts ?? 0,
    // The full bus is in hand — pre-scan its regime vocabulary so a regime gate on a scope this tape
    // never writes gets the DEFINITIVE dead-on-arrival notice at arm (kestrel-ocyf), not a hedge.
    tapeRegimeScopes: tapeRegimeScopesOf(events),
    ...(opts.makerFairParams !== undefined ? { makerFairParams: opts.makerFairParams } : {}),
    ...(envelope !== undefined ? { envelope } : {}),
    ...(configId !== undefined ? { configId } : {}),
    ...(cellConfigStamp !== undefined ? { cellConfigId: cellConfigStamp } : {}),
    ...(fillSeed !== undefined ? { fillSeed } : {}),
    ...(sampledQualification !== undefined ? { sampledQualification } : {}),
  });
  for (const doc of opts.documents) core.arm(toModule(doc));

  for (const ev of events) core.step(ev);
  core.settle();

  const report = core.buildReport(events.length, busSha256Of(events));
  if (opts.out !== undefined) writeReport(opts.out, report);
  return report;
}

/** Reassess every leg of a book event against the resting orders, emit floor fills, and feed
 * each fill back into the engine so inventory/basis/TP advance (RUNTIME §5–§7). */
function reassessBook(
  ev: BookEvent,
  fill: SimFillEngine,
  drain: () => BusEvent[],
  engine: PlanEngine,
  // kestrel-wcnd: τ is RESOLVED BY THE CALLER against this book's own `expiry` (the caller holds the
  // provider + the fail-closed ladder), so this function can never re-derive it from the session date.
  // `null` ⇒ `@fair` is UNKNOWN for every leg here (fail-closed) — never a same-day default.
  tau: number | null,
): void {
  for (const leg of ev.legs) {
    const fairVal = fairFor(ev, leg, tau);
    fill.onBook(ev.instrument, leg, ev.ts, fairVal);
    for (const s of drain()) {
      if (s.stream === "ORDER" && s.type === "fill") engine.onEvent(s);
    }
  }
}

/** ExecutionFair for one leg at a book event, or `undefined` when unbuildable (no injected tau,
 * or the fair model returns null). Only `maker-fair-v1` consumes it; `strict-cross-v1` ignores
 * it (RUNTIME §6). */
function fairFor(ev: BookEvent, leg: OptionQuote, tau: number | null): number | undefined {
  if (tau === null) return undefined;
  const r = executionFair({
    underlyingSpot: ev.underlier_px,
    strike: leg.strike,
    right: leg.right,
    tauYears: tau,
    liquidQuotes: ev.legs,
    asof: ev.ts,
  });
  return r === null ? undefined : r.value;
}

// ─────────────────────────────────────────────────────────────────────────────
// Report assembly
// ─────────────────────────────────────────────────────────────────────────────

function buildReport(
  meta: MetaEvent,
  busEvents: number,
  emitted: EmittedStream,
  gate: SimGate,
  settle: ReturnType<SimFillEngine["settle"]>,
  busSha256: string,
  settleTs: number,
  spotSettle?: SpotSettleReport,
  sampledQualification?: SampledQualificationClaim,
): EpisodeReport {
  // Per-order telemetry FROM THE BUS (a57.9): esc stage per order ref + the session reprice tally
  // are read off the engine's emitted TELEMETRY stream — the report consumes the bus, never
  // scrapes engine internals. Fold `order` records: the last `esc_stage` per ref is the final
  // ladder rung; each `reprice` marks one cancel/replace, so their count is the honest session
  // tally (F6). Fail-closed on a bus that carries no telemetry (e.g. an older emitted stream): the
  // map stays empty and `reprice_count` stays 0 — a documented default (esc stage 0 below), never
  // a crash and never an invented value (RUNTIME §8).
  const escByRef = new Map<string, number>();
  let repriceCount = 0;
  for (const e of emitted.events) {
    if (e.stream !== "TELEMETRY") continue;
    if (e.type === "order") {
      escByRef.set(e.order_id, e.esc_stage); // last-wins ⇒ final esc rung for this ref
      if (e.reprice) repriceCount += 1;
    }
  }

  // Join settle outcomes across BOTH judges (ADR-0017): the options engine's intrinsic settle and
  // the spot engine's mark-to-final-spot settle share the accounting subset the report reads; a
  // ref lives in exactly one engine, so the union is collision-free.
  interface OutcomeLite {
    readonly floorFilled: boolean;
    readonly floorFillPx: number | null;
    readonly expectedFillProb: number;
    readonly floorPnl: number;
    readonly expectedPnl: number;
  }
  const outByRef = new Map<string, OutcomeLite>(settle.outcomes.map((o) => [o.ref, o]));
  for (const o of spotSettle?.outcomes ?? []) outByRef.set(o.ref, o);
  const cancelledRefs = new Set<string>();
  for (const e of emitted.events) {
    if (e.stream === "ORDER" && e.type === "cancel" && e.reason === "cancelled") cancelledRefs.add(e.order_id);
  }

  // Orders — one entry per submitted intent, joined across intent / engine child / settle.
  const orders: OrderReport[] = gate.intents.map((intent) => {
    const oc = outByRef.get(intent.ref);
    const esc = escByRef.get(intent.ref) ?? 0;
    const filled = oc?.floorFilled ?? false;
    const pFill = oc?.expectedFillProb ?? (filled ? 1 : 0);
    const fillPx = filled ? (oc?.floorFillPx ?? null) : null;
    return {
      ref: intent.ref,
      plan: intent.plan,
      plan_instance: intent.plan_instance,
      role: intent.role,
      instrument: intent.instrument,
      side: intent.side,
      qty: intent.qty,
      ...(intent.strike !== undefined ? { strike: intent.strike } : {}),
      ...(intent.right !== undefined ? { right: intent.right } : {}),
      px: round(intent.px),
      annotation: intent.sourceAnnotation,
      esc_stages: esc,
      pFill: round(pFill),
      filled,
      fillPx: fillPx === null ? null : round(fillPx),
      floorPnl: round(oc?.floorPnl ?? 0),
      expectedPnl: round(oc?.expectedPnl ?? 0),
      ...(cancelledRefs.has(intent.ref) && !filled ? { cancelled: true as const } : {}),
    };
  });

  // Plan lifecycle traces from the emitted PLAN stream, ONE trace per exact INSTANCE (kestrel-22j.15).
  // Group BY INSTANCE first — not by name — so a legal same-name replacement keeps its own lifecycle +
  // terminal outcome instead of collapsing onto its predecessor. The emitted stream always carries the
  // engine-minted `plan_instance`, so this keys on the exact id; the fold is shared with the Blotter
  // projector (groupPlanInstances) so the two projections can never drift. Lineage aggregation stays a
  // SEPARATE roll-up over `plan` (the name).
  const plans: PlanTrace[] = groupPlanInstances(emitted.events).map((g) => {
    const lifecycle: PlanLifecycleStep[] = g.events.map((pe) => ({
      ts: pe.ts,
      state: pe.state,
      ...(pe.outcome !== undefined ? { outcome: pe.outcome } : {}),
      ...(pe.reason !== undefined ? { reason: pe.reason } : {}),
    }));
    const last = lifecycle[lifecycle.length - 1]!;
    return { plan: g.plan, plan_instance: g.instance, lifecycle, final_state: last.outcome ?? last.state };
  });

  // Totals — floor / expected across BOTH settle reports; premium = filled BUY debits (for a spot
  // buy that IS the cost basis — the bounded risk of a long equity, ADR-0017).
  let premium = 0;
  for (const o of settle.outcomes) {
    if (o.side === "buy" && o.floorFilled) premium += o.qty * o.px * settle.multiplier;
  }
  for (const o of spotSettle?.outcomes ?? []) {
    if (o.side === "buy" && o.floorFilled) premium += o.qty * o.px * spotSettle!.multiplier;
  }
  const floorTotal = settle.floorTotal + (spotSettle?.floorTotal ?? 0);
  const expectedTotal = settle.expectedTotal + (spotSettle?.expectedTotal ?? 0);

  // Emitted counts by stream.
  const emittedCounts: Record<string, number> = {};
  for (const e of emitted.events) emittedCounts[e.stream] = (emittedCounts[e.stream] ?? 0) + 1;

  // ── the three-channel headline (kestrel-9gu.12) ────────────────────────────
  // floor + expected always (summed across BOTH judges); sampled ONLY on a seeded run (`sampledTotal`
  // present ⇔ the sampler was armed). The headline is SELECTED through the ONE gate policy
  // (src/grade/headline.ts): floor until the 9gu.8.6 qualification claim is on the record, and never
  // when any sampled realization rests on extrapolated support (bankableEv, ADR-0016 §5). A stale
  // settle mark (kestrel-xwf) taints the headline — every channel settled against the same untrustworthy
  // spot — WITHOUT switching it (loud, never silent). A parity recovery ANNOTATES but never PARDONS: a
  // stale parity mark still taints (kestrel-pez unification). The taint reasons mirror the Blotter
  // projector's `settle_mark` phrasing so the two surfaces agree.
  const settleMarkRec = settleMarkRecordOf(settle.settleMark);
  const markTaintReasons: string[] = [];
  if (settleMarkRec.stale) {
    const staleness =
      settleMarkRec.age_ms === null
        ? `settle mark ${settleMarkRec.px} has unstated provenance (no spot observation on the tape)`
        : `settle mark ${settleMarkRec.px} is stale: value last established ${settleMarkRec.age_ms}ms before settle (> ${settleMarkRec.stale_after_ms}ms threshold)`;
    markTaintReasons.push(
      settleMarkRec.source === "parity"
        ? `${staleness}; recovered via closing put-call parity (annotated recovery) — P&L settled against it is non-bankable (kestrel-xwf)`
        : settleMarkRec.mark_uncertain
          ? `${staleness}; settle mark uncertain — no closing put-call parity derivable, last-known mark retained — P&L settled against it is non-bankable (kestrel-xwf, fail-closed)`
          : `${staleness} — P&L settled against it is non-bankable (kestrel-xwf)`,
    );
  }
  // The never-naked taint (kestrel-22j.4): a run whose FILLED book ever nets to a naked short (an
  // uncovered short — unbounded on a call / short equity) can never headline as a clean win, however
  // positive its P&L. Read the filled legs in CAUSAL order off the emitted ORDER stream (the same
  // bytes the Blotter sees) and watch the running net per position key — a taint that RIDES the
  // headline exactly like the xwf settle-mark taint, never switching the selected channel.
  const filledLegs: FilledLeg[] = [];
  for (const e of emitted.events) {
    if (e.stream === "ORDER" && e.type === "fill") {
      const key = e.strike !== undefined && e.right !== undefined ? `${e.instrument} ${e.strike}${e.right}` : e.instrument;
      filledLegs.push({ key, side: e.side, qty: e.qty });
    }
  }
  const taintReasons = [...markTaintReasons, ...nakedShortTaint(filledLegs)];
  const sampledUsd = settle.sampledTotal !== undefined ? round(settle.sampledTotal) : undefined;
  const headlineGate = sampledQualified({
    sampledArmed: sampledUsd !== undefined,
    sampledExtrapolated: settle.outcomes.some((o) => o.sampled?.filled === true && o.support === "extrapolated"),
    ...(sampledQualification !== undefined ? { qualification: sampledQualification } : {}),
  });
  const floorUsd = round(floorTotal);
  const expectedUsd = round(expectedTotal);
  const headline = selectHeadline(
    { floor: floorUsd, expected: expectedUsd, ...(sampledUsd !== undefined ? { sampled: sampledUsd } : {}) },
    headlineGate,
    taintReasons,
  );

  return {
    session: {
      date: meta.session_date,
      instruments: meta.instruments.map((i) => i.symbol),
      mode: "sim",
      fill_model: settle.fillModel,
      bus_events: busEvents,
      determinism_hash: emitted.hash(),
      bus_sha256: busSha256,
      reprice_count: repriceCount,
      settle_ts: settleTs,
    },
    plans,
    orders,
    totals: {
      realized_floor_usd: floorUsd,
      expected_usd: expectedUsd,
      ...(sampledUsd !== undefined ? { sampled_usd: sampledUsd } : {}),
      premium_spent: round(premium),
      settle_mark: settleMarkRec,
      headline,
    },
    emitted: emittedCounts,
  };
}

/** The report's fixed-shape settle-mark record (kestrel-xwf) — the engine's {@link SettleMark} with
 * explicit `null`s (never omitted keys), so a report consumer always sees the provenance + verdict +
 * annotated recovery. */
function settleMarkRecordOf(m: SettleMark): EpisodeReport["totals"]["settle_mark"] {
  return {
    px: m.px,
    as_of_ts: m.asOfTs,
    last_observed_ts: m.lastObservedTs,
    age_ms: m.ageMs,
    stale_after_ms: m.staleAfterMs,
    stale: m.stale,
    source: m.source,
    mark_uncertain: m.markUncertain,
    note: m.note ?? null,
  };
}

/** Serialize + write a report to a path as pretty JSON (used by {@link runSimSession}'s `out`
 * and the CLI). */
export function writeReport(path: string, report: EpisodeReport): void {
  writeFileSync(path, JSON.stringify(report, null, 2) + "\n");
}
