/**
 * # fill/engine — SimFillEngine, the pure resting-order state machine (RUNTIME §6–7)
 *
 * A deterministic, time-injected engine that holds resting orders, reassesses them against
 * book updates through a {@link FillModel}, and cash-settles the outcome at session close.
 * It owns no clock and no RNG on its default path (RUNTIME §0): every timestamp is passed in,
 * and the only randomness — an *optional* seeded sampler for Monte-Carlo realization — is
 * injected. **Same event sequence ⇒ same outcomes** (the determinism invariant).
 *
 * Lifecycle of one order:
 *
 * 1. {@link SimFillEngine.place} — the order rests; a `place` ORDER event is emitted.
 * 2. {@link SimFillEngine.cancel} — **effective immediately**; a cancelled order can never
 *    later fill, no matter what the tape does afterward.
 * 3. {@link SimFillEngine.onBook} — every resting order on the updated leg is reassessed. A
 *    strict-cross result is a **floor fill**: the order leaves the book into filled inventory
 *    and a `fill` ORDER event is emitted. A hazard result accrues into the order's survival
 *    product for expected-value accounting; the order keeps resting.
 * 4. {@link SimFillEngine.settle} — at the settle spot: filled inventory settles at
 *    **intrinsic**; still-resting orders **expire worthless-unfilled** (no position, $0
 *    floor). The report carries both the floor outcome and the E[$] under `pFill`
 *    (RUNTIME §6).
 *
 * Expected-value accounting: for each resting order the engine keeps a **survival** product
 * `S = Π(1 − pFillᵢ)`; its expected fill probability is `1 − S`. A floor fill pins that to
 * `1`. At settle, `floorPnl` counts only floor fills, while `expectedPnl` weights every fill's
 * settle P&L by its expected fill probability — so a single grade reports the conservative
 * floor and the calibrated expectation side by side.
 */

import type { NewBusEvent, OptionQuote, OrderAction, OrderPayload, Right } from "../bus/index.ts";
import { mulberry32 } from "../bus/index.ts";
import { sha256 } from "../crypto/sha256.ts";
import { intrinsic } from "../fair/index.ts";
import { isCalibratedSupport, type FillModel, type FillOrderView, type FillSupport, type Moneyness } from "./model.ts";

/**
 * The seeded per-episode RNG substream key (ADR-0016 §3): `hash(runSeed, model.version, order.ref,
 * episodeIndex) → uint32`, folded to a mulberry32 seed. sha256 (portable, deterministic — no wall
 * clock, no unseeded RNG, RUNTIME §0) over the canonical tuple string, top 32 bits parsed as the
 * seed. Adding `episodeIndex` to the prior fill model's `(seed, model.version, ref)` key gives each
 * dark↔lit episode under one ref its own independent, replay-stable draw (deviation D1); a book
 * observation that does NOT mint an episode consumes no substream and perturbs nothing.
 */
function episodeSeed(runSeed: number, modelVersion: string, ref: string, episodeIndex: number): number {
  const h = sha256(`${runSeed}|${modelVersion}|${ref}|${episodeIndex}`);
  return parseInt(h.slice(0, 8), 16) >>> 0;
}

/**
 * The **reference re-mint cadence** for a resting episode-keyed order (ADR-0016 §episode-identity,
 * boundary **B4**, kestrel-9gu.13). A per-EPISODE hazard model (`MakerFairCalV1`) folds one Bernoulli
 * per resting *episode*, and episodes previously minted ONLY on placement/reprice/dark↔lit-flip — so a
 * STATIC resting order (no reprice, no regime flip) took a single draw all session and its cumulative
 * fill could never approach the calibrated ground truth (a patient at-fair BUY pinned at p≈0.25 rather
 * than saturating). B4 re-mints **one lit episode per `DT_REF` of continuous rest in the SAME regime**,
 * matching the upstream reference model's per-observation-at-DT_REF draw (its measured time-to-fill
 * median ≈ 0s makes the per-episode rate a per-`DT_REF`-window rate).
 *
 * It is keyed on the **injected tape clock** (`ts`), never wall time or observation COUNT: the number of
 * re-mints over a rest span is `floor(restMs / DT_REF)`, so a dense tape and a sparse tape covering the
 * same span mint the same episodes (density-independent — "a bus's book cadence is an artifact of
 * recording", ADR-0016 §Context). A re-look INSIDE a window mints nothing (the N1 anti-double-count core
 * is preserved: `windows = 0`). `5_000` ms = the reference model's DT_REF (5s).
 */
export const EPISODE_REMINT_DT_REF_MS = 5_000;

/**
 * Once a resting order's survival product drops below this, its expected fill probability is ≥ `1 − eps`
 * (effectively certain) and further B4 re-mints of the same regime move neither the bankable expected
 * channel nor a still-open sampled draw meaningfully — so the catch-up loop STOPS folding (and stops
 * emitting per-episode hazard telemetry) at this floor. This bounds telemetry and work on a sparse-tape
 * catch-up (a single look after a long rest gap would otherwise mint thousands of saturated episodes)
 * while keeping the engine's survival byte-identical to what the bus telemetry re-derives (both stop at
 * the same deterministic threshold — ADR-0011 re-derivability holds).
 */
const EPISODE_SURVIVAL_SATURATION_EPS = 1e-9;

// ─────────────────────────────────────────────────────────────────────────────
// Orders + outcomes
// ─────────────────────────────────────────────────────────────────────────────

/** An order handed to {@link SimFillEngine.place}. A single defined-risk option leg resting
 * at a limit price. `ref` is the caller's stable id (echoed as `order_id` on the bus). */
export interface FillOrder {
  readonly ref: string;
  /** The plan NAME (Lineage key) that authored the order, when it came from one (carried onto bus events). */
  readonly plan?: string;
  /** The authoring plan's exact INSTANCE identity (kestrel-22j.15), carried onto the order's bus events so
   * an order joins its exact instance, not merely its recurring name. */
  readonly plan_instance?: string;
  readonly instrument: string;
  readonly side: "buy" | "sell";
  readonly qty: number;
  readonly px: number;
  readonly strike: number;
  readonly right: Right;
  /** Coarse moneyness vs the underlier for the directional far-OTM guard. Absent
   * ⇒ the symmetric legacy path (the guard cannot fire on an unclassified leg). Threaded to the
   * {@link FillModel} as {@link FillOrderView.moneyness}. */
  readonly moneyness?: Moneyness;
  /** `true` when this leg is the covering wing of a multi-leg defined-risk package (exempt from the
   * sell-far-OTM cap — it fills as a combo). Threaded as {@link FillOrderView.covered}. */
  readonly covered?: boolean;
}

/** The optional Monte-Carlo sampled outcome (present only when the engine was given a seeded
 * sampler — a `fillSeed`, kestrel-9gu.8.1). Deterministic under its seed. `filled` is the causal
 * realization: `true` iff the order took a strict-cross floor fill (containment: floor ⊆ sampled) OR
 * an episode's draw realized a hazard fill (`draw < pFill`). `pnl` is its settle P&L when filled
 * (else `0`). Off the deterministic floor/expected accounting path — a seeded run is one replicate of
 * the ensemble (ADR-0016). */
export interface SampledOutcome {
  readonly filled: boolean;
  readonly pnl: number;
}

/** One order's settle outcome — the floor and the expectation, side by side (RUNTIME §6). */
export interface OrderOutcome {
  readonly ref: string;
  readonly plan?: string;
  /** The authoring plan's exact INSTANCE identity (kestrel-22j.15), when the order came from one. */
  readonly plan_instance?: string;
  readonly instrument: string;
  readonly side: "buy" | "sell";
  readonly qty: number;
  readonly px: number;
  readonly strike: number;
  readonly right: Right;
  /** Did the strict-cross floor fire for this order? */
  readonly floorFilled: boolean;
  /** The price the floor fill happened at (the resting price), else `null`. */
  readonly floorFillPx: number | null;
  /** `1` if floor-filled, else the accrued expected fill probability `1 − survival`. */
  readonly expectedFillProb: number;
  /** The order-level fill-support flag (9gu.3): `calibrated` iff every assessment folded into this
   * order's survival product was calibrated (a definite floor fill is a structural fact ⇒ always
   * `calibrated`), else `extrapolated` — the conservative, non-bankable default a grader honours. */
  readonly support: FillSupport;
  /** Per-contract intrinsic at the settle spot. */
  readonly intrinsicAtSettle: number;
  /** Realized $ under the strict-cross floor (0 for an unfilled order). */
  readonly floorPnl: number;
  /** E[$] under `pFill`: `expectedFillProb × (settle P&L of the fill)`. */
  readonly expectedPnl: number;
  /** The optional seeded-sampler realization, when a sampler was supplied. */
  readonly sampled?: SampledOutcome;
}

/**
 * The declared default settle-mark staleness threshold (kestrel-xwf): a settle spot whose value was
 * last established more than this many ms before the settle instant grades STALE and taints the
 * headline. 5 minutes — comfortably coarser than the tape's event cadence and the 1-minute structural
 * wake grid (a live feed refreshes the mark many times per threshold), yet orders of magnitude finer
 * than the hours-frozen failure it exists to catch (2025-04-09: frozen from 13:01 through the close).
 */
export const SETTLE_MARK_STALE_AFTER_MS = 300_000;

/** The settle mark's source provenance, threaded INTO {@link SimFillEngine.settle} by the session
 * driver (which sees the input tape — the engine itself never scrapes it). `null` means unstated —
 * the fail-closed default the verdict treats as stale (kestrel-xwf). All fields are injected-clock
 * `ts`, never `seq`: an input event's seq shifts under JOURNAL/TELEMETRY interleaving, and emitted
 * bytes must not depend on that (the a57.11 invariant). */
export interface SettleMarkProvenance {
  /** `ts` of the input event that last ESTABLISHED the settle spot's value (a frozen feed
   * re-printing the same px does not refresh it), or `null` when unstated. */
  readonly asOfTs: number | null;
  /** `ts` of the last spot-bearing input event of ANY kind (feed-death vs frozen-value
   * disambiguation), or `null` when none was observed. */
  readonly lastObservedTs: number | null;
  /** A closing put-call-parity CANDIDATE recovery for a frozen mark (kestrel-xwf annotated
   * recovery), derived by the session driver from the last folded two-sided closing book
   * (`S = K + C_mid − P_mid` at the strike nearest the frozen mark). The engine applies it ONLY
   * when the core verdict grades STALE — the book then cash-settles against `spot` with
   * `source: "parity"`. Absent/`null` = not derivable (dark or one-legged closing book): a stale
   * mark is then retained `mark_uncertain` (honestly refused, never fabricated). */
  readonly parity?: { readonly spot: number; readonly strike: number } | null;
}

/** How a settle mark was resolved (kestrel-xwf; mirrors the bus's `SettleMarkSource` — redeclared
 * here so `src/fill` documents its own vocabulary): `market` = live within the threshold; `parity` =
 * stale primary mark recovered via closing put-call parity (annotated); `stale` = stale and
 * unrecoverable — retained mark-uncertain. */
export type SettleMarkResolution = "market" | "parity" | "stale";

/**
 * The settle mark WITH its provenance + fail-closed staleness verdict (kestrel-xwf): what spot the book
 * cash-settled against, when that value was established, and whether it was stale at the settle instant.
 * `stale === true` means the settled P&L is graded against a mark the tape stopped supporting — a
 * consumer treats it as NON-BANKABLE (the taint rides {@link ../blotter Blotter} `totals.headline`),
 * never silently as a fresh number. The fix is honest provenance, not a better guess: no "true close"
 * is ever invented (RUNTIME §8 fail-closed).
 */
export interface SettleMark {
  /** The settle spot the book marked to (the parity-recovered value when `source: "parity"`;
   * 0 when no spot was ever observed — and then `stale`). */
  readonly px: number;
  readonly asOfTs: number | null;
  readonly lastObservedTs: number | null;
  /** `settle ts − asOfTs`, clamped ≥ 0; `null` when the as-of is unstated. */
  readonly ageMs: number | null;
  /** The declared threshold this verdict was graded under. */
  readonly staleAfterMs: number;
  /** Fail-closed: `true` when `ageMs` is unstated or exceeds `staleAfterMs`. A parity recovery does
   * NOT clear it — the verdict grades the primary feed, not the fallback (non-bankable either way). */
  readonly stale: boolean;
  /** How the mark was resolved (`"market"` iff not `stale`). */
  readonly source: SettleMarkResolution;
  /** `true` iff stale AND unrecovered (`source: "stale"`) — the retained mark is untrustworthy. */
  readonly markUncertain: boolean;
  /** An honest human note naming the condition (frozen level, age, parity strike) — present iff `stale`. */
  readonly note?: string;
}

/** The settle report (RUNTIME §6–7). Stamps its judge — fill-model name/version and hazard
 * calibration — because EVs across fill-model versions do not naively compare (ADR-0006). */
export interface SettleReport {
  readonly settleSpot: number;
  /** The settle spot's provenance + staleness verdict (kestrel-xwf) — `settleSpot` never travels
   * without it, so a stale mark can never grade as a silently fresh-looking number. */
  readonly settleMark: SettleMark;
  readonly fillModel: string;
  readonly hazardVersion?: string;
  readonly calibrated?: boolean;
  readonly multiplier: number;
  readonly outcomes: readonly OrderOutcome[];
  readonly floorTotal: number;
  readonly expectedTotal: number;
  /** Present only when a seeded sampler was supplied. */
  readonly sampledTotal?: number;
}

/** Construction options. `model` is the one required seam; `multiplier` scales per-contract
 * P&L to dollars (default `1` — a caller sets the instrument's contract size); `sampler` is the
 * optional seeded fill sampler (ADR-0016). When present, its `runSeed` keys a per-episode
 * mulberry32 substream (`hash(runSeed, model.version, order.ref, episodeIndex)`) and a draw at each
 * episode mint realizes a **causal** sampled fill when `draw < pFill` — a real `fill` ORDER event at
 * the resting price, fed back into Plan management. **Absent ⇒ the sampled channel is OFF**
 * (fail-closed): the run grades floor + expected only, taking no draws.
 * `staleMarkAfterMs` overrides the declared settle-mark staleness threshold
 * ({@link SETTLE_MARK_STALE_AFTER_MS}, kestrel-xwf). */
export interface SimFillEngineOptions {
  readonly model: FillModel;
  readonly multiplier?: number;
  readonly sampler?: { readonly runSeed: number };
  readonly staleMarkAfterMs?: number;
}

// ─────────────────────────────────────────────────────────────────────────────
// Internal resting state
// ─────────────────────────────────────────────────────────────────────────────

interface RestingState {
  readonly order: FillOrder;
  /** The `ts` of this order's last assessment (or placement). Injected, monotone. */
  lastTs: number;
  /** Survival product `Π(1 − pFillᵢ)`; expected fill probability = `1 − survival`. */
  survival: number;
  /** Latched `true` once ANY assessment folded into `survival` was NOT calibrated (9gu.3 support):
   * an uncalibrated hazard cell, a dark internalization lift, or an unstated flag. The order's
   * expected-$ is then non-bankable — a grader refuses to bank an extrapolated cell — so the
   * settle-outcome telemetry stamps the order `extrapolated`. A floor fill overrides this (structural). */
  supportExtrapolated: boolean;
  /** The seeded-sampler realization, once a draw has fired (latched — one sampled fill per order). */
  sampledFilled: boolean;
  /** For an episode-keyed model: the last EPISODE key already folded into `survival`. A same-key
   * re-look contributes nothing (one Bernoulli per resting episode, not per look). */
  lastEpisodeKey?: string;
  /** The 0-based index of the NEXT episode to mint under this ref (ADR-0016 §1): `0` at placement,
   * `++` after each mint (a dark↔lit flip for a keyed model, every fold for an un-keyed one). The
   * value AT mint keys that episode's draw substream — so re-observing the same regime (N1, no
   * mint) reuses no index and consumes no randomness. */
  episodeIndex: number;
  /** The tape `ts` from which the B4 time-based re-mint counts `DT_REF` windows for the CURRENT
   * continuous regime (ADR-0016 §episode-identity B4, kestrel-9gu.13). Seeded to the placement ts and
   * RESET to the look ts on each regime boundary (a first-look B1 or a dark↔lit B3), then ADVANCED by
   * whole `DT_REF` windows as same-regime rest accrues — so the number of re-mints depends only on the
   * injected clock (density-independent), never on observation count. Keyed models only; an un-keyed
   * per-second-hazard model integrates time via `dtSinceLast` and never consults this. */
  cadenceAnchorTs: number;
}

interface FilledLot {
  readonly order: FillOrder;
  readonly fillPx: number;
  readonly fillTs: number;
  /** Whether the sampler had realized this order in the sampled channel (always `true` for a floor
   * lot — containment floor ⊆ sampled; also `true` for a hazard-draw lot). */
  readonly sampledFilled: boolean;
  /** `true` when this lot came from a SAMPLED hazard draw (not a strict-cross floor fill) — it is
   * NOT a floor fill (`floorFilled` stays `false`, `floorPnl` `0`), only a sampled-channel
   * realization; its `support` may be `extrapolated` (⇒ unbankable, ADR-0016 §5). */
  readonly viaSampled: boolean;
  /** The support of a `viaSampled` lot (the episode cell it realized from). `calibrated` for a floor
   * lot (a structural price-priority fact). */
  readonly support: FillSupport;
  /** The order's survival product at the moment it filled — frozen because a filled order accrues no
   * more. A floor lot pins expected-fill-prob to `1` (a definite fill) and ignores this; a
   * `viaSampled` lot reports the accrued analytic `1 − survivalAtFill` (its expected channel is
   * truncated at removal). */
  readonly survivalAtFill: number;
}

/** A held lot that a **carry-mark** settle (kestrel-5zl.16.3) marked to the session close and let
 * SURVIVE — the multi-session hand-off (the multi-session horizon design). Its `basis` is the carry-mark (the mark price
 * at close), re-baselined so the NEXT session's P&L is measured from it; `order` carries that basis as
 * its `px`, so seeding it into the next session ({@link SimFillEngine.seedFilled}) re-opens the exact
 * position. Only produced by a `settle(…, { carry: true })`; an intrinsic (final/length-1) settle
 * leaves this empty (the position dies), so a length-1 sequence is byte-identical to today. */
export interface CarriedLot {
  readonly order: FillOrder;
  /** The carry-mark (mark-at-close) — the re-baselined basis for the next session. */
  readonly basis: number;
}

// ─────────────────────────────────────────────────────────────────────────────
// SimFillEngine
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The resting-order state machine. Construct one per book (or per session); drive it with
 * `place` / `cancel` / `onBook`, then `settle`. Every mutator returns the typed bus events it
 * produced (seq-less {@link NewBusEvent}s — the owner {@link ../bus/write.ts BusWriter} stamps
 * `seq`); the same events are also appended to the engine's cumulative {@link events} log.
 */
export class SimFillEngine {
  readonly #model: FillModel;
  readonly #multiplier: number;
  readonly #sampler: { readonly runSeed: number } | undefined;
  readonly #staleMarkAfterMs: number;

  readonly #resting = new Map<string, RestingState>();
  readonly #filled: FilledLot[] = [];
  readonly #cancelled = new Set<string>();
  readonly #seen = new Set<string>();
  readonly #events: NewBusEvent[] = [];
  readonly #carried: CarriedLot[] = [];
  /** 9gu.9 stale-print gate: the last trade print SEEN per leg (`instrument|strike|right`), folded on
   * EVERY book look. `OptionQuote.last` is carried forward session-cumulatively by the tape converter
   * (99%+ of last-bearing leg-events on the recorded tapes are stale carries), so the trade-through rule
   * may fire only when the print CHANGED on this event — a fresh execution, never a morning print an
   * afternoon order could not have interacted with. The first sighting of a leg's print is inert (no
   * prior to diff against — conservative; the floor undercounts). The engine strips `last` from the leg
   * it hands the model unless the print is fresh, so all three judges get the gate for free. */
  readonly #lastPrint = new Map<string, number>();
  #settled = false;

  constructor(opts: SimFillEngineOptions) {
    this.#model = opts.model;
    this.#multiplier = opts.multiplier ?? 1;
    this.#sampler = opts.sampler;
    this.#staleMarkAfterMs = opts.staleMarkAfterMs ?? SETTLE_MARK_STALE_AFTER_MS;
  }

  /** The cumulative typed bus events this engine has produced, in order. Read-only. */
  get events(): readonly NewBusEvent[] {
    return this.#events;
  }

  /** The refs of all currently-resting (placed, not cancelled, not floor-filled) orders. */
  restingRefs(): readonly string[] {
    return [...this.#resting.keys()];
  }

  /** A resting order's current expected fill probability (`1 − survival`), or `undefined` if
   * no such order is resting. Exposed for grading/inspection. */
  expectedFillProb(ref: string): number | undefined {
    const st = this.#resting.get(ref);
    return st === undefined ? undefined : 1 - st.survival;
  }

  /**
   * Place a resting order. The `ref` must be unique across the engine's lifetime (a re-used
   * ref is refused loudly — fail-closed, RUNTIME §8). Emits a `place` ORDER event. Returns the
   * order's `ref`.
   */
  place(order: FillOrder, ts: number): string {
    if (this.#settled) throw new Error(`SimFillEngine: place after settle (${order.ref})`);
    if (this.#seen.has(order.ref)) {
      throw new Error(`SimFillEngine: duplicate order ref ${JSON.stringify(order.ref)}`);
    }
    this.#seen.add(order.ref);
    this.#resting.set(order.ref, {
      order,
      lastTs: ts,
      survival: 1,
      supportExtrapolated: false,
      sampledFilled: false,
      episodeIndex: 0,
      cadenceAnchorTs: ts,
    });
    this.#emit("place", ts, order, { px: order.px });
    return order.ref;
  }

  /**
   * Seed a **carried** position (kestrel-5zl.16.2 — the multi-session open). Re-opens a lot handed
   * over by the prior session's carry-mark settle at its carry-mark `basis`: emits a `place` then a
   * `fill` ORDER event (both at `basis`, at the session's open `ts`) so the GRADED bus is
   * self-consistent — the position appears as an opening fill the Blotter projects — and pushes it into
   * filled inventory, where {@link settle} marks it. Never reassessed by {@link onBook} (it is inventory,
   * not a resting order). `ref` must be unique across the engine's lifetime (fail-closed on reuse). No
   * caller seeds this on a length-1 sequence, so this path never runs for an existing single-session run.
   */
  seedFilled(order: FillOrder, basis: number, ts: number): void {
    if (this.#settled) throw new Error(`SimFillEngine: seedFilled after settle (${order.ref})`);
    if (this.#seen.has(order.ref)) {
      throw new Error(`SimFillEngine: duplicate order ref ${JSON.stringify(order.ref)}`);
    }
    this.#seen.add(order.ref);
    // The carried lot's basis IS its resting/fill price — `#outcome` measures P&L off `order.px`.
    const seeded: FillOrder = { ...order, px: basis };
    this.#emit("place", ts, seeded, { px: basis });
    this.#emit("fill", ts, seeded, { px: basis, reason: "carried" });
    // A carried lot is a DEFINITE opening fill (realized before this session), so it is floor-class:
    // not a sampled draw (`viaSampled: false`), structurally `calibrated`, and — containment, ADR-0016
    // §4 — present in the sampled channel too whenever the sampler is armed (a definite fill exists in
    // every channel; `survivalAtFill: 1` is the floor pin, unused for a non-sampled lot).
    this.#filled.push({
      order: seeded,
      fillPx: basis,
      fillTs: ts,
      sampledFilled: this.#sampler !== undefined,
      viaSampled: false,
      support: "calibrated",
      survivalAtFill: 1,
    });
  }

  /** The lots a carry-mark {@link settle} marked to close and let survive — the next session's opening
   * inventory (kestrel-5zl.16.3). Empty until (and unless) a `settle(…, { carry: true })` ran. */
  carriedInventory(): readonly CarriedLot[] {
    return this.#carried;
  }

  /**
   * Cancel a resting order, **effective immediately**: it is removed from the book and can
   * never later fill. A no-op (no event) if the ref is not currently resting (already filled,
   * already cancelled, or unknown). Returns whether a resting order was removed.
   */
  cancel(ref: string, ts: number): boolean {
    const st = this.#resting.get(ref);
    if (st === undefined) return false;
    this.#resting.delete(ref);
    this.#cancelled.add(ref);
    this.#emit("cancel", ts, st.order, { reason: "cancelled" });
    return true;
  }

  /**
   * Reassess every resting order on one leg against a new quote. `fair` is the engine's
   * ExecutionFair for the leg (omit when unbuildable — the model then degrades to the floor).
   * Floor (strict-cross) fills leave the book and emit a `fill` event; hazard results accrue
   * into the survival product and keep resting. Returns the events produced this call.
   */
  onBook(instrument: string, leg: OptionQuote, ts: number, fair?: number): readonly NewBusEvent[] {
    if (this.#settled) throw new Error("SimFillEngine: onBook after settle");
    const before = this.#events.length;

    // 9gu.9 stale-print gate (see #lastPrint): the trade-through arm may test `leg.last` ONLY when the
    // print CHANGED on this event — the tape carries `last` forward session-cumulatively, so an
    // unchanged value is a stale carry, not a fresh execution. The engine hands the model a leg with
    // `last` STRIPPED unless fresh (a pure per-look model cannot see across events; this memory is the
    // engine's). Folded on EVERY look — including looks matching no order — so freshness state is
    // global and an order placed mid-session is inert against the pre-placement print it first sees.
    const legKey = `${instrument}|${leg.strike}|${leg.right}`;
    const priorPrint = this.#lastPrint.get(legKey);
    const freshPrint = leg.last !== undefined && priorPrint !== undefined && leg.last !== priorPrint;
    if (leg.last !== undefined) this.#lastPrint.set(legKey, leg.last);
    let assessLeg = leg;
    if (!freshPrint && leg.last !== undefined) {
      const { last: _stale, ...rest } = leg;
      assessLeg = rest;
    }

    // Snapshot the matching resting orders first: a fill mutates the map mid-iteration, and
    // insertion order must be stable for determinism.
    const matches: RestingState[] = [];
    for (const st of this.#resting.values()) {
      const o = st.order;
      if (o.instrument === instrument && o.strike === leg.strike && o.right === leg.right) {
        matches.push(st);
      }
    }

    for (const st of matches) {
      // 9gu.4 — exit-clock-at-entry-fill / causal sequencing (fail-closed).
      // An order is eligible ONLY against book events at or after its own placement (`lastTs` is
      // seeded to the placement ts). A book event that PREDATES the order — a rewound/stale tape, or,
      // structurally, a TP/exit sell weighed against a print BEFORE its entry fill — can never fill it
      // (selling before you own the contract once reversed a study verdict). In a monotone session
      // this never fires; the guard exists so an out-of-order look cannot manufacture a pre-entry exit.
      if (ts < st.lastTs) continue;
      const dt = Math.max(0, ts - st.lastTs);
      const view: FillOrderView = {
        side: st.order.side,
        qty: st.order.qty,
        px: st.order.px,
        strike: st.order.strike,
        right: st.order.right,
        ...(st.order.moneyness !== undefined ? { moneyness: st.order.moneyness } : {}),
        ...(st.order.covered !== undefined ? { covered: st.order.covered } : {}),
      };
      const res = this.#model.assess(view, assessLeg, fair === undefined ? { dtSinceLast: dt } : { fair, dtSinceLast: dt });
      st.lastTs = ts;

      if (res.fill !== undefined) {
        // Floor fill: definite, at the resting price (strict-cross-FIRST, ADR-0016 §4). It short-
        // circuits the hazard, so the sampled channel can never MISS a fill the floor credited —
        // containment floor ⊆ sampled: a floor lot is marked `sampledFilled` whenever the sampler is
        // armed (every draw ∈ [0,1) satisfies draw < 1). Leaves the book into inventory.
        this.#resting.delete(st.order.ref);
        const floorSampled = this.#sampler !== undefined || st.sampledFilled;
        this.#filled.push({
          order: st.order,
          fillPx: res.fill.px,
          fillTs: ts,
          sampledFilled: floorSampled,
          viaSampled: false,
          support: "calibrated",
          survivalAtFill: st.survival,
        });
        this.#emit("fill", ts, st.order, { px: res.fill.px, reason: res.kind });
        continue;
      }

      // Hazard accrual (no floor fill this look). Decide how many episodes to MINT for this order on
      // this look, then fold each one (ADR-0016 §episode-identity):
      //  - Un-keyed models (the per-second hazard, no `episodeKey`): exactly ONE fold per look, as
      //    before — the hazard already integrates `dtSinceLast`, so it takes no time-based re-mint.
      //  - Keyed per-episode models (`MakerFairCalV1`): one fold on a regime boundary (B1 first look /
      //    B3 dark↔lit flip), PLUS one fold per `DT_REF` window of continuous SAME-regime rest (B4,
      //    kestrel-9gu.13). A same-regime re-look inside a window mints nothing (N1 anti-double-count:
      //    `windows === 0`), so re-observation still never inflates the product.
      if (res.pFill > 0) {
        const keyed = res.episodeKey !== undefined;
        // `catchUp` = the same-regime B4 path (keyed AND the key is unchanged from the last fold).
        // Captured BEFORE the mint loop mutates `lastEpisodeKey` so the saturation guard below never
        // skips a STRUCTURAL boundary mint (B1/B3, which always folds its one episode).
        const catchUp = keyed && res.episodeKey === st.lastEpisodeKey;
        let mints: number;
        if (!keyed) {
          mints = 1; // un-keyed per-second hazard: one per-look fold (byte-identical to before)
        } else if (!catchUp) {
          // B1/B3 regime boundary: one structural mint, and (re)anchor the B4 cadence clock HERE so the
          // window count is measured from the instant this regime began, not from an earlier regime.
          mints = 1;
          st.cadenceAnchorTs = ts;
        } else {
          // B4: the same regime has persisted — mint one episode per COMPLETED `DT_REF` window since the
          // anchor. Keyed on the injected clock, so the count is density-independent; advance the anchor
          // by whole windows so it is chunk-invariant (a dense and a sparse tape over the same rest span
          // mint the same episodes — ADR-0016 §Context: book cadence is a recording artifact).
          const windows = Math.floor((ts - st.cadenceAnchorTs) / EPISODE_REMINT_DT_REF_MS);
          mints = windows;
          st.cadenceAnchorTs += windows * EPISODE_REMINT_DT_REF_MS;
        }

        for (let m = 0; m < mints; m++) {
          // Saturation floor (B4 catch-up only): once expected fill is effectively 1, further windows of
          // the same regime move nothing bankable and a still-open sampled draw would fill regardless —
          // so stop folding (and stop emitting per-episode telemetry) rather than mint unbounded
          // saturated episodes on a long-gap catch-up. The engine survival stays byte-identical to the
          // bus-re-derived one (both stop at this same deterministic threshold, ADR-0011).
          if (catchUp && st.survival < EPISODE_SURVIVAL_SATURATION_EPS) break;

          // Each mint consumes the next episode index and advances it — so successive mints under one
          // ref get unique substreams (0 at placement's first episode B1, then ++ on each subsequent
          // mint: a dark↔lit flip B3 or a DT_REF re-mint B4 for a keyed model; every fold for an
          // un-keyed one). A same-regime re-look inside a window (N1) mints nothing, so it neither
          // advances the index nor consumes a draw (ADR-0016 §1/§3).
          const mintIndex = st.episodeIndex;
          st.episodeIndex = mintIndex + 1;

          st.survival *= 1 - res.pFill;
          // 9gu.3 support: an assessment we actually fold into the survival product taints the order's
          // expected-$ as non-bankable unless it is calibrated. An unstated flag is treated
          // conservatively (not calibrated) — mirrors isCalibratedSupport's fail-closed default.
          if (!isCalibratedSupport(res.support)) st.supportExtrapolated = true;
          if (res.episodeKey !== undefined) st.lastEpisodeKey = res.episodeKey;

          // One draw per episode, consumed at mint off the (runSeed, model.version, ref, mintIndex)
          // substream (ADR-0016 §3). Extrapolated/dark episodes STILL draw (9gu.8 needs realized
          // branches for causal management) but their gains are unbankable (the support flag rides
          // the lot + telemetry, §5).
          let sampledThisMint = false;
          if (this.#sampler !== undefined && !st.sampledFilled) {
            const draw = mulberry32(episodeSeed(this.#sampler.runSeed, this.#model.version, st.order.ref, mintIndex))();
            if (draw < res.pFill) {
              sampledThisMint = true;
              st.sampledFilled = true;
            }
          }

          // Per-episode hazard telemetry — episode-keyed models only (the per-episode judge). The
          // survival product Π(1 − pFill) re-derives from these bus records (ADR-0016 / ADR-0011). An
          // unstated support records as `extrapolated` (fail-closed, non-bankable — isCalibratedSupport
          // treats a missing flag conservatively).
          if (res.episodeKey !== undefined) {
            const epSupport: FillSupport = isCalibratedSupport(res.support) ? "calibrated" : "extrapolated";
            this.#emitHazard(ts, st.order.ref, mintIndex, res.episodeKey, res.pFill, epSupport, sampledThisMint);
          }

          // A realized sampled draw is a CAUSAL fill: emit a real `fill` ORDER event at the resting
          // price and leave the book into a sampled lot (NOT a floor fill — floorFilled/floorPnl stay
          // 0). Fed back into Plan management downstream (kestrel-9gu.8.3). Removing the order ends the
          // mint loop for it this look.
          if (sampledThisMint) {
            this.#resting.delete(st.order.ref);
            this.#filled.push({
              order: st.order,
              fillPx: st.order.px,
              fillTs: ts,
              sampledFilled: true,
              viaSampled: true,
              support: isCalibratedSupport(res.support) ? "calibrated" : "extrapolated",
              survivalAtFill: st.survival,
            });
            this.#emit("fill", ts, st.order, { px: st.order.px, reason: "sampled" });
            break;
          }
        }
      }
    }

    return this.#events.slice(before);
  }

  /**
   * Cash-settle at the close (RUNTIME §6). Filled inventory settles at **intrinsic** from the
   * settle spot; every still-resting order **expires worthless-unfilled** (no position, $0
   * floor) and emits a `cancel` event annotated `expired-unfilled`. Idempotent: a second
   * settle finds an empty book and reports only the (already-final) filled inventory.
   *
   * The report carries both the floor outcome and the E[$] under `pFill`, plus the sampled
   * total when a seeded sampler was supplied. Determinism: outcomes are emitted filled-first
   * (fill order), then resting (placement order).
   *
   * **Carry-mark (the multi-session horizon design, kestrel-5zl.16.3).** With `{ carry: true }` — a NON-final session of a
   * multi-session Instance — held inventory is marked to the session-close spot and RECORDED to
   * {@link carriedInventory} with its basis re-baselined to that mark, so the position survives to the
   * next session. The realized P&L, the emitted ORDER/TELEMETRY bytes, and the returned report are
   * **byte-identical** to a plain intrinsic settle (mark == `intrinsic(settleSpot, …)`, the same
   * per-session mark-to-market); the ONLY difference is the additional (bus-silent) carried-lot
   * hand-off. The default / final settle (`carry` absent or `false`) is today's behaviour exactly — the
   * position dies at intrinsic — so a length-1 sequence is byte-for-byte unchanged.
   *
   * **`opts.mark` is the settle spot's source provenance (kestrel-xwf)**, threaded by the session
   * driver. FAIL-CLOSED: omitted/unstated provenance grades STALE (never a silently fresh-looking
   * mark), as does an as-of older than the declared threshold — the verdict rides the report's
   * `settleMark` AND the bus (a `settle_mark` TELEMETRY record) so the Blotter re-derives the
   * taint from bytes. A STALE mark applies `opts.mark.parity` — the session's closing put-call-parity
   * candidate — when present (`source: "parity"`, an ANNOTATED recovery: the book cash-settles
   * against the recovered spot, the staleness verdict unchanged) and is otherwise retained
   * `markUncertain` (`source: "stale"` — honestly refused, never fabricated).
   */
  settle(
    settleSpot: number,
    ts: number,
    opts?: { readonly carry?: boolean; readonly mark?: SettleMarkProvenance },
  ): SettleReport {
    const carry = opts?.carry ?? false;
    const mark = opts?.mark;

    // Settle-mark provenance + fail-closed staleness verdict (kestrel-xwf), resolved FIRST because the
    // book cash-settles against the RESOLVED mark: the mark's as-of vs the settle instant, under the
    // declared threshold. Unstated provenance is STALE, never fresh-by-default. A STALE mark applies
    // the session's closing put-call-parity candidate when one was derivable (`source: "parity"` — an
    // ANNOTATED recovery, the staleness verdict unchanged) and is otherwise retained mark-uncertain
    // (`source: "stale"` — honestly refused, never fabricated).
    const asOfTs = mark?.asOfTs ?? null;
    const ageMs = asOfTs === null ? null : Math.max(0, ts - asOfTs);
    const stale = ageMs === null || ageMs > this.#staleMarkAfterMs;
    const parity = stale ? (mark?.parity ?? null) : null;
    const resolvedSpot = parity !== null ? parity.spot : settleSpot;
    const source: SettleMarkResolution = !stale ? "market" : parity !== null ? "parity" : "stale";
    const staleness =
      ageMs === null
        ? "provenance unstated (no spot observation on the tape)"
        : `value last established ${ageMs}ms before settle (> ${this.#staleMarkAfterMs}ms threshold)`;
    const note = !stale
      ? undefined
      : parity !== null
        ? `settle mark ${settleSpot} is stale: ${staleness}; recovered via closing put-call parity at strike ${parity.strike} ⇒ ${parity.spot}`
        : `settle mark ${settleSpot} is stale: ${staleness}; no closing put-call parity derivable — mark-uncertain`;
    const settleMark: SettleMark = {
      px: resolvedSpot,
      asOfTs,
      lastObservedTs: mark?.lastObservedTs ?? null,
      ageMs,
      staleAfterMs: this.#staleMarkAfterMs,
      stale,
      source,
      markUncertain: stale && parity === null,
      ...(note !== undefined ? { note } : {}),
    };

    const outcomes: OrderOutcome[] = [];
    let floorTotal = 0;
    let expectedTotal = 0;
    let sampledTotal = 0;

    // Filled inventory — settle at intrinsic. A strict-cross FLOOR lot is a structural price-priority
    // fact: `floorFilled`, expected-fill-prob pinned to `1`, `calibrated`. A `viaSampled` hazard-draw
    // lot is NOT a floor fill (floorFilled/floorPnl stay 0) — it contributes only to the sampled
    // channel; its expected channel is the accrued analytic `1 − survivalAtFill` on its own support
    // (extrapolated ⇒ unbankable, ADR-0016 §5).
    for (const lot of this.#filled) {
      const floorFilled = !lot.viaSampled;
      const expectedProb = lot.viaSampled ? 1 - lot.survivalAtFill : 1;
      const oc = this.#outcome(
        lot.order,
        resolvedSpot,
        floorFilled,
        floorFilled ? lot.fillPx : null,
        expectedProb,
        lot.support,
        lot.sampledFilled,
      );
      outcomes.push(oc);
      floorTotal += oc.floorPnl;
      expectedTotal += oc.expectedPnl;
      if (oc.sampled !== undefined) sampledTotal += oc.sampled.pnl;
      // Carry-mark: the lot survives to the next session at its mark-to-close basis (the re-baseline
      // that lands the overnight gap in the NEXT session's P&L, not this one). Bus-silent — no event.
      if (carry) this.#carried.push({ order: { ...lot.order, px: oc.intrinsicAtSettle }, basis: oc.intrinsicAtSettle });
    }

    // Still-resting — expire worthless-unfilled ($0 floor), but carry accrued E[$] + its support flag.
    for (const st of this.#resting.values()) {
      this.#emit("cancel", ts, st.order, { reason: "expired-unfilled" });
      const support: FillSupport = st.supportExtrapolated ? "extrapolated" : "calibrated";
      const oc = this.#outcome(st.order, resolvedSpot, false, null, 1 - st.survival, support, st.sampledFilled);
      outcomes.push(oc);
      floorTotal += oc.floorPnl;
      expectedTotal += oc.expectedPnl;
      if (oc.sampled !== undefined) sampledTotal += oc.sampled.pnl;
    }
    this.#resting.clear();
    this.#settled = true;

    // Record the probabilistic accounting ON THE BUS (a57.1 slice 1, ADR-0011): one settle-outcome
    // TELEMETRY record per order, carrying the accrued expected-fill-prob, the 9gu.3 support flag, and
    // the unrounded floor/expected P&L — so the Blotter projector re-derives totals{floor,expected} +
    // orders[].support from bus bytes alone, with no engine-state scrape. Emitted AFTER the expired-
    // unfilled cancels above so every ORDER event keeps its exact seq/bytes (only these additive
    // TELEMETRY lines are new), in outcomes order (filled-first, then resting) for determinism.
    for (const oc of outcomes) this.#emitSettleTelemetry(oc, ts);

    // Commit the provenance ON THE BUS (ADR-0010: the Bus is the truth) — one settle_mark TELEMETRY
    // record per session, after the per-order settle records, so the projector folds the taint from
    // bus bytes alone. Emitted ONLY when the session settled at least one order: with no outcomes
    // there is no P&L to qualify, and a zero-documents run must stay a byte-identical pass-through
    // (RUNTIME §7 certification). Absent keys are OMITTED (byte-minimal), mirroring the payload.
    if (outcomes.length > 0) {
      this.#events.push({
        ts,
        stream: "TELEMETRY",
        type: "settle_mark",
        px: settleMark.px,
        ...(settleMark.asOfTs !== null ? { as_of_ts: settleMark.asOfTs } : {}),
        ...(settleMark.lastObservedTs !== null ? { last_observed_ts: settleMark.lastObservedTs } : {}),
        ...(settleMark.ageMs !== null ? { age_ms: settleMark.ageMs } : {}),
        stale_after_ms: settleMark.staleAfterMs,
        stale: settleMark.stale,
        source: settleMark.source,
        mark_uncertain: settleMark.markUncertain,
        ...(settleMark.note !== undefined ? { note: settleMark.note } : {}),
      });
    }

    const cal = this.#model.calibration;
    return {
      settleSpot: resolvedSpot,
      settleMark,
      fillModel: `${this.#model.name}/${this.#model.version}`,
      ...(cal !== undefined ? { hazardVersion: cal.version, calibrated: cal.calibrated } : {}),
      multiplier: this.#multiplier,
      outcomes,
      floorTotal,
      expectedTotal,
      ...(this.#sampler !== undefined ? { sampledTotal } : {}),
    };
  }

  // ── internals ──────────────────────────────────────────────────────────────

  /** Build one order's settle outcome. `scaled` is the settle P&L of a *filled* position:
   * BUY earns `intrinsic − px`, SELL earns `px − intrinsic`, times qty × multiplier. */
  #outcome(
    order: FillOrder,
    settleSpot: number,
    floorFilled: boolean,
    floorFillPx: number | null,
    expectedFillProb: number,
    support: FillSupport,
    sampledFilled: boolean,
  ): OrderOutcome {
    const intr = intrinsic(settleSpot, order.strike, order.right); // fair's canonical (spot,strike,right)
    const perContract = order.side === "buy" ? intr - order.px : order.px - intr;
    const scaled = perContract * order.qty * this.#multiplier;
    return {
      ref: order.ref,
      ...(order.plan !== undefined ? { plan: order.plan } : {}),
      ...(order.plan_instance !== undefined ? { plan_instance: order.plan_instance } : {}),
      instrument: order.instrument,
      side: order.side,
      qty: order.qty,
      px: order.px,
      strike: order.strike,
      right: order.right,
      floorFilled,
      floorFillPx,
      expectedFillProb,
      support,
      intrinsicAtSettle: intr,
      floorPnl: floorFilled ? scaled : 0,
      expectedPnl: expectedFillProb * scaled,
      ...(this.#sampler !== undefined ? { sampled: { filled: sampledFilled, pnl: sampledFilled ? scaled : 0 } } : {}),
    };
  }

  /** Emit one per-episode hazard TELEMETRY record (kestrel-9gu.8.2, ADR-0016) at an episode mint —
   * the folded per-episode `pFill` + support + (on a seeded run) whether its draw realized, so the
   * survival product `Π(1 − pFill)` and the sampled path re-derive from bus bytes (ADR-0011).
   * Observational: the engine never reads it back (determinism stable on replay). `sampled` is
   * omitted entirely when the sampled channel is off (no draw was taken). */
  #emitHazard(
    ts: number,
    orderId: string,
    episodeIndex: number,
    episodeKey: string,
    pFill: number,
    support: FillSupport,
    sampled: boolean,
  ): void {
    this.#events.push({
      ts,
      stream: "TELEMETRY",
      type: "hazard",
      order_id: orderId,
      episode_index: episodeIndex,
      episode_key: episodeKey,
      p_fill: pFill,
      support,
      ...(this.#sampler !== undefined ? { sampled } : {}),
    });
  }

  /** Emit one settle-outcome TELEMETRY record (a57.1 slice 1) onto the cumulative log — the accrued
   * probabilistic accounting the projector re-derives totals + support from. Observational: the
   * engine never reads it back, so it moves no order/fill/plan decision (determinism stable). The
   * unrounded `floor_pnl`/`expected_pnl` sum-then-round back to the report's realized/expected totals;
   * on a SEEDED run the sampled channel rides too (`sampled_filled`/`sampled_pnl`, kestrel-9gu.12) so
   * all three channels re-derive from bus bytes — an unseeded record is byte-identical to before. */
  #emitSettleTelemetry(oc: OrderOutcome, ts: number): void {
    this.#events.push({
      ts,
      stream: "TELEMETRY",
      type: "settle",
      order_id: oc.ref,
      expected_fill_prob: oc.expectedFillProb,
      support: oc.support,
      floor_filled: oc.floorFilled,
      floor_pnl: oc.floorPnl,
      expected_pnl: oc.expectedPnl,
      ...(oc.sampled !== undefined ? { sampled_filled: oc.sampled.filled, sampled_pnl: oc.sampled.pnl } : {}),
    });
  }

  /** Append a typed ORDER event to the cumulative log. */
  #emit(action: OrderAction, ts: number, order: FillOrder, extra: Partial<OrderPayload>): void {
    const payload: OrderPayload = {
      order_id: order.ref,
      ...(order.plan !== undefined ? { plan: order.plan } : {}),
      ...(order.plan_instance !== undefined ? { plan_instance: order.plan_instance } : {}),
      instrument: order.instrument,
      side: order.side,
      qty: order.qty,
      strike: order.strike,
      right: order.right,
      ...extra,
    };
    this.#events.push({ ts, stream: "ORDER", type: action, ...payload });
  }
}
