/**
 * # adapters/broker/ibkr — the IB Gateway PAPER ORDER FACE (kestrel-7o2.8)
 *
 * The first Kestrel module allowed to name `placeOrder`, and it may do so ONLY behind the 7o2.9
 * safety envelope. It is a {@link BrokerAdapter} — therefore a {@link Gate} — so it drops into the
 * ONE execution seam the engine already fires through (`engine/plans.ts`), and `sim | paper | live`
 * stays one Session path where only the gate differs.
 *
 * ## A TRANSMITTER, never a re-pricer
 * The {@link OrderIntent} arrives FULLY RESOLVED: a numeric `px` the engine already earned (with its
 * `sourceAnnotation` receipt), never-naked-checked, directional-guard evidence attached. This adapter
 * therefore **never touches `intent.px`** — it does not round it, does not snap it "to improve", does
 * not substitute a mid (a mid is a HEALTH SIGNAL, never a price and never a value, RUNTIME §4). It
 * transmits that exact number as an IB `LMT` limit price, or it REFUSES. There is no third path.
 *
 * ## PAPER ONLY. `live` is refused at construction.
 * Live routing is kestrel-7o2.10 and lands only behind the human-signed {@link LiveArm}. A `live`
 * config here is a typed {@link IbkrOrderRefused} `mode-gate` — fail-closed, never a port fact.
 *
 * ## The walls, ALL of them BEFORE the wire
 * `submit` runs a guard chain and only then calls `placeOrder`. Any wall ⇒ a typed throw and
 * **`placeOrder` is never called** (the {@link IbOrderClient} spy in the tests is the witness):
 *
 *  0. **KILL-SWITCH** (7o2.9) — tripped (operator STAND_DOWN, or a reconciliation break) ⇒
 *     {@link ClampRefused} `killed`. Consulted first so a halted process refuses uniformly. On the
 *     `makeGate("paper")` path this is the SAME switch the seam's L0 clamp consults (makeGate threads
 *     it in): exactly ONE instance, so a trip in EITHER layer halts the whole path — never two
 *     independent switches that cannot reach each other.
 *  1. **THE INTENT ITSELF** — `qty` a positive whole number, `px` a positive finite price, the `ref`
 *     not already on the ledger. IB is never this system's validator: a SELL of `qty: -3` would
 *     sign-invert its way through the never-naked wall and put `totalQuantity: -3` on the wire.
 *  2. **CONTRACT** — the leg must already be resolved to the gateway's OWN definition (7o2.6). An
 *     unresolved leg is refused; an identity is NEVER guessed on the hot path.
 *  3. **NEVER-NAKED** — the projected position may never go NEGATIVE. It is measured against the
 *     **BROKER's own reported truth MINUS the quantity our WORKING (unfilled) SELLs have already
 *     promised away** — the worst case, once every order in flight has filled. A long option (max loss
 *     = premium) and a long equity are defined risk; an uncovered short is unbounded risk, and no
 *     budget makes it acceptable, so this is a doctrine refusal rather than a ceiling.
 *
 *     A RESTING sell does not move broker truth until it fills. Reading truth alone therefore let a
 *     SECOND sell of the same long clear this wall — and when both filled, this module opened an
 *     uncovered short (kestrel-7o2.8, blocker 1). Hence the RESERVATION: a working sell holds its
 *     quantity against the key from SUBMIT until a terminal outcome releases it.
 *  4. **INTRINSIC FLOOR** — a SELL is floored at intrinsic. Below it ⇒ refused. And because the
 *     adapter may not re-price, an UNKNOWN intrinsic also refuses (the floor is never *assumed*
 *     satisfied — that would be a silent default, and this runtime has none).
 *  5. **L0 CLAMP** (7o2.9 {@link RiskLimits}) — `maxOrderQty`, `maxPositionQty`, `maxNotionalUsd`
 *     (`notional = px x qty x the VENUE's multiplier`). Over ANY ceiling ⇒ {@link ClampRefused}, and
 *     NOTHING is transmitted. This is ONE of TWO INDEPENDENTLY-PROVEN ceilings, defense-in-depth
 *     (ADR-0034 §4) — NOT a subordinate backstop to a single canonical owner. The OTHER is the seam's
 *     `makeLiveClamp` ABOVE the adapter, which on the `makeGate("paper")` path wraps this face, SHARES
 *     its ONE kill-switch, and enforces the SAME limits + the SAME true per-contract multiplier (all
 *     threaded in via {@link IbkrBroker.limits} / {@link IbkrBroker.multiplierOf}). Each layer is proven
 *     load-bearing on its own: this WALL 5 is the ONLY ceiling when the face is reached BARE (unit tests,
 *     the env-gated dry-run/equity-order proof scripts — where no seam wraps it, and fail-closed forbids
 *     a bare venue face with no ceiling), and a mutation test gutting it goes RED; the seam is the only
 *     ceiling over a WALL-5-less adapter, and a mutation gutting the seam goes RED too. Same numbers,
 *     never divergent — two barriers, not one owner with a shadow.
 *  6. **PRICE ANCHOR** ({@link PriceAnchor}) — the walls above bound SIZE; this one bounds the PRICE.
 *     The order's `@fair` must lie INSIDE the venue's OBSERVED two-sided book, and that book must be
 *     LIVE: delayed/frozen data is a HEALTH SIGNAL, never a price anchor. A fair above the ask (or
 *     below the bid) means the ANCHOR ITSELF is lying, and no size ceiling can rescue a bad price — so
 *     no authorizable ticket is produced at all. (Live, a ticket printed CLEARED at `@fair = 0.9229`
 *     against an observed offer of `0.73`: "a BUY never bids above fair" did zero work, because fair
 *     was above the offer. `@fair`'s own derivation is the engine's — kestrel-ku99 — but a consumer
 *     that cannot corroborate its anchor must fail closed.)
 *
 * {@link IbkrBroker.preflight} runs walls 0–6 and returns the resolved {@link OrderTicket} **without
 * transmitting anything** — that is what the dry-run ticket prints, and it is why a ticket can be
 * reviewed by a human before a single byte reaches the venue.
 *
 * ## The inbound pump — IB callbacks → the EXISTING closed ORDER vocabulary
 * The engine already folds `ORDER place | fill | cancel | reject` (bus/types.ts, a CLOSED
 * {@link OrderAction} set). No new event kind is invented:
 *
 *  - `openOrder` / a live `orderStatus` → **`place`** (the venue's acknowledgment), emitted at most
 *    ONCE per ref. IB duplicates `orderStatus` routinely, so every mapping here is idempotent.
 *  - `execDetails` → **`fill`**, one per NEW `execId`. This — not `orderStatus.filled` — is the fill
 *    source of truth: `orderStatus` reports a CUMULATIVE count and repeats itself, while each partial
 *    fill has its own `execId`. So PARTIAL and MULTI-fill both fall out for free, each at the price
 *    the venue actually printed, and a replayed callback can never double-count.
 *  - `orderStatus` `Cancelled`/`ApiCancelled` → **`cancel`**; `Inactive`, or an order-scoped fatal IB
 *    `error`, → **`reject`**. Both terminal, both latched.
 *  - `commissionReport` → the LEDGER only. It is not an ORDER event: the vocabulary is closed, and a
 *    commission is not an order action.
 *
 * Causal order is preserved (a `fill` that somehow beats its acknowledgment still emits `place`
 * first), so a replay of the same callback sequence yields a byte-identical event stream.
 *
 * ## The reconciliation ledger — the BROKER's report is authoritative, and the trip fires AT OBSERVATION
 * {@link IbkrBroker.ledger} holds per-order state. {@link IbkrBroker.positions} is the venue's own
 * signed net position — its `position` push as a BASELINE, plus the executions observed SINCE it —
 * never the engine's expectation. The push is SUBSCRIBED at construction (`reqPositions`): IB does not
 * emit `position` unsolicited, and without the subscription "broker truth" is really just the engine's
 * own mirror wearing that label.
 *
 * The break TRIPS THE {@link KillSwitch} FROM INSIDE THE `execDetails` HANDLER — the moment the venue's
 * report proves an over-fill, a post-terminal fill, or ANY key gone negative. **An invariant is only
 * real if something calls it**: the shipped version could only trip via {@link IbkrBroker.reconcile},
 * which nothing in production ever called, so an over-fill sat at −3 with the switch un-tripped and the
 * next submit still accepted (blocker 3). `reconcile()` survives as an explicit AUDIT — it compares the
 * engine/Blotter EXPECTED snapshot against broker truth — but it is no longer the only thing that can
 * trip. Never a silent divergence, and the engine never "corrects" the broker.
 *
 * ## Determinism at the edge
 * The IB client and the order-id minter are INJECTED, so the unit tests drive an in-memory double
 * with no socket, no timer, and no real order. Nothing here reads a wall clock: ORDER events are
 * stamped with the `now` the driver pins on the gate (RUNTIME §0), exactly as `SimGate` is.
 */

import { EventName, OrderType, TimeInForce, OrderStatus, OrderAction as IbOrderAction, SecType, OptionType, isNonFatalError } from "@stoqey/ib";
import type { Contract, Order, Execution, CommissionReport, OrderState } from "@stoqey/ib";

import type { BrokerAdapter, KillSwitch, PositionKey, PositionSnapshot, RiskLimits } from "../../broker.ts";
import { ClampRefused, makeKillSwitch, positionKeyOf, registerPaperVenue } from "../../broker.ts";
import type { OrderIntent } from "../../../engine/index.ts";
import type { NewBusEvent, OrderAction, Right } from "../../../bus/index.ts";
import type { IbkrConfig } from "./config.ts";
import { describeIbkrConfig } from "./config.ts";
import type { IbkrContract } from "./contract.ts";
import type { IbkrTransport } from "./transport.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Typed, fail-closed refusals.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Why the paper order face REFUSED to transmit (kestrel-7o2.8) — a closed, typed vocabulary. Each of
 * these means the SAME thing about the wire: **nothing was sent**.
 *
 * `naked-short` / `below-intrinsic` / `intrinsic-unknown` are the RISK DOCTRINE walls (never-naked;
 * a SELL floored at intrinsic; a floor never *assumed* satisfied). They are DISTINCT from the 7o2.9
 * {@link ClampRefused} (a bounded-risk CEILING) because they are not ceilings at all — no budget
 * makes an uncovered short acceptable.
 */
export type IbkrOrderFailure =
  /** A `live` config reached the paper face. Live routing is 7o2.10, behind the human-signed arm. */
  | "mode-gate"
  /** The intent itself is MALFORMED (qty ≤ 0 / fractional / non-finite; px ≤ 0 / non-finite; a ref
   * already on the ledger; a re-issued IB order id that would re-point correlation). Refused at wall 1
   * — IB is NEVER this system's validator, and a SELL of `qty: -3` must not be allowed to sign-invert
   * its way through the risk wall. */
  | "invalid-order"
  /** The leg has no pre-resolved gateway contract — an identity is never guessed on the hot path. */
  | "unresolved-contract"
  /** The resulting position has an UNBOUNDED (non-computable / infinite) max_loss — a naked short CALL
   * or a short EQUITY. Refused by the BOUNDED-RISK platform INVARIANT itself, REGARDLESS of policy
   * (kestrel-buos): no budget can satisfy `size × max_loss ≤ budget` when max_loss is infinite. */
  | "unbounded-risk"
  /** The resulting position's max_loss is FINITE but exceeds the risk budget (`size × max_loss >
   * budget`). Bounded risk is the invariant; a budget that cannot cover the worst case refuses. */
  | "over-budget"
  /** The order would open (or deepen) an UNCOVERED SHORT whose max_loss is finite and budgeted (a
   * cash-secured / naked PUT) while the no-uncovered-short POLICY is ON (the default — the 0DTE book
   * keeps it hard). An OVERRIDABLE policy, not the platform invariant (kestrel-buos): turn it off to
   * sell puts. The UNBOUNDED cases above are refused regardless of it. */
  | "naked-short"
  /** A SELL priced BELOW the leg's intrinsic value. */
  | "below-intrinsic"
  /** A SELL whose intrinsic is UNKNOWN — the floor cannot be proven, so it fails closed. Applies to
   * OPTIONS only: an equity has no optionality and its floor is 0 (kestrel-buos §e). */
  | "intrinsic-unknown"
  /** No OBSERVED two-sided book for the leg (unquoted, dark, one-sided, crossed), or no `@fair` at
   * all. A price we cannot audit against the venue's own book is not authorizable. */
  | "anchor-unresolvable"
  /** The book on offer is DELAYED/FROZEN. That is a HEALTH SIGNAL, never a PRICE ANCHOR (7o2.8). */
  | "anchor-stale"
  /** The `@fair` RECEIPT cannot vouch for the price — a stale index, frozen input, or no ATM coverage
   * (kestrel-ltrf). `@fair` is UNDERLYING-anchored, not book-anchored, so it may LEGITIMATELY sit
   * away from a lagging/wide index-option quote; the wall gates on an UNTRUSTED receipt, NOT on a naive
   * fair-outside-[bid,ask] clamp (which defeats the whole point of a fresh-index fair). */
  | "fair-untrusted"
  /** The venue's `placeOrder` threw — surfaced LOUD, never swallowed. */
  | "transmit-failed";

/**
 * The paper order face refused to transmit (kestrel-7o2.8) — a LOUD, typed, fail-closed refusal that
 * routes to STAND_DOWN. A DISTINCT class (not a bare `Error`, not {@link ClampRefused}) so a caller
 * can catch a DOCTRINE refusal specifically and can never mistake it for a bounded-risk ceiling.
 * NEVER carries a credential/account (the config is described through the redacting formatter).
 */
export class IbkrOrderRefused extends Error {
  override readonly name = "IbkrOrderRefused";
  readonly failure: IbkrOrderFailure;
  /** The refused order's engine ref, when the refusal belongs to one. */
  readonly ref: string | undefined;
  constructor(
    failure: IbkrOrderFailure,
    reason: string,
    options: { ref?: string | undefined; cause?: unknown } = {},
  ) {
    super(
      `IBKR paper order REFUSED [${failure}]: ${reason} (fail-closed; NOTHING transmitted; STAND_DOWN)`,
      options.cause === undefined ? undefined : { cause: options.cause },
    );
    this.failure = failure;
    this.ref = options.ref;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Injected seams.
// ─────────────────────────────────────────────────────────────────────────────

/** A listener over the IB event bus (the transport's convention — concrete closures are wrapped at
 * the registration site, so the `never[]` never escapes into a handler body). */
type IbListener = (...args: never[]) => void;

/**
 * The NARROW ORDER surface of the shared IB client. This is the ONE place in the package that names
 * `placeOrder`, and it exists only behind the guard chain above.
 *
 * Note the deliberate asymmetry with 7o2.5/7o2.6: the transport's {@link import("./transport.ts").IbClient}
 * and the contract layer's `IbContractClient` EXCLUDE every order method, so neither of them can
 * transmit even by reaching through the client. THIS view widens toward exactly two order calls (plus
 * the read-only position pull), and it is reached only via {@link orderClientOf} — a single, named,
 * auditable door. A test injects a double implementing exactly this; production passes the
 * transport's ONE shared client, so the feed face and the order face are two faces of ONE socket,
 * never a second connection.
 */
export interface IbOrderClient {
  /** Transmit an order. The `id` is the API client's order id (see {@link IbkrBrokerDeps.nextOrderId}). */
  placeOrder(id: number, contract: Contract, order: Order): unknown;
  /** Pull a resting order by its IB order id. */
  cancelOrder(orderId: number): unknown;
  /** Solicit the next valid order id (`nextValidId`) — the id sequence's origin. */
  reqIds(numIds?: number): unknown;
  /** Subscribe to the account's AUTHORITATIVE position push (`position` / `positionEnd`). READ-ONLY. */
  reqPositions(): unknown;
  on(event: EventName, listener: IbListener): unknown;
  removeListener(event: EventName, listener: IbListener): unknown;
}

/**
 * The ONE shared IB session, viewed through the ORDER surface (kestrel-7o2.8). The transport hands
 * out its single guarded client — throwing the typed connection error if the session is not connected
 * or has gone degraded, so an order can never be transmitted over a dead socket. This re-views it as
 * an {@link IbOrderClient}. The cast is the SINGLE, NAMED door through which order authority reaches
 * the socket; the real `IBApi` is a structural superset. There is never a second socket.
 */
export function orderClientOf(transport: IbkrTransport): IbOrderClient {
  return transport.client() as unknown as IbOrderClient;
}

/**
 * The pre-resolved contract book — a SYNCHRONOUS leg → contract lookup.
 *
 * Why it exists: the {@link Gate} seam is synchronous (`submit(intent): string`), but the 7o2.6
 * resolution is a network round-trip. Resolving on the hot path would put a socket between the tick
 * and the order — so contracts are resolved AHEAD of it (at arm time) and looked up here. That is the
 * fire-then-inform discipline applied to identity: nothing slow, and nothing guessed, at the tick.
 * A leg that is not in the book is REFUSED (`unresolved-contract`), never resolved by hand.
 */
export interface ContractBook {
  get(leg: { readonly instrument: string; readonly strike?: number; readonly right?: Right }): IbkrContract | undefined;
}

/** Build a {@link ContractBook} from contracts already resolved through 7o2.6's `resolveContract`.
 * Keyed by {@link positionKeyOf}, so the book, the never-naked wall, the L0 position ceiling and the
 * reconciliation ledger all key IDENTICALLY (no skew). */
export function contractBook(contracts: readonly IbkrContract[]): ContractBook {
  const byKey = new Map<PositionKey, IbkrContract>();
  for (const c of contracts) {
    byKey.set(
      c.kind === "option"
        ? positionKeyOf({ instrument: c.symbol, strike: c.strike, right: c.right })
        : positionKeyOf({ instrument: c.symbol }),
      c,
    );
  }
  return { get: (leg) => byKey.get(positionKeyOf(leg)) };
}

// ─────────────────────────────────────────────────────────────────────────────
// The PRICE ANCHOR (kestrel-7o2.8, the blocker-4 wall).
// ─────────────────────────────────────────────────────────────────────────────

/**
 * How the VENUE said it sourced a quote (IB's `marketDataType`: 1 live · 2 frozen · 3 delayed ·
 * 4 delayed-frozen). **Only `live` may ANCHOR a price.** Delayed/frozen data is an honest HEALTH
 * SIGNAL — "is this thing tradeable?" — and it is never a price and never a value (RUNTIME §4). A
 * quote whose provenance we cannot establish is `unknown`, which is likewise refused: fail-closed.
 */
export type QuoteFreshness = "live" | "frozen" | "delayed" | "delayed-frozen" | "unknown";

/**
 * The PRICE ANCHOR for one leg (kestrel-7o2.8): the venue's OBSERVED two-sided book, and the `@fair`
 * the engine anchored `intent.px` on — the two numbers whose disagreement the live proof exposed.
 *
 * The 2026-07-14 dry run printed a ticket carrying `@fair = 0.9229` against an observed offer of
 * `0.73` — 28% ABOVE the ask on a liquid ATM 0DTE leg — and every wall still printed CLEARED, because
 * the walls bound SIZE, not PRICE. "A BUY never bids above fair" did zero work because FAIR ITSELF was
 * above the offer. So the book is no longer merely printed beside the price: it BOUNDS it, at the one
 * seam every order path crosses ({@link IbkrBroker.preflight}).
 */
export interface PriceAnchor {
  /** The venue's observed BEST BID. */
  readonly bid: number;
  /** The venue's observed BEST OFFER. */
  readonly ask: number;
  /** The `@fair` the ENGINE resolved for this leg — the anchor `intent.px` was derived from. It is
   * UNDERLYING-anchored (the fresh index spot), NOT book-anchored: for a lagging/wide index-option
   * quote it may LEGITIMATELY sit away from the posted `[bid, ask]`, so it is NOT clamped to the book
   * (kestrel-ltrf). Non-finite ⇒ UNKNOWN ⇒ refused (never assumed — a silent default is never a
   * default here). */
  readonly fair: number;
  /** Whether the `@fair` RECEIPT vouches for the price (kestrel-ltrf). The ENGINE owns `@fair`'s
   * derivation and hands the adapter its verdict: `true` iff the receipt is live-anchored, ATM-covered,
   * and not stale/tainted. `false` ⇒ the anchor is UNTRUSTED ⇒ refused (`fair-untrusted`). This is
   * what REPLACES the round-1 naive fair-outside-[bid,ask] clamp — the wall gates on trust, not on
   * where fair happens to sit relative to a book that legitimately lags a fast index. */
  readonly fairTrusted: boolean;
  /** The provenance of `bid`/`ask`. Anything but `live` refuses: a price anchor never accepts
   * delayed/frozen input. */
  readonly freshness: QuoteFreshness;
}

/** Construction deps for {@link ibkrBroker}. Every non-determinism source (the socket, the order-id
 * minter) is injected, so the unit tests run with none of them. */
export interface IbkrBrokerDeps {
  /** The shared IB client's ORDER view — the transport's ONE session in production ({@link orderClientOf}). */
  readonly client: IbOrderClient;
  /** Contracts resolved AHEAD of the hot path (7o2.6). A leg absent from it is refused. */
  readonly contracts: ContractBook;
  /** Surface this adapter's fresh ORDER events onto the emitted bus — the SimGate `drain()` analogue.
   * Called after every `submit`/`cancel` AND after every inbound pump batch. */
  readonly drain: () => void;
  /** The L0 pre-transmit ceilings (7o2.9). Over ANY of them ⇒ {@link ClampRefused}, nothing transmitted. */
  readonly limits: RiskLimits;
  /**
   * The BOUNDED-RISK budget in dollars (kestrel-buos): a risk-INCREASING order is refused unless
   * `size × max_loss ≤ budget`. Absent ⇒ {@link RiskLimits.maxNotionalUsd} (a long's max_loss IS its
   * notional, so the two coincide there; the budget is what bounds a naked PUT, whose max_loss is
   * `(strike − premium) × multiplier` rather than the notional). Bounded risk is the PLATFORM
   * INVARIANT — an UNBOUNDED max_loss is refused at any budget.
   */
  readonly budgetUsd?: number;
  /**
   * The no-uncovered-short POLICY (kestrel-buos) — an OVERRIDABLE default, ON when absent. When ON (the
   * 0DTE prop book keeps it hard) an order that would open UNCOVERED short exposure is refused even
   * when its max_loss is finite and budgeted (i.e. the naked PUT is refused). When OFF, a budgeted
   * naked put is ALLOWED. The UNBOUNDED cases (naked call / short equity) are refused REGARDLESS — they
   * fail the platform invariant, not merely the policy. A default is not an invariant.
   */
  readonly noUncoveredShort?: boolean;
  /** The kill-switch to consult/trip. Absent ⇒ a fresh one ({@link IbkrBroker.killSwitch}). Share it
   * so an operator halt, a transport degradation, and a reconciliation break hit the SAME switch. */
  readonly killSwitch?: KillSwitch;
  /**
   * The leg's per-contract INTRINSIC value in dollars-per-point (the engine computes it from
   * canonical state — this adapter never derives a price). `undefined` ⇒ UNKNOWN, and a SELL then
   * fails closed rather than assuming its floor is satisfied.
   */
  readonly intrinsicOf: (leg: {
    readonly instrument: string;
    readonly strike?: number;
    readonly right?: Right;
  }) => number | undefined;
  /**
   * The leg's PRICE ANCHOR: the venue's OBSERVED two-sided book plus the `@fair` the engine priced
   * against (kestrel-7o2.8). REQUIRED — every order path crosses this wall, and there is no opt-out:
   * `undefined` (no observed book) REFUSES the order rather than transmitting against a price nothing
   * corroborates. Delayed/frozen input refuses too; a fair outside the book refuses outright.
   */
  readonly priceAnchorOf: (leg: {
    readonly instrument: string;
    readonly strike?: number;
    readonly right?: Right;
  }) => PriceAnchor | undefined;
  /** The engine/Blotter EXPECTED signed net positions — the side {@link IbkrBroker.reconcile} compares
   * AGAINST the broker's authoritative truth. Never used as a risk input (broker truth is). */
  readonly expectedPositions: () => PositionSnapshot;
  /**
   * The SETTLEMENT ORACLE (kestrel-7o2.24, ADR-0034 q3): the VENUE's own statement that this key's
   * expired position was cash-settled — position removed, cash posted. It is the ONLY thing that can
   * absolve a position's disappearance in {@link IbkrBroker.reconcile}, and it is supplied by the
   * VENUE composition (which reads the account's cash/expiry reports), never by this face.
   *
   * ABSENT ⇒ NOTHING IS EVER ABSOLVED — every disappearance trips, exactly as it did before this dep
   * existed. That is the fail-closed default and the reason the carve-out cannot leak into a caller
   * that did not ask for it.
   */
  readonly settlementOf?: (key: PositionKey) => SettlementReceipt | undefined;
  /** Mint the next IB API order id. A COUNTER seeded from the gateway's `nextValidId` — never an RNG,
   * even here at the edge. */
  readonly nextOrderId: () => number;
  /** The venue's positions as ALREADY KNOWN at construction (a resumed session's opening book). The
   * broker's `position` push and its own executions fold on top. Absent ⇒ flat. */
  readonly seedPositions?: PositionSnapshot;
  /** Reconciliation tolerance (absolute qty per key). Absent ⇒ `0` (exact match required). */
  readonly tolerance?: number;
  /** Time-in-force for transmitted orders. Defaults to `DAY` — an order that outlives the session is
   * an order nobody is watching. */
  readonly tif?: TimeInForce;
  /** Redacted-diagnostic sink (default no-op). Receives only already-redacted strings. */
  readonly log?: (line: string) => void;
}

// ─────────────────────────────────────────────────────────────────────────────
// The ledger + the ticket.
// ─────────────────────────────────────────────────────────────────────────────

/** Where one order stands. `working` is the venue's acknowledgment; the three terminals are latched
 * (a terminal order never re-emits, so a replayed callback cannot perturb the stream). */
export type OrderPhase = "pending" | "working" | "filled" | "cancelled" | "rejected";

/**
 * One order's line in the RECONCILIATION LEDGER (kestrel-7o2.8): what the ENGINE submitted beside
 * what the BROKER actually reported. The broker's side is AUTHORITATIVE — it is never overwritten by
 * an engine expectation, and a divergence between the two is a break, not a correction.
 */
export interface IbkrOrderRecord {
  /** The engine's ref (`intent.ref`) — the correlation key on every ORDER event. */
  readonly ref: string;
  /** The IB API order id this ref was transmitted under. */
  readonly ibOrderId: number;
  /** The resolved intent, verbatim — the ENGINE's side of the ledger (including its untouched `px`). */
  readonly intent: OrderIntent;
  /** The gateway's own contract definition this was transmitted against. */
  readonly contract: IbkrContract;
  /** ENGINE truth: what we asked for. */
  readonly submittedQty: number;
  /** BROKER truth: cumulative qty the venue reported EXECUTING (summed over distinct `execId`s). */
  readonly filledQty: number;
  /** BROKER truth: size-weighted average of the prices the venue actually printed. */
  readonly avgFillPx: number | undefined;
  /** BROKER truth: commission the venue reported (per `execId`, deduped). */
  readonly commissionUsd: number;
  /** BROKER truth: the venue's own last-reported remaining quantity. */
  readonly remainingQty: number | undefined;
  readonly phase: OrderPhase;
}

/**
 * A fully-guarded, NOT-YET-TRANSMITTED order (kestrel-7o2.8) — what {@link IbkrBroker.preflight}
 * returns and what the DRY-RUN prints for a human to review. It has cleared all four walls; it simply
 * has not been sent. Every number on it is READ, never invented: `limitPx` is the engine's own `px`
 * verbatim, `multiplier` is the VENUE's.
 */
export interface OrderTicket {
  readonly ref: string;
  /** The gateway's own resolved contract (7o2.6) — conId, OCC localSymbol, multiplier, expiry. */
  readonly contract: IbkrContract;
  /** The exact IB `Contract` that would go on the wire. */
  readonly ibContract: Contract;
  /** The exact IB `Order` that would go on the wire, minus the id (minted at transmit). */
  readonly ibOrder: Order;
  readonly side: "buy" | "sell";
  readonly qty: number;
  /** The ENGINE's price, verbatim. Never rounded, never snapped, never a mid. */
  readonly limitPx: number;
  /** The VENUE's contract multiplier (dollars per point). Read from the definition, never assumed. */
  readonly multiplier: number;
  /** `limitPx x qty x multiplier` — what the L0 notional ceiling bounds. */
  readonly notionalUsd: number;
  /**
   * The DEFINED-RISK max loss in dollars. For a BUY of a long option this IS the premium (the whole
   * never-naked claim: you cannot lose more than you paid). For a BUY of equity it is bounded by the
   * price paid. A SELL that closes a held long is RISK-REDUCING — its max loss is already borne, so
   * it adds `0`.
   */
  readonly maxLossUsd: number;
  /** The venue's signed net position in this leg BEFORE the order (broker truth). */
  readonly heldBefore: number;
  /** Quantity of this leg ALREADY PROMISED AWAY by SELLs that are working (transmitted, acknowledged,
   * NOT yet filled). A resting sell does not move broker truth until it fills, so it is RESERVED here
   * — otherwise a second sell of the same long reads the same `heldBefore` and clears too, and both
   * fill (kestrel-7o2.8 blocker 1: the uncovered short this module actually opened). */
  readonly workingSellQty: number;
  /** The WORST-CASE signed net position: `heldBefore - workingSellQty + signed(qty)` — what we hold
   * once this order AND every already-working SELL has filled. Never negative — never-naked. */
  readonly heldAfter: number;
  /** The intrinsic floor that applied, when one did (a SELL). */
  readonly intrinsicFloor: number | undefined;
  /** The PRICE ANCHOR this ticket cleared: the observed LIVE two-sided book, and the `@fair` inside
   * it. A ticket can only exist if its anchor was trustworthy, so this is a RECEIPT, not a hope. */
  readonly anchor: PriceAnchor;
  /** The L0 verdict. `preflight` only ever RETURNS on `cleared` — anything else THREW. */
  readonly clamp: "cleared";
  /** The engine's price-resolution receipt, carried onto the ticket so `@fair` is auditable. */
  readonly sourceAnnotation: string;
}

// ─────────────────────────────────────────────────────────────────────────────
// SETTLEMENT — the one disappearance that is an EVENT, not a break (kestrel-7o2.24, ADR-0034 q3).
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The VENUE's own statement that a specific EXPIRED position was CASH-SETTLED — the position removed,
 * the cash posted. This is the ONE piece of evidence that can absolve a position's disappearance from
 * {@link IbkrBroker.reconcile}'s trip.
 *
 * It is READ FROM THE VENUE, never derived here. The adapter cannot compute a settlement (it has no
 * settlement price, no account cash, no clearing calendar) and it must never assume one — a position
 * that "should have" settled is not a position that DID. So this arrives through the injected
 * {@link IbkrBrokerDeps.settlementOf} oracle, whose absence means NO disappearance is ever absolved.
 */
export interface SettlementReceipt {
  /** The SIGNED net quantity the venue settled away. Must account for the ENTIRE expected position —
   * a receipt that explains only part of a disappearance explains none of it. */
  readonly qty: number;
  /** The settlement cash the venue posted, in dollars. `0` is LEGITIMATE (an option that expired
   * worthless — the common 0DTE end); the RECEIPT's existence is the evidence, not its size. A long
   * can only ever RECEIVE at settlement, so a negative value is refused. */
  readonly cashUsd: number;
  /** When the venue settled it (the venue's own timestamp, ms). Must not predate the leg's expiry. */
  readonly settledAt: number;
}

/**
 * One recorded SETTLEMENT — {@link IbkrBroker.reconcile}'s answer to a legitimately-expired position
 * that the venue removed and paid out. **This is a RECORD, not a correction**: the position is not
 * "reconciled away" silently; the disappearance is accounted for, on the ledger, with the receipt and
 * the expiry that justified it.
 *
 * It is deliberately NOT a Bus ORDER event. The {@link OrderAction} vocabulary is CLOSED
 * (`place | fill | cancel | reject`, bus/types.ts) — a settlement is not an order action, exactly as a
 * `commissionReport` is not (which likewise folds onto the LEDGER only). Promoting settlement to a
 * first-class Bus event would change `serialize(project(bus))` bytes repo-wide and needs its own ADR;
 * see the ADR-0034 q3 amendment.
 */
export interface SettlementRecord {
  readonly key: PositionKey;
  /** The engine-expected quantity that settled away (the receipt accounted for exactly this). */
  readonly qty: number;
  /** The venue's posted settlement cash. */
  readonly cashUsd: number;
  /** The leg's own expiry (`YYYYMMDD`), read from the venue's contract definition — never inferred. */
  readonly expiry: string;
  /** The venue's settlement timestamp, verbatim from the receipt. */
  readonly settledAt: number;
  /** The `now` the DRIVER had pinned when the settlement was recorded (RUNTIME §0 — never a wall clock). */
  readonly observedAt: number;
}

/**
 * The leg's own EXPIRY INSTANT: 00:00 UTC on its `YYYYMMDD` expiry date. A PURE function of the string
 * (`Date.UTC` is arithmetic, not a clock) — nothing here reads the wall clock.
 *
 * A malformed or impossible date (`20260231`) yields `undefined`, which fails the carve-out closed:
 * an expiry we cannot establish is not an expiry.
 *
 * SPIKE-GRADE, and stated rather than smuggled (ADR-0034 q3): this is the START of expiry day, not the
 * moment the contract actually stops trading. It is therefore the LOOSER of the two available bounds —
 * it admits a disappearance at 09:00 on expiry day. It is safe only because it is a NECESSARY condition
 * beside a SUFFICIENT one: the venue's own settlement receipt is what actually does the absolving, and
 * nothing is absolved without it. The tighter bound (the leg's real last-trade/settlement instant) needs
 * a settlement-time field the contract layer does not carry yet (kestrel-7o2.24).
 */
function expiryInstantUtc(expiry: string): number | undefined {
  if (!/^\d{8}$/.test(expiry)) return undefined;
  const y = Number(expiry.slice(0, 4));
  const m = Number(expiry.slice(4, 6));
  const d = Number(expiry.slice(6, 8));
  const ms = Date.UTC(y, m - 1, d);
  const back = new Date(ms);
  // Reject a date JS silently rolled over (20260231 → 2026-03-03): a wrong expiry is worse than none.
  if (back.getUTCFullYear() !== y || back.getUTCMonth() !== m - 1 || back.getUTCDate() !== d) return undefined;
  return ms;
}

// ─────────────────────────────────────────────────────────────────────────────
// The paper order face.
// ─────────────────────────────────────────────────────────────────────────────

/** The IBKR PAPER {@link BrokerAdapter} (kestrel-7o2.8) — a Gate, plus the ledger/reconciliation
 * surface the safety envelope reads. */
export interface IbkrBroker extends BrokerAdapter {
  now: number;
  submit(intent: OrderIntent): string;
  cancel(ref: string): void;
  readonly events: readonly NewBusEvent[];
  /** The BROKER's AUTHORITATIVE signed net position per {@link PositionKey} (ADR-0034 §4). */
  positions(): PositionSnapshot;
  /** Guard an intent through all four walls and return its {@link OrderTicket} — TRANSMITTING NOTHING. */
  preflight(intent: OrderIntent): OrderTicket;
  /** The per-order reconciliation ledger (engine expectation beside broker truth). */
  ledger(): readonly IbkrOrderRecord[];
  /** Compare the engine/Blotter EXPECTED positions + the ledger against the BROKER's own truth; any
   * break TRIPS the kill-switch, fail-closed. The ONE exception is a legitimately EXPIRED-AND-SETTLED
   * position, which is recorded on {@link IbkrBroker.settlements} instead (kestrel-7o2.24). */
  reconcile(): void;
  /** The SETTLEMENT LEDGER: every disappearance {@link IbkrBroker.reconcile} accounted for as a
   * cash settlement rather than a break, with the receipt and expiry that justified each one. */
  settlements(): readonly SettlementRecord[];
  /** The kill-switch every transmit consults. Trip it to halt all transmission. On the `makeGate("paper")`
   * path this is the SAME instance the seam's {@link import("../../broker.ts").LiveClamp} consults —
   * `makeGate` threads it in, so a trip in EITHER layer (an operator STAND_DOWN on the clamp, or this
   * face's own at-observation never-naked trip) halts the whole path. There is exactly ONE. */
  readonly killSwitch: KillSwitch;
  /** This face's configured L0 ceilings (kestrel-7o2.9). EXPOSED so `makeGate("paper")` can thread them
   * into the seam's {@link import("../../broker.ts").makeLiveClamp} — the SAME limits then back BOTH the
   * seam's L0 clamp and this face's WALL 5 (ADR-0034 §4/§7: L0 is above the adapter): TWO INDEPENDENTLY-
   * PROVEN ceilings over ONE set of numbers, so they can never diverge. NEITHER is subordinate — each is
   * mutation-proven load-bearing on its own. */
  readonly limits: RiskLimits;
  /** The venue's TRUE per-intent contract multiplier — an option `100`, an equity `1` (kestrel-7o2.8).
   * EXPOSED so `makeGate("paper")` threads it into the seam's {@link import("../../broker.ts").makeLiveClamp}
   * L0 clamp, whose notional ceiling is then computed on REAL per-contract notional rather than a flat
   * `1×` that is 100× too loose for a 100× option. Resolves the contract from this face's ContractBook;
   * an UNRESOLVED contract returns the CONSERVATIVE `100` (over-estimate the notional so the seam ceiling
   * fails closed — the adapter's own WALL 2 then refuses the unresolved leg outright). Never transmits. */
  multiplierOf(intent: OrderIntent): number;
  /** Detach every IB listener this face registered (idempotent; safe from a STAND_DOWN path). */
  detach(): void;
}

/** A float epsilon for the intrinsic floor — a SELL exactly AT intrinsic must clear it, and binary
 * floating point must not turn `3.4 >= 3.4` into a refusal. Deliberately far below one tick. */
const PX_EPSILON = 1e-9;

/** The conservative per-contract multiplier {@link IbkrBroker.multiplierOf} returns for an UNRESOLVED
 * contract (kestrel-7o2.8): the OPTION multiplier `100`, the larger of the two classes, so the seam
 * clamp OVER-estimates an unknown leg's notional and errs toward refusal (fail-closed). The adapter's
 * own WALL 2 then refuses the unresolved leg outright — this value only shapes the seam's ceiling math. */
const CONSERVATIVE_MULTIPLIER = 100;

/** IB error code 202 — "Order Canceled - reason: …". A routine CANCEL, NOT a reject: mapping it onto
 * reject makes the Blotter learn the wrong terminal (kestrel-7o2.8 minor). */
const IB_ORDER_CANCELLED = 202;

/** IB cancel-rejection codes — the venue REFUSED a cancel because the order is STILL LIVE:
 *   10148 "OrderId … cannot be cancelled, state: …"  (live in some non-cancellable state)
 *   10147 "OrderId … that needs to be cancelled is not found"  (cancel of an order we still track)
 * These are NOT order rejects. Terminating/releasing on one frees a reservation for a LIVE order — the
 * exact Route A naked short. An error of still-live/unknown fate must never free a reservation. */
const IB_CANCEL_REJECTION_CODES = new Set<number>([10147, 10148]);

/**
 * Is this order-scoped error a CANCEL-REJECTION — i.e. the order is STILL LIVE at the venue (Route A)?
 * Keyed on the documented cancel-rejection codes, with a message backstop for the "cannot be cancelled
 * / state:" family in case the venue reports a variant code. Fail-closed: when in doubt about a cancel
 * rejection, treat the order as live and DO NOT release its reservation.
 */
function isStillLiveOrderError(code: number, err: Error): boolean {
  if (IB_CANCEL_REJECTION_CODES.has(code)) return true;
  return /cannot be cancel|still live|, ?state:/i.test(err.message);
}

class IbkrPaperBroker implements IbkrBroker {
  now = 0;

  readonly #cfg: IbkrConfig;
  readonly #client: IbOrderClient;
  readonly #contracts: ContractBook;
  readonly #drain: () => void;
  readonly #limits: RiskLimits;
  readonly #budgetUsd: number;
  readonly #noUncoveredShort: boolean;
  readonly #intrinsicOf: IbkrBrokerDeps["intrinsicOf"];
  readonly #priceAnchorOf: IbkrBrokerDeps["priceAnchorOf"];
  readonly #expectedPositions: () => PositionSnapshot;
  readonly #settlementOf: ((key: PositionKey) => SettlementReceipt | undefined) | undefined;
  readonly #nextOrderId: () => number;
  readonly #tolerance: number;
  readonly #tif: TimeInForce;
  readonly #log: (line: string) => void;
  readonly killSwitch: KillSwitch;

  readonly #events: NewOrderEvent[] = [];
  /** ref → the ledger line. */
  readonly #byRef = new Map<string, MutableRecord>();
  /** IB order id → ref (the inbound pump's correlation). */
  readonly #byIbId = new Map<number, string>();
  /** Executions already folded, by `execId` — IB replays them, and a fill must never double-count. */
  readonly #seenExecs = new Set<string>();
  /** Commissions already folded, by `execId` (same replay hazard). */
  readonly #seenCommissions = new Set<string>();
  /** BROKER truth, folded from the venue's OWN executions (`execDetails`) SINCE that key's baseline. */
  readonly #execPositions = new Map<PositionKey, number>();
  /**
   * BROKER truth, from the venue's OWN account push (`position`) — the account's statement of record.
   * It is a BASELINE, not an answer: {@link IbkrPaperBroker.positions} adds the executions observed
   * SINCE it, and the push REBASES that fold (stamping it to zero) rather than overwriting it.
   * Overwriting is what shipped, and it silently DISCARDED every fill observed on top of a push — the
   * naked short of kestrel-7o2.8 blocker 2.
   */
  readonly #venueBaseline = new Map<PositionKey, number>();
  /**
   * Quantity RESERVED per key by SELLs that are WORKING — transmitted, not yet filled. Broker truth
   * does not move until a fill prints, so without this a second sell of the same long reads the same
   * `heldBefore`, clears never-naked, and both fill. Reserved at SUBMIT; released on EVERY terminal
   * outcome (fill, cancel, reject) and on a transmit rollback. This is the never-naked wall's memory.
   */
  readonly #workingSellQty = new Map<PositionKey, number>();
  /** The opening book a resumed session was seeded with. */
  readonly #seed: PositionSnapshot;
  /** Disappearances accounted for as CASH SETTLEMENTS (kestrel-7o2.24) — one per key, at most. */
  readonly #settlements = new Map<PositionKey, SettlementRecord>();
  #registered: Array<[EventName, IbListener]> = [];

  constructor(cfg: IbkrConfig, deps: IbkrBrokerDeps) {
    // WALL: PAPER ONLY. `live` is a Kestrel mode gate, not a port fact — live routing (7o2.10) lands
    // only behind the human-signed LiveArm, and this face ships none. Refused at CONSTRUCTION, so a
    // live-configured order face cannot even be built, let alone reached.
    if (cfg.mode === "live") {
      throw new IbkrOrderRefused(
        "mode-gate",
        `the IBKR order face is PAPER-ONLY (${describeIbkrConfig(cfg)}) — live routing is kestrel-7o2.10 and requires the human-signed LiveArm (protocol excludes \`live\` from WALLET_SIGNABLE_SCOPES); a config flag can never grant it`,
      );
    }
    this.#cfg = cfg;
    this.#client = deps.client;
    this.#contracts = deps.contracts;
    this.#drain = deps.drain;
    this.#limits = deps.limits;
    // BOUNDED RISK: default the budget to the L0 notional ceiling (a long's max_loss IS its notional).
    this.#budgetUsd = deps.budgetUsd ?? deps.limits.maxNotionalUsd;
    // POLICY: no-uncovered-short defaults ON (the safe default; the 0DTE book keeps it hard).
    this.#noUncoveredShort = deps.noUncoveredShort ?? true;
    this.#intrinsicOf = deps.intrinsicOf;
    this.#priceAnchorOf = deps.priceAnchorOf;
    this.#expectedPositions = deps.expectedPositions;
    // ABSENT ⇒ the carve-out is structurally unreachable for this caller: no oracle, no absolution.
    this.#settlementOf = deps.settlementOf;
    this.#nextOrderId = deps.nextOrderId;
    this.#tolerance = deps.tolerance ?? 0;
    this.#tif = deps.tif ?? TimeInForce.DAY;
    this.#log = deps.log ?? ((): void => {});
    this.killSwitch = deps.killSwitch ?? makeKillSwitch();
    this.#seed = deps.seedPositions ?? {};
    this.#attach();

    // SUBSCRIBE to the venue's own statement of record — IB does NOT emit `position` unsolicited.
    // Without this call the venue map stays permanently EMPTY in production and `positions()` degrades
    // to the seed plus our own execution fold: the ENGINE's mirror, not the BROKER's truth, wearing
    // the label of broker truth (kestrel-7o2.8 blocker 3). An invariant is only real if something
    // calls it, so the pull is subscribed HERE, at construction, not left to a caller to remember.
    try {
      this.#client.reqPositions();
    } catch (cause) {
      this.killSwitch.trip(
        `could not subscribe to the venue's POSITION push (reqPositions threw: ${String(cause)}) — the broker's own statement of record is unavailable, so every risk wall would be reading a guess. Halting paper transmission (fail-closed)`,
      );
    }
  }

  // ── the Gate seam ───────────────────────────────────────────────────────────

  get events(): readonly NewBusEvent[] {
    return this.#events;
  }

  /** This face's configured L0 ceilings — read by `makeGate("paper")` so the seam's L0 clamp enforces the
   * SAME limits. One of TWO INDEPENDENTLY-PROVEN ceilings, not a backstop to the other: each is load-bearing
   * on its own (a mutation gutting either turns a test RED). */
  get limits(): RiskLimits {
    return this.#limits;
  }

  /** The venue's TRUE per-intent contract multiplier (kestrel-7o2.8) — resolved from this face's OWN
   * ContractBook (an option `100`, an equity `1`), the SAME number WALL 5's notional uses. `makeGate("paper")`
   * threads this into the seam clamp so its notional ceiling is computed on real per-contract notional,
   * not a flat `1×` that is 100× too loose for a 100× option. FAIL-CLOSED on an unresolved contract: it
   * returns the CONSERVATIVE {@link CONSERVATIVE_MULTIPLIER} (over-estimate the notional so the seam
   * ceiling errs toward refusal); the adapter's own WALL 2 then refuses the unresolved leg outright.
   * Transmits nothing — a read-only resolution. */
  multiplierOf(intent: OrderIntent): number {
    return this.#contracts.get(intent)?.multiplier ?? CONSERVATIVE_MULTIPLIER;
  }

  /**
   * Guard a resolved intent through all four walls and return the {@link OrderTicket} it would
   * transmit — **TRANSMITTING NOTHING**. `submit` is exactly `preflight` + `placeOrder`, so the ticket
   * a human reviews in the DRY-RUN is bit-for-bit the order that would go on the wire. Throws the same
   * typed refusals `submit` does; it simply never reaches the socket.
   */
  preflight(intent: OrderIntent): OrderTicket {
    // ── WALL 0: the kill-switch. First, so a halted process refuses uniformly (7o2.9).
    if (this.killSwitch.tripped) {
      throw new ClampRefused(
        "killed",
        intent.ref,
        `paper transmission halted — kill-switch tripped: ${this.killSwitch.reason ?? "(no reason)"} (fail-closed)`,
      );
    }

    // ── WALL 1: THE INTENT ITSELF. A malformed order is refused HERE, not handed to IB to validate.
    // A SELL of `qty: -3` sign-inverts (`heldAfter` INCREASES), sails through every risk wall below,
    // and puts `totalQuantity: -3` on the wire. The walls may only reason about a well-formed order.
    if (!Number.isFinite(intent.qty) || !Number.isInteger(intent.qty) || intent.qty <= 0) {
      throw new IbkrOrderRefused(
        "invalid-order",
        `qty ${intent.qty} is not a positive whole number of contracts/shares — refusing at the boundary rather than letting IB be this system's validator (a negative qty would sign-invert straight through the never-naked wall)`,
        { ref: intent.ref },
      );
    }
    if (!Number.isFinite(intent.px) || intent.px <= 0) {
      throw new IbkrOrderRefused(
        "invalid-order",
        `px ${intent.px} is not a positive finite limit price — the adapter transmits the engine's number or it REFUSES; it never repairs one`,
        { ref: intent.ref },
      );
    }
    if (this.#byRef.has(intent.ref)) {
      throw new IbkrOrderRefused(
        "invalid-order",
        `ref ${intent.ref} is ALREADY on the ledger — a second order under a live ref would clobber its line (and orphan any reservation it holds). Refs are the engine's correlation key and are never reused`,
        { ref: intent.ref },
      );
    }

    // ── WALL 2: the contract. Pre-resolved by 7o2.6 or nothing — never guessed on the hot path.
    const contract = this.#contracts.get(intent);
    if (contract === undefined) {
      throw new IbkrOrderRefused(
        "unresolved-contract",
        `leg ${positionKeyOf(intent)} has no pre-resolved gateway contract — an identity is NEVER guessed at the tick (resolve it ahead of the hot path via resolveContract, kestrel-7o2.6)`,
        { ref: intent.ref },
      );
    }

    const key = positionKeyOf(intent);
    // The BROKER's own reported truth is the risk input — never the engine's expectation (ADR-0034 §4).
    const heldBefore = this.positions()[key] ?? 0;
    // …MINUS what our own WORKING SELLs have already promised away. A resting sell has not moved
    // broker truth yet, so truth alone over-states what is still ours to sell.
    const workingSellQty = this.#workingSellQty.get(key) ?? 0;
    const availableBefore = heldBefore - workingSellQty;
    const signed = intent.side === "buy" ? intent.qty : -intent.qty;
    const heldAfter = availableBefore + signed;

    const multiplier = contract.multiplier;
    const notionalUsd = intent.px * intent.qty * multiplier;

    // ── WALL 3: BOUNDED RISK + the (overridable) no-uncovered-short POLICY (kestrel-buos).
    //
    // The PLATFORM INVARIANT is BOUNDED RISK: max_loss must be COMPUTABLE (finite) and
    // `size × max_loss ≤ budget`, fail-closed. "Never naked" is an APPLICATION policy (default ON), not
    // a platform constraint — two reasonable strategies differ on a cash-secured put (ARCHITECTURE §6),
    // so the platform forbids only the ALWAYS-wrong cases (unbounded max_loss) and leaves the merely
    // risky ones to an overridable dial.
    //
    // A STRICTLY RISK-REDUCING order is PERMITTED BY CONSTRUCTION: a BUY against a short, or a SELL that
    // closes (does not exceed) a long. The face must ALWAYS be able to GET FLAT, so these are never
    // refused for DIRECTION — the crude `heldAfter < 0` sign test refused a BUY that reduces a short
    // (it could never buy its way flat) and a riskless equity close-out, and it bounded a SIGN, not a
    // RISK. A risk-reducing order still passes the L0 ceilings and the price-anchor wall below —
    // reducing risk is not a licence to transmit garbage — but it is never refused for direction.
    //
    // The exposure is measured against the WORST CASE — broker truth minus every working sell — so the
    // second sell of a long a first still-resting sell already spoke for is not treated as covered
    // (7o2.8 blocker 1's reservation).
    const isBuy = intent.side === "buy";
    const riskReducing = (isBuy && availableBefore < 0) || (!isBuy && heldAfter >= 0);
    let maxLossUsd = 0;
    if (!riskReducing) {
      if (isBuy) {
        // Opening/adding a LONG. max_loss is the premium (option) / notional (equity) PAID — finite by
        // construction. The L0 notional ceiling (wall 5) bounds it against the budget below.
        maxLossUsd = notionalUsd;
      } else {
        // A SELL that leaves a NET SHORT of `shortQty`. Compute the RESULTING position's max_loss, or
        // refuse if it is unbounded — bounded risk is the invariant, enforced regardless of policy.
        const shortQty = -heldAfter;
        if (contract.kind === "equity") {
          throw new IbkrOrderRefused(
            "unbounded-risk",
            `a SHORT EQUITY at ${key} (net ${heldAfter}) has an UNBOUNDED max_loss (the stock can rise without limit) — bounded risk cannot be satisfied at ANY budget, so it is refused REGARDLESS of the no-uncovered-short policy (kestrel-buos: bounded risk is the invariant)`,
            { ref: intent.ref },
          );
        }
        if (contract.right === "C") {
          throw new IbkrOrderRefused(
            "unbounded-risk",
            `a naked short CALL at ${key} (net ${heldAfter}) has an UNBOUNDED max_loss — refused by the BOUNDED-RISK invariant itself, regardless of policy (kestrel-buos)`,
            { ref: intent.ref },
          );
        }
        // A short PUT: max_loss = (strike − premium) × multiplier per contract, the stock floored at 0.
        // FINITE — a cash-secured put is a defined-risk trade. `intent.px` is the premium the engine
        // priced; the adapter never invents it.
        const perContract = Math.max(contract.strike - intent.px, 0) * multiplier;
        maxLossUsd = perContract * shortQty;
        if (!Number.isFinite(maxLossUsd)) {
          throw new IbkrOrderRefused(
            "unbounded-risk",
            `max_loss for the short PUT at ${key} is not computable (non-finite) — fail-closed (kestrel-buos)`,
            { ref: intent.ref },
          );
        }
        // BUDGET: bounded risk requires size × max_loss ≤ budget.
        if (maxLossUsd > this.#budgetUsd + PX_EPSILON) {
          throw new IbkrOrderRefused(
            "over-budget",
            `the short PUT at ${key} (net ${heldAfter}) carries max_loss $${maxLossUsd} which exceeds the risk budget $${this.#budgetUsd} — bounded risk requires size × max_loss ≤ budget (kestrel-buos, fail-closed)`,
            { ref: intent.ref },
          );
        }
        // POLICY: no-uncovered-short (default ON — the 0DTE book keeps it hard). When ON, even a
        // budgeted, finite-risk uncovered put is refused. When OFF, it is ALLOWED. A dial, not the
        // invariant — the UNBOUNDED cases above are refused regardless.
        if (this.#noUncoveredShort) {
          throw new IbkrOrderRefused(
            "naked-short",
            `${intent.side} ${intent.qty} of ${key} would leave a NET SHORT of ${shortQty} (uncovered). Its max_loss $${maxLossUsd} is FINITE and within budget, but the no-uncovered-short POLICY is ON (the default — the 0DTE book keeps it hard). Refusing (kestrel-buos: turn the policy off to sell puts; bounded risk still binds)`,
            { ref: intent.ref },
          );
        }
      }
    }

    // ── WALL 4: the INTRINSIC FLOOR. A SELL (a close of a held long) may never price below intrinsic.
    // The adapter cannot re-price, so it cannot FLOOR the price — it REFUSES, and the engine's own
    // floor (engine/pricing.ts) remains the one place a price is made.
    //
    // The floor is keyed on INSTRUMENT KIND, not on undefined-ness (kestrel-buos §e): an OPTION has an
    // intrinsic and an UNKNOWN one refuses (never assumed satisfied); an EQUITY has NO optionality, so
    // its floor is simply 0 and an undefined intrinsic is not a refusal — the round-1 test that keyed
    // the floor on undefined-ness obstructed a RISKLESS equity close-out in live testing.
    let intrinsicFloor: number | undefined;
    if (intent.side === "sell" && contract.kind === "option") {
      const intr = this.#intrinsicOf(intent);
      if (intr === undefined || !Number.isFinite(intr)) {
        throw new IbkrOrderRefused(
          "intrinsic-unknown",
          `the intrinsic of ${key} is UNKNOWN, so the SELL floor cannot be proven — refusing rather than ASSUMING the floor is satisfied (a silent default is never a default here)`,
          { ref: intent.ref },
        );
      }
      intrinsicFloor = intr;
      if (intent.px + PX_EPSILON < intr) {
        throw new IbkrOrderRefused(
          "below-intrinsic",
          `SELL ${key} at ${intent.px} is BELOW its intrinsic ${intr} — a sell is floored at intrinsic, always. The adapter is a transmitter and will not re-price it up`,
          { ref: intent.ref },
        );
      }
    }

    // ── WALL 5: the L0 CLAMP (7o2.9 RiskLimits) — the ADAPTER's venue-boundary ceiling, one of TWO
    // independently-proven ceilings (defense-in-depth, ADR-0034 §4).
    //
    // The OTHER ceiling is the seam's makeLiveClamp ABOVE the adapter. On the `makeGate("paper")` path
    // that clamp wraps this face, SHARES this face's ONE kill-switch (threaded in by makeGate), and
    // enforces the SAME limits AND the SAME true per-contract multiplier (both threaded in via
    // `this.limits` / `this.multiplierOf`) — so an over-ceiling order is refused by the seam before it
    // reaches here. But this block is NOT a subordinate shadow of a single canonical owner: it is an
    // INDEPENDENT barrier proven load-bearing on its own. (a) This face is ALSO reachable BARE (unit
    // tests, the env-gated dry-run/equity-order proof scripts) where NO seam wraps it, and fail-closed
    // forbids a bare venue face with no ceiling — a mutation gutting this block goes RED there. (b) It
    // uses the VENUE's true per-contract `multiplier` (100 for an option, 1 for equity), the SAME number
    // the seam now reads via multiplierOf — same limits, same switch, never divergent. Two barriers.
    if (intent.qty > this.#limits.maxOrderQty) {
      throw new ClampRefused(
        "order-size",
        intent.ref,
        `order qty ${intent.qty} exceeds maxOrderQty ${this.#limits.maxOrderQty} (fail-closed, bounded-risk)`,
      );
    }
    if (Math.abs(heldAfter) > this.#limits.maxPositionQty) {
      throw new ClampRefused(
        "position",
        intent.ref,
        `projected net position ${heldAfter} at ${key} exceeds maxPositionQty ${this.#limits.maxPositionQty} (fail-closed)`,
      );
    }
    if (notionalUsd > this.#limits.maxNotionalUsd) {
      throw new ClampRefused(
        "notional",
        intent.ref,
        `order notional ${notionalUsd} (px ${intent.px} x qty ${intent.qty} x multiplier ${multiplier}) exceeds maxNotionalUsd ${this.#limits.maxNotionalUsd} (fail-closed)`,
      );
    }

    // ── WALL 6: THE PRICE ANCHOR (kestrel-7o2.8 blocker 4; corrected per kestrel-ltrf). The
    // walls above bound SIZE; this one bounds the PRICE. It gates on whether the `@fair` RECEIPT can be
    // TRUSTED — NOT on a naive fair-outside-[bid,ask] clamp. `@fair` is UNDERLYING-anchored (the fresh
    // index spot), not book-anchored: for a fast-moving index option the posted quote lags the fast index
    // and/or is wide, so fair LEGITIMATELY sits away from the posted option quote — clamping to
    // [bid,ask] would defeat the whole point of fair. What stands, fail-closed:
    //
    //   (a) NO OBSERVED BOOK, or a dark / one-sided / crossed one ⇒ the anchor is UNRESOLVABLE.
    //   (b) DELAYED / FROZEN data ⇒ a HEALTH SIGNAL, never a price-anchor INPUT. Request live; without
    //       it the anchor is UNRESOLVABLE (this part of the round-1 wall is kept verbatim).
    //   (c) an UNKNOWN (non-finite) @fair ⇒ unresolvable.
    //   (d) an UNTRUSTED @fair RECEIPT — a stale index, a frozen input, no ATM coverage — ⇒ refuse to
    //       transmit on a fair the receipt cannot vouch for. The precise fresh-tight-book diagnostic
    //       belongs to the fair redesign (src/fair, kestrel-ku99/ltrf — OUT OF SCOPE here); this module
    //       consumes the engine's TRUST verdict rather than re-deriving it or clamping to a lagging book.
    const anchor = this.#priceAnchorOf(intent);
    if (anchor === undefined) {
      throw new IbkrOrderRefused(
        "anchor-unresolvable",
        `no OBSERVED two-sided book for ${key} — the PRICE ANCHOR is UNRESOLVABLE, and an unaudited price is never authorizable (fail-closed; the book is never assumed)`,
        { ref: intent.ref },
      );
    }
    if (anchor.freshness !== "live") {
      throw new IbkrOrderRefused(
        "anchor-stale",
        `the book for ${key} is ${anchor.freshness.toUpperCase()} (bid=${anchor.bid} ask=${anchor.ask}) — delayed/frozen data is a HEALTH SIGNAL, never a PRICE ANCHOR (RUNTIME §4). Anything that PRICES an order requires LIVE data; without it the anchor is UNRESOLVABLE`,
        { ref: intent.ref },
      );
    }
    if (
      !Number.isFinite(anchor.bid) ||
      !Number.isFinite(anchor.ask) ||
      anchor.bid <= 0 ||
      anchor.ask <= 0 ||
      anchor.ask + PX_EPSILON < anchor.bid
    ) {
      throw new IbkrOrderRefused(
        "anchor-unresolvable",
        `the observed book for ${key} is DARK / ONE-SIDED / CROSSED (bid=${anchor.bid} ask=${anchor.ask}) — that is not a book, and a fabricated level is exactly what fail-closed forbids`,
        { ref: intent.ref },
      );
    }
    if (!Number.isFinite(anchor.fair)) {
      throw new IbkrOrderRefused(
        "anchor-unresolvable",
        `@fair for ${key} is UNKNOWN — the price cannot be corroborated, and it is never ASSUMED sound (a silent default is never a default here)`,
        { ref: intent.ref },
      );
    }
    if (!anchor.fairTrusted) {
      throw new IbkrOrderRefused(
        "fair-untrusted",
        `the @fair ${anchor.fair} for ${key} carries an UNTRUSTED receipt (a stale index, a frozen input, or no ATM coverage) — refusing to transmit on a fair the receipt cannot vouch for (kestrel-ltrf). @fair is UNDERLYING-anchored, so it may legitimately sit away from the posted book ${anchor.bid} / ${anchor.ask}; the wall gates on the receipt's TRUST, never on a naive book clamp`,
        { ref: intent.ref },
      );
    }

    // Every wall cleared. Build the EXACT wire payload — and hand it back untransmitted.
    const ibContract = ibContractOf(contract);
    const ibOrder: Order = {
      action: intent.side === "buy" ? IbOrderAction.BUY : IbOrderAction.SELL,
      orderType: OrderType.LMT,
      totalQuantity: intent.qty,
      // THE ENGINE'S PRICE, VERBATIM. Not rounded. Not snapped "to improve". Never a mid.
      lmtPrice: intent.px,
      tif: this.#tif,
      transmit: true,
      orderRef: intent.ref,
      ...(this.#cfg.account === undefined ? {} : { account: this.#cfg.account }),
    };

    return {
      ref: intent.ref,
      contract,
      ibContract,
      ibOrder,
      side: intent.side,
      qty: intent.qty,
      limitPx: intent.px,
      multiplier,
      notionalUsd,
      // The DEFINED-RISK max_loss the resulting position carries (kestrel-buos): the premium/notional
      // for a long, `(strike − premium) × multiplier × shortQty` for a budgeted short put, and 0 for a
      // strictly risk-reducing order. A ticket only exists if this is FINITE and within budget.
      maxLossUsd,
      heldBefore,
      workingSellQty,
      heldAfter,
      intrinsicFloor,
      anchor,
      clamp: "cleared",
      sourceAnnotation: intent.sourceAnnotation,
    };
  }

  /**
   * Transmit a resolved intent to the paper venue. `preflight` FIRST (all four walls; any refusal
   * throws and nothing reaches the wire), then exactly one `placeOrder` at the engine's own price.
   * Returns the ref the engine correlates the venue's later ORDER events against.
   */
  submit(intent: OrderIntent): string {
    const ticket = this.preflight(intent); // throws ⇒ NOTHING transmitted
    const ibOrderId = this.#nextOrderId();

    // MINOR: a re-issued IB order id must NOT silently re-point correlation. A duplicate `nextOrderId`
    // would map this ref's order id onto a ref already in flight, so an EARLIER order's executions would
    // fold under a LATER record — inflating the long the risk wall reads. Refuse it like a duplicate ref
    // (invalid-order); NOTHING is transmitted, and no reservation is taken.
    if (this.#byIbId.has(ibOrderId)) {
      throw new IbkrOrderRefused(
        "invalid-order",
        `nextOrderId re-issued ibOrderId ${ibOrderId}, already correlated to ref ${this.#byIbId.get(ibOrderId)} — a duplicated order id would re-point correlation and fold one order's executions under another (inflating the risk wall's read). Refusing (kestrel-7o2.8)`,
        { ref: intent.ref },
      );
    }
    const order: Order = { ...ticket.ibOrder, orderId: ibOrderId };

    const rec: MutableRecord = {
      ref: intent.ref,
      ibOrderId,
      intent,
      contract: ticket.contract,
      submittedQty: intent.qty,
      filledQty: 0,
      fillNotional: 0,
      commissionUsd: 0,
      remainingQty: undefined,
      avgFillPx: undefined,
      phase: "pending",
      placed: false,
      terminal: false,
      reservedQty: 0,
      execIds: new Set<string>(),
    };
    this.#byRef.set(intent.ref, rec);
    this.#byIbId.set(ibOrderId, intent.ref);

    // RESERVE the working SELL, BEFORE the wire. From this instant the long it closes is spoken for:
    // no second sell of it can clear never-naked, however long this one rests unfilled. Released on
    // EVERY terminal outcome — and on the rollback below, so a dead socket never locks a long either.
    if (intent.side === "sell") {
      const key = positionKeyOf(intent);
      rec.reservedQty = intent.qty;
      this.#workingSellQty.set(key, (this.#workingSellQty.get(key) ?? 0) + intent.qty);
    }

    try {
      this.#client.placeOrder(ibOrderId, ticket.ibContract, order);
    } catch (cause) {
      // ROUTE C: a throw from the socket is LOUD, and the order's fate is UNKNOWN — a partial socket
      // write may have left it LIVE at the venue. A silent assume-NOT-filled is the exact mirror of the
      // forbidden silent assume-filled: if it fills, its execution must still find its record and fold
      // into broker truth, and its reservation must still guard the long. So we KEEP the correlation
      // (#byRef / #byIbId) and KEEP the reservation, marking the record UNKNOWN-FATE — never erased. A
      // second sell of the same long is then refused (the reservation stands), and reconciliation
      // resolves the order when the venue speaks. The throw below is the loud surface.
      rec.unknownFate = true;
      rec.phase = "pending";
      this.#log(
        `placeOrder THREW for ${intent.ref} [ibOrderId=${ibOrderId}] — UNKNOWN FATE (may be LIVE at the venue). Keeping its correlation + reservation; it will fold if it fills (Route C, fail-closed): ${String(cause)}`,
      );
      throw new IbkrOrderRefused(
        "transmit-failed",
        `placeOrder threw on the IB Gateway socket for ${intent.ref} (${describeIbkrConfig(this.#cfg)}) — the order's fate is UNKNOWN and its correlation + reservation are KEPT (never assume not-filled)`,
        { ref: intent.ref, cause },
      );
    }

    this.#log(`transmitted ${intent.side} ${intent.qty} ${positionKeyOf(intent)} @ ${intent.px} [ibOrderId=${ibOrderId}]`);
    // No ORDER event is minted here: `place` comes from the VENUE's own acknowledgment (the inbound
    // pump), because the broker's report is authoritative — not our optimism that it arrived.
    this.#drain();
    return intent.ref;
  }

  /** Pull a resting order. A no-op on an unknown/already-terminal ref (fail-closed, never a crash).
   * The `cancel` ORDER event is minted by the VENUE's terminal status, not by our request. */
  cancel(ref: string): void {
    const rec = this.#byRef.get(ref);
    if (rec === undefined || rec.terminal) {
      this.#drain();
      return;
    }
    try {
      this.#client.cancelOrder(rec.ibOrderId);
    } catch (cause) {
      // A cancel is RISK-REDUCING: a failure to pull is loud in the log, but it must never crash the
      // STAND_DOWN path it is usually called from.
      this.#log(`cancelOrder threw for ${ref} [ibOrderId=${rec.ibOrderId}]: ${String(cause)}`);
    }
    this.#drain();
  }

  // ── the broker's AUTHORITATIVE truth ────────────────────────────────────────

  /**
   * The BROKER's own signed net position per {@link PositionKey} (ADR-0034 §4) — the risk input every
   * wall reads, and the reconciliation anchor. It is NEVER the engine's expectation.
   *
   * It is a BASELINE PLUS A FOLD, never a last-write-wins overwrite:
   *
   *  - the account's own `position` push is the BASELINE for its key (its statement of record, which
   *    outranks the seed — a resumed session's opening book);
   *  - the venue's OWN executions observed SINCE that baseline fold on top.
   *
   * The shipped version let the push OVERWRITE the fold, so once IB had pushed a key, every fill this
   * adapter subsequently OBSERVED was silently dropped from the risk input — and a discarded fill is a
   * naked short waiting to happen (kestrel-7o2.8 blocker 2: push LONG 2 → sell 2 → the venue prints
   * the execution → `positions()` still said 2 → a second sell of 2 cleared and transmitted).
   */
  positions(): PositionSnapshot {
    const snap: Record<PositionKey, number> = {};
    for (const [key, qty] of Object.entries(this.#seed)) snap[key] = qty;
    // The venue's statement of record REPLACES the seed for its key — and RE-BASES the fold, which
    // was stamped to zero at the push, so what follows counts only what we have observed SINCE.
    for (const [key, qty] of this.#venueBaseline) snap[key] = qty;
    for (const [key, qty] of this.#execPositions) snap[key] = (snap[key] ?? 0) + qty;
    return snap;
  }

  ledger(): readonly IbkrOrderRecord[] {
    return [...this.#byRef.values()].map(
      (r): IbkrOrderRecord => ({
        ref: r.ref,
        ibOrderId: r.ibOrderId,
        intent: r.intent,
        contract: r.contract,
        submittedQty: r.submittedQty,
        filledQty: r.filledQty,
        avgFillPx: r.avgFillPx,
        commissionUsd: r.commissionUsd,
        remainingQty: r.remainingQty,
        phase: r.phase,
      }),
    );
  }

  /**
   * The RECONCILIATION TRIP (kestrel-7o2.9 / ADR-0034 §4). Two comparisons, both anchored to the
   * BROKER's own report — which is authoritative, always:
   *
   *  1. **Per-order**: the venue may never report EXECUTING more than we submitted. An over-fill is a
   *     break, not something the engine reconciles away.
   *  2. **Per-position**: the engine/Blotter EXPECTED snapshot vs the broker's PULL. A broker fill the
   *     engine never originated, or an engine order with no broker terminal state, both diverge here.
   *
   * Any break beyond `tolerance` TRIPS the {@link KillSwitch} — every subsequent transmit then refuses
   * `killed`. Never a silent divergence.
   *
   * ## The ONE carve-out: EXPIRY / CASH SETTLEMENT (kestrel-7o2.24, ADR-0034 q3)
   * A cash-settled index option held to settle DISAPPEARS from the venue's position report and is
   * replaced by cash. Read as expected-N-vs-broker-0 that is a break — so the switch tripped on the
   * NORMAL END of every hold-to-cash-settle position, i.e. on the happy path of the whole design.
   *
   * So one shape, and only one, is recorded as a {@link SettlementRecord} instead of tripping. It is
   * narrow ON PURPOSE — a broad carve-out silently deletes the property this method exists for. See
   * {@link IbkrPaperBroker.settlementAbsolving} for the conditions; every one of them is necessary and
   * none is sufficient alone.
   */
  reconcile(): void {
    for (const r of this.#byRef.values()) {
      if (r.filledQty > r.submittedQty + this.#tolerance) {
        this.killSwitch.trip(
          `reconciliation break (OVER-FILL) on ${r.ref} [ibOrderId=${r.ibOrderId}]: engine submitted ${r.submittedQty}, broker reported executing ${r.filledQty} — the BROKER's report is authoritative; halting paper transmission (ADR-0034 §4, fail-closed)`,
        );
        return;
      }
    }
    const expected = this.#expectedPositions();
    const actual = this.positions();
    for (const key of new Set<PositionKey>([...Object.keys(expected), ...Object.keys(actual)])) {
      const e = expected[key] ?? 0;
      const a = actual[key] ?? 0;
      if (Math.abs(a - e) > this.#tolerance) {
        // The ONE disappearance that is an EVENT rather than a break: a long that reached its own
        // expiry and that the VENUE ITSELF says it settled for cash. Anything else falls through and
        // trips — including a vanish on expiry day with no receipt, and a receipt that does not
        // account for the whole position.
        const settled = this.#settlementAbsolving(key, e, a);
        if (settled !== undefined) {
          this.#recordSettlement(settled);
          continue;
        }
        this.killSwitch.trip(
          `reconciliation break at ${key}: engine expected ${e}, broker PULL reported ${a} (delta ${a - e}, tolerance ${this.#tolerance}) — halting paper transmission (ADR-0034 §4, fail-closed)`,
        );
        return; // the first break latches the switch; every subsequent submit refuses "killed"
      }
    }
  }

  settlements(): readonly SettlementRecord[] {
    return [...this.#settlements.values()];
  }

  /**
   * Is this per-key divergence a legitimate EXPIRY CASH SETTLEMENT (kestrel-7o2.24)? Returns the
   * {@link SettlementRecord} to file when it is, `undefined` when it is not — and `undefined` means the
   * caller TRIPS. Every condition below is NECESSARY; the venue's own receipt is the only one that is
   * anywhere near sufficient, and it still does not stand alone.
   *
   * The narrowness IS the safety property. A position vanishing for any other reason — an unexplained
   * broker adjustment, a position closed by someone else on the account, a leg that vanished before it
   * could possibly have expired, a partial disappearance — must still trip, because each of those is a
   * book we no longer agree with the venue about, and that is precisely what the kill-switch is for.
   */
  #settlementAbsolving(key: PositionKey, expected: number, actual: number): SettlementRecord | undefined {
    // (1) NO ORACLE, NO ABSOLUTION. A caller that supplied no settlement source gets the pre-carve-out
    //     behaviour exactly, with no branch it could accidentally fall into.
    if (this.#settlementOf === undefined) return undefined;

    // (2) The position must be GONE — not reduced. A partial disappearance is not a settlement; a
    //     settled contract settles in full.
    if (Math.abs(actual) > this.#tolerance) return undefined;

    // (3) It must have been a LONG. The spike is BUY-only and hold-to-cash-settle; a SHORT that
    //     vanishes is assignment/exercise territory, whose cash flows this receipt does not model.
    //     Fail closed on it rather than guess.
    if (!(expected > this.#tolerance)) return undefined;

    // (4) The leg must be an OPTION WITH A KNOWN EXPIRY, taken from the VENUE's own pre-resolved
    //     contract definition (WALL 2's book, via this session's ledger) — never inferred from a
    //     symbol, a calendar, or the receipt itself. No definition ⇒ no expiry ⇒ no absolution.
    const contract = this.#contractForKey(key);
    if (contract === undefined || contract.kind !== "option") return undefined;
    const expiryAt = expiryInstantUtc(contract.expiry);
    if (expiryAt === undefined) return undefined;

    // (5) The SESSION must actually have REACHED the leg's own expiry, by the clock the DRIVER pinned
    //     (RUNTIME §0 — `now` is tape time, never a wall clock). A position that vanished before its
    //     expiry did not expire, whatever anyone reports about it.
    if (!Number.isFinite(this.now) || this.now < expiryAt) return undefined;

    // (6) THE VENUE'S OWN RECEIPT. The date is a necessary condition, never the evidence: a position
    //     that merely went missing on expiry day is not a settled position.
    const receipt = this.#settlementOf(key);
    if (receipt === undefined) return undefined;

    // (7) The receipt must account for the ENTIRE expected position, exactly. A receipt for a
    //     different size explains a different position, and an unexplained delta is a break.
    if (!Number.isFinite(receipt.qty) || Math.abs(receipt.qty - expected) > this.#tolerance) return undefined;

    // (8) The cash must be REAL and non-negative. `0` is legitimate (expired worthless — the common
    //     0DTE end); a long can only ever RECEIVE at settlement, so a negative is refused, and a
    //     non-finite one is UNKNOWN, which is never a default.
    if (!Number.isFinite(receipt.cashUsd) || receipt.cashUsd < 0) return undefined;

    // (9) The settlement must not PREDATE the expiry it claims to settle.
    if (!Number.isFinite(receipt.settledAt) || receipt.settledAt < expiryAt) return undefined;

    return {
      key,
      qty: expected,
      cashUsd: receipt.cashUsd,
      expiry: contract.expiry,
      settledAt: receipt.settledAt,
      observedAt: this.now,
    };
  }

  /**
   * The VENUE's own contract definition for a position key, taken from THIS session's ledger (every
   * line carries the pre-resolved {@link IbkrContract} it was transmitted against — WALL 2 guarantees
   * it). A key with no ledger line has no definition here, so it can never be absolved.
   *
   * KNOWN NARROWNESS, stated rather than smuggled: a position this session did not ORIGINATE — a
   * resumed session's seeded book, or a leg pushed by the account and never traded here — has no
   * ledger line, so its expiry settlement will TRIP rather than record. That errs toward the halt,
   * which is the right direction, and closing it needs a key→contract lookup the {@link ContractBook}
   * does not expose today (kestrel-7o2.24).
   */
  #contractForKey(key: PositionKey): IbkrContract | undefined {
    for (const r of this.#byRef.values()) {
      if (positionKeyOf(r.intent) === key) return r.contract;
    }
    return undefined;
  }

  /** File the settlement — once per key — and say so out loud. A carve-out that absolves WITHOUT
   * recording is an erased position, which is the very thing `reconcile()` exists to make impossible. */
  #recordSettlement(rec: SettlementRecord): void {
    if (this.#settlements.has(rec.key)) return;
    this.#settlements.set(rec.key, rec);
    this.#log(
      `SETTLEMENT at ${rec.key}: the engine expected ${rec.qty}, the venue reports 0 — the leg reached its own expiry (${rec.expiry}) and the venue posted $${rec.cashUsd} of settlement cash at ${rec.settledAt}. Recorded as a settlement, NOT a reconciliation break (kestrel-7o2.24, ADR-0034 q3)`,
    );
  }

  // ── the inbound pump ────────────────────────────────────────────────────────

  /** Wire the venue's own callbacks onto the CLOSED ORDER vocabulary. Registered once at construction;
   * {@link detach} removes exactly these (never the blanket `removeAllListeners` hammer — the socket
   * is SHARED with the feed face). */
  #attach(): void {
    this.#on(EventName.openOrder, (orderId: number, _c: Contract, _o: Order, _s: OrderState) => {
      // The venue ACKNOWLEDGES the order exists. That — not our `placeOrder` returning — is `place`.
      const rec = this.#byIbId.get(orderId) === undefined ? undefined : this.#byRef.get(this.#byIbId.get(orderId) as string);
      if (rec === undefined || rec.terminal) return;
      this.#ensurePlaced(rec);
      this.#drain();
    });

    this.#on(
      EventName.orderStatus,
      (orderId: number, status: OrderStatus, filled: number, remaining: number, _avg: number) => {
        const ref = this.#byIbId.get(orderId);
        const rec = ref === undefined ? undefined : this.#byRef.get(ref);
        if (rec === undefined || rec.terminal) return;
        rec.remainingQty = remaining;
        void filled; // CUMULATIVE and duplicated by IB — `execDetails` is the fill truth, not this.

        switch (status) {
          case OrderStatus.PendingSubmit:
          case OrderStatus.PreSubmitted:
          case OrderStatus.Submitted:
          case OrderStatus.ApiPending:
          case OrderStatus.PendingCancel:
            this.#ensurePlaced(rec);
            break;
          case OrderStatus.Filled:
            // The fills themselves arrive as `execDetails` (per-execId, so partial + multi both work).
            // This status only closes the record — emitting a `fill` here would DOUBLE-COUNT.
            //
            // And it closes it ONLY on a COMPLETE fill. `filledQty > 0` latched a PARTIAL as filled,
            // after which every remaining execution on the order was dropped as post-terminal — the
            // engine would never learn of the rest of its own fill.
            this.#ensurePlaced(rec);
            if (rec.filledQty >= rec.submittedQty) this.#terminate(rec, "filled");
            break;
          case OrderStatus.Cancelled:
          case OrderStatus.ApiCancelled:
            this.#ensurePlaced(rec);
            this.#emit("cancel", rec, rec.intent.px, `venue status ${status}`);
            this.#terminate(rec, "cancelled");
            break;
          case OrderStatus.Inactive:
            // IB's other reject face: received, then made inactive (rejected / killed by the system).
            this.#ensurePlaced(rec);
            this.#emit("reject", rec, rec.intent.px, `venue status ${status}`);
            this.#terminate(rec, "rejected");
            break;
          case OrderStatus.Unknown:
            // Never a silent shrug: log it, but do NOT invent a terminal state for the order.
            this.#log(`ib orderStatus Unknown for ${rec.ref} [ibOrderId=${orderId}] — no state inferred`);
            break;
          default:
            break;
        }
        this.#drain();
      },
    );

    this.#on(EventName.execDetails, (_reqId: number, _c: Contract, execution: Execution) => {
      // THE FILL TRUTH. Each partial fill carries its own execId, so this is idempotent under IB's
      // replays AND naturally handles partial + multi-fill (one ORDER `fill` per distinct execution).
      const orderId = execution.orderId;
      const execId = execution.execId;
      if (orderId === undefined || execId === undefined) return; // an unattributable execution: never guessed onto an order
      if (this.#seenExecs.has(execId)) return; // replay — never double-count
      const ref = this.#byIbId.get(orderId);
      const rec = ref === undefined ? undefined : this.#byRef.get(ref);
      if (rec === undefined) return; // not ours (another client id on the shared account)

      const shares = execution.shares;
      const price = execution.price;
      if (shares === undefined || price === undefined || shares <= 0) return; // incomplete: never completed by hand

      this.#seenExecs.add(execId);
      rec.execIds.add(execId); // so the later commissionReport (which names only the execId) finds its order

      // A fill on an order we have ALREADY LATCHED TERMINAL (rejected/cancelled/filled) is a BREAK, not
      // an event: the venue executed something we believe is dead. Broker truth is still truth, so the
      // shares FOLD into the position — but no ORDER event is emitted after a terminal (replay
      // stability), and the break trips below.
      const postTerminal = rec.terminal;

      rec.filledQty += shares;
      rec.fillNotional += shares * price;
      rec.avgFillPx = rec.fillNotional / rec.filledQty;

      // The BROKER's own execution folds into the BROKER's own position (its truth, not ours).
      const key = positionKeyOf(rec.intent);
      const signed = rec.intent.side === "buy" ? shares : -shares;
      this.#execPositions.set(key, (this.#execPositions.get(key) ?? 0) + signed);
      // This fill CONSUMED that much of the working sell's reservation — the long it spoke for is gone
      // from broker truth now, so holding the reservation too would double-count it.
      this.#release(rec, shares);

      if (!postTerminal) {
        // Causal order: a fill can never precede its own `place`, even if IB delivers it that way.
        this.#ensurePlaced(rec);
        // The ORDER `fill` the engine OBSERVES — at the price the VENUE actually printed.
        this.#emit("fill", rec, price, undefined, shares);
        // TERMINAL only when the venue has executed EVERYTHING we submitted. Latching on the first
        // partial (`filledQty > 0`) would strand the remainder: the record goes terminal and every
        // later execution on it is dropped as post-terminal.
        if (rec.filledQty >= rec.submittedQty) this.#terminate(rec, "filled");
      } else {
        this.#log(
          `POST-TERMINAL execution ${execId} on ${rec.ref} (phase=${rec.phase}) — folded into broker truth, NOT emitted`,
        );
      }

      // FAIL CLOSED AT OBSERVATION — not on an opt-in reconcile() nobody calls.
      this.#tripOnObservation(rec, execId, postTerminal);
      this.#drain();
    });

    this.#on(EventName.commissionReport, (report: CommissionReport) => {
      // NOT an ORDER event — the vocabulary is CLOSED and a commission is not an order action. It
      // folds onto the LEDGER, where the reconciliation reads the venue's full account of the trade.
      const execId = report.execId;
      const commission = report.commission;
      if (execId === undefined || commission === undefined) return;
      if (this.#seenCommissions.has(execId)) return; // replay
      const rec = [...this.#byRef.values()].find((r) => this.#execBelongsTo(r, execId));
      if (rec === undefined) return;
      this.#seenCommissions.add(execId);
      rec.commissionUsd += commission;
    });

    this.#on(EventName.error, (err: Error, code: number, reqId?: number) => {
      // IB multiplexes ORDER-scoped messages onto the same `error` event the transport listens to. A
      // fatal one for an order id we own is a CLAIM about that order — but NOT every such claim is a
      // venue-confirmed terminal.
      if (reqId === undefined || isNonFatalError(code, err)) return;
      const ref = this.#byIbId.get(reqId);
      const rec = ref === undefined ? undefined : this.#byRef.get(ref);
      if (rec === undefined || rec.terminal) return;

      // ROUTE A: a CANCEL-REJECTION (10147/10148 "OrderId ... cannot be cancelled, state: ...") means
      // the order is STILL LIVE at the venue. Terminating/releasing on it frees a reservation for a
      // live order — reopening the exact naked short blocker 1 closed. An error of UNKNOWN/still-live
      // fate must NEVER free a reservation or latch a terminal: log it and await the venue's own
      // terminal orderStatus, the single source of a release (the round-1 fix's own principle for
      // cancel(), applied to errors too).
      if (isStillLiveOrderError(code, err)) {
        this.#log(
          `ib error ${code} on ${rec.ref} [ibOrderId=${reqId}] is a CANCEL-REJECTION — the order is STILL LIVE, not terminal. Keeping its reservation and awaiting the venue's terminal status (Route A, fail-closed): ${err.message}`,
        );
        return;
      }
      // A routine CANCEL (IB 202 "Order Canceled - reason:") is a CANCEL, not a REJECT — mapping it onto
      // reject makes the Blotter learn the wrong terminal. Emit the right one.
      if (code === IB_ORDER_CANCELLED) {
        this.#emit("cancel", rec, rec.intent.px, `ib error ${code}: ${err.message}`);
        this.#terminate(rec, "cancelled");
        this.#drain();
        return;
      }
      // A genuine order REJECT (201, etc.): terminal, and it releases the reservation.
      this.#emit("reject", rec, rec.intent.px, `ib error ${code}: ${err.message}`);
      this.#terminate(rec, "rejected");
      this.#drain();
    });

    this.#on(EventName.position, (_account: string, contract: Contract, pos: number, _avgCost?: number) => {
      // The ACCOUNT's own statement about itself — the highest authority there is. It REBASES this
      // key: `pos` becomes the new BASELINE, and the execution fold RESTARTS from it (the statement
      // already contains everything printed up to it). It does NOT overwrite the fold — that is how a
      // fill we had already OBSERVED got silently discarded from the risk input (blocker 2), and a
      // discarded sell-fill reads as a long we no longer own.
      const key = positionKeyFromContract(contract);
      if (key === undefined) return;

      // ROUTE B: SEQUENCE the rebase against what has already been folded. reqPositions() fires at
      // construction and IB's snapshot is ASYNC, so a push carrying a PRE-FILL snapshot can land AFTER
      // we have already folded that fill. If it did, `pos` re-asserts the baseline the executions were
      // folded ON TOP of — accepting it (and zeroing the fold) would RE-INFLATE a position we have sold
      // down and MASK the trip. Detect that exact staleness — the push equals the prior baseline while
      // executions have been folded since — and IGNORE the rebase, keeping the more-conservative
      // (more-short) exec view. A fresh push (a real change) differs from the prior baseline and rebases
      // normally. Ignoring a re-inflating push only ever errs toward LESS long / more short — fail-closed.
      const foldSince = this.#execPositions.get(key) ?? 0;
      const priorBaseline = this.#venueBaseline.has(key) ? (this.#venueBaseline.get(key) as number) : (this.#seed[key] ?? 0);
      if (foldSince !== 0 && pos === priorBaseline) {
        this.#log(
          `STALE position push for ${key}: pos ${pos} equals the prior baseline while ${foldSince} has already been folded on top — IGNORING the rebase so a sold-down position is not re-inflated (Route B, fail-closed)`,
        );
      } else {
        this.#venueBaseline.set(key, pos);
        this.#execPositions.set(key, 0);
      }

      // ROUTE D: the break scan runs at EVERY observation of broker truth — the position push included,
      // not only inside execDetails. The position handler is the venue's statement of record, and a
      // guard nobody invokes at this observation point is decoration. A negative baseline (a naked
      // short, however it arrived) trips here.
      this.#scanBrokerTruthForBreaks();
      this.#drain();
    });
  }

  detach(): void {
    for (const [event, listener] of this.#registered) {
      this.#client.removeListener(event, listener);
    }
    this.#registered = [];
  }

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

  /** Does `execId` belong to this record? (The commission report names only the execution.) */
  #execBelongsTo(rec: MutableRecord, execId: string): boolean {
    return rec.execIds.has(execId);
  }

  /** Emit `place` at most ONCE per ref, whatever order the venue's callbacks arrive in — so a `fill`
   * that beats its acknowledgment still reads as place→fill, and a replay adds nothing. */
  #ensurePlaced(rec: MutableRecord): void {
    if (rec.placed) return;
    rec.placed = true;
    rec.phase = "working";
    this.#emit("place", rec, rec.intent.px);
  }

  /** Latch a terminal phase — a terminal order never emits again (replay stability) — and RELEASE
   * whatever this order still had reserved. A cancelled/rejected sell never sold anything, so the long
   * it spoke for is sellable again; a filled one has had its reservation consumed fill-by-fill, and
   * this releases any residue (e.g. a partial that the venue then cancelled). */
  #terminate(rec: MutableRecord, phase: Extract<OrderPhase, "filled" | "cancelled" | "rejected">): void {
    rec.phase = phase;
    rec.terminal = true;
    this.#release(rec, rec.reservedQty);
  }

  /** Give back `qty` of this order's working-SELL reservation (never more than it still holds). */
  #release(rec: MutableRecord, qty: number): void {
    if (rec.reservedQty <= 0 || qty <= 0) return;
    const released = Math.min(qty, rec.reservedQty);
    rec.reservedQty -= released;
    const key = positionKeyOf(rec.intent);
    const next = (this.#workingSellQty.get(key) ?? 0) - released;
    if (next > 0) this.#workingSellQty.set(key, next);
    else this.#workingSellQty.delete(key);
  }

  /**
   * FAIL CLOSED **AT OBSERVATION** (kestrel-7o2.8, blocker 3). The deepest lesson of the adversarial
   * gate: {@link IbkrPaperBroker.reconcile} existed, was tested, and was NEVER CALLED in production —
   * so an over-fill left `positions()` at −3 (a naked short in this module's OWN accounting) while the
   * kill-switch stayed un-tripped and the NEXT submit was still accepted. A guard nobody invokes is
   * decoration. So the trip fires HERE, inside the `execDetails` handler, the moment the venue's own
   * report proves a break:
   *
   *  - a POST-TERMINAL fill (the venue executed an order we had latched rejected/cancelled/filled);
   *  - an OVER-FILL (the venue executed MORE than we submitted);
   *  - ANY key driven NEGATIVE — an uncovered short, however it got there (via {@link
   *    IbkrPaperBroker.scanBrokerTruthForBreaks}, which ALSO runs at the position-push observation).
   *
   * `reconcile()` survives as an explicit AUDIT; it is simply no longer the only thing that can trip.
   */
  #tripOnObservation(rec: MutableRecord, execId: string, postTerminal: boolean): void {
    if (this.killSwitch.tripped) return;

    if (postTerminal) {
      this.killSwitch.trip(
        `reconciliation break (POST-TERMINAL FILL) on ${rec.ref} [ibOrderId=${rec.ibOrderId}, execId=${execId}]: the venue EXECUTED an order this face had already latched ${rec.phase} — the BROKER's report is authoritative, so this is a break, not an event. Halting paper transmission (ADR-0034 §4, fail-closed)`,
      );
      return;
    }
    if (rec.filledQty > rec.submittedQty + this.#tolerance) {
      this.killSwitch.trip(
        `reconciliation break (OVER-FILL) on ${rec.ref} [ibOrderId=${rec.ibOrderId}]: engine submitted ${rec.submittedQty}, broker reported executing ${rec.filledQty} — the BROKER's report is authoritative; halting paper transmission (ADR-0034 §4, fail-closed)`,
      );
      return;
    }
    this.#scanBrokerTruthForBreaks();
  }

  /**
   * The BREAK SCAN over broker truth (kestrel-7o2.8, Route D). Runs at EVERY observation of broker
   * truth — the `execDetails` fold AND the `position` push, the venue's statement of record. "A guard
   * nobody invokes is decoration" applies to EVERY observation point, not just one.
   *
   * A key driven NEGATIVE is an UNCOVERED SHORT. Under the no-uncovered-short POLICY (default ON — the
   * 0DTE book), that is the one thing no code path may leave standing, so it trips. When the policy is
   * OFF (an author deliberately sells puts, kestrel-buos), a negative key is a LEGITIMATE budgeted
   * short — the submit-time bounded-risk wall already vetted it — so mere negativity is not a break and
   * does not trip here.
   */
  #scanBrokerTruthForBreaks(): void {
    if (this.killSwitch.tripped) return;
    if (!this.#noUncoveredShort) return; // shorts are allowed when the policy is off — not a break
    for (const [key, qty] of Object.entries(this.positions())) {
      if (qty < -this.#tolerance) {
        this.killSwitch.trip(
          `reconciliation break (NEGATIVE POSITION) at ${key}: the broker's own report now leaves ${qty} — an UNCOVERED SHORT, which the no-uncovered-short policy (ON) forbids. Halting paper transmission (fail-closed)`,
        );
        return;
      }
    }
  }

  /** Append one seq-less ORDER event, mirroring `SimFillEngine`'s `#emit` field-for-field. The owner
   * (the emitted stream, via the injected `drain`) stamps `seq`; the clock is the `now` the driver
   * pinned on this gate (RUNTIME §0 — never a wall clock). */
  #emit(action: OrderAction, rec: MutableRecord, px: number, reason?: string, qty?: number): void {
    const intent = rec.intent;
    this.#events.push({
      ts: this.now,
      stream: "ORDER",
      type: action,
      order_id: intent.ref,
      ...(intent.plan !== undefined ? { plan: intent.plan } : {}),
      ...(intent.plan_instance !== undefined ? { plan_instance: intent.plan_instance } : {}),
      instrument: intent.instrument,
      side: intent.side,
      qty: qty ?? intent.qty,
      ...(intent.strike !== undefined ? { strike: intent.strike } : {}),
      ...(intent.right !== undefined ? { right: intent.right } : {}),
      px,
      ...(reason !== undefined ? { reason } : {}),
    });
  }

  /** Register a narrowly-typed listener and remember it for {@link detach}. */
  #on<A extends unknown[]>(event: EventName, listener: (...args: A) => void): void {
    const wrapped = listener as unknown as IbListener;
    this.#client.on(event, wrapped);
    this.#registered.push([event, wrapped]);
  }
}

/** The ledger line's mutable interior (the public {@link IbkrOrderRecord} is a read-only projection). */
interface MutableRecord {
  readonly ref: string;
  readonly ibOrderId: number;
  readonly intent: OrderIntent;
  readonly contract: IbkrContract;
  readonly submittedQty: number;
  filledQty: number;
  /** Running sum of `shares x price` — the size-weighted average's numerator. */
  fillNotional: number;
  avgFillPx: number | undefined;
  commissionUsd: number;
  remainingQty: number | undefined;
  phase: OrderPhase;
  placed: boolean;
  terminal: boolean;
  /** The venue's `placeOrder` THREW after this record was created — the order's fate is UNKNOWN (it may
   * be LIVE at the venue). Its correlation and reservation are KEPT rather than erased, so a fill still
   * folds and the long stays guarded (Route C). Informational; the guarantees come from the retained
   * `#byRef`/`#byIbId`/reservation, not from this flag. */
  unknownFate?: boolean;
  /** How much of this order's SELL quantity is still RESERVED against its key (never-naked's memory).
   * `qty` at submit for a sell, `0` for a buy; drawn down by each fill, zeroed at the terminal. */
  reservedQty: number;
  readonly execIds: Set<string>;
}

// ─────────────────────────────────────────────────────────────────────────────
// Pure helpers.
// ─────────────────────────────────────────────────────────────────────────────

/** The exact IB {@link Contract} to transmit against — built from the GATEWAY's own definition
 * (7o2.6), not hand-rolled. `conId` is the authoritative handle; the option carries the VENUE's
 * MULTIPLIER (never an assumed 100x) and its own OCC local symbol. NO price rides on it, ever. */
export function ibContractOf(c: IbkrContract): Contract {
  if (c.kind === "option") {
    return {
      conId: c.conId,
      symbol: c.symbol,
      secType: SecType.OPT,
      exchange: c.exchange,
      currency: c.currency,
      localSymbol: c.localSymbol,
      lastTradeDateOrContractMonth: c.expiry,
      strike: c.strike,
      right: c.right === "C" ? OptionType.Call : OptionType.Put,
      multiplier: c.multiplier,
      ...(c.tradingClass === undefined ? {} : { tradingClass: c.tradingClass }),
    };
  }
  return {
    conId: c.conId,
    symbol: c.symbol,
    secType: SecType.STK,
    exchange: c.exchange,
    currency: c.currency,
    localSymbol: c.localSymbol,
    ...(c.primaryExchange === undefined ? {} : { primaryExch: c.primaryExchange }),
  };
}

/** The {@link PositionKey} for an IB-reported contract (the `position` push). Fail-closed: a contract
 * we cannot key (no symbol, or a half-specified option) is IGNORED rather than keyed by a guess — an
 * invented key would corrupt the very reconciliation it is meant to feed. */
function positionKeyFromContract(c: Contract): PositionKey | undefined {
  const symbol = c.symbol;
  if (symbol === undefined || symbol.length === 0) return undefined;
  if (c.secType === SecType.OPT) {
    const strike = c.strike;
    const right = c.right === OptionType.Call ? "C" : c.right === OptionType.Put ? "P" : undefined;
    if (strike === undefined || right === undefined) return undefined;
    return positionKeyOf({ instrument: symbol, strike, right });
  }
  return positionKeyOf({ instrument: symbol });
}

/** A seq-less ORDER event — the exact `NewBusEvent` variant `SimFillEngine` appends. */
type NewOrderEvent = Extract<NewBusEvent, { stream: "ORDER" }>;

// ─────────────────────────────────────────────────────────────────────────────
// Construction + the mode-keyed factory registration.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Build the IBKR **PAPER** order face (kestrel-7o2.8). REFUSES `mode: "live"` at construction — live
 * routing is 7o2.10 and requires the human-signed {@link LiveArm}; a config flag can never grant it.
 */
export function ibkrBroker(cfg: IbkrConfig, deps: IbkrBrokerDeps): IbkrBroker {
  return new IbkrPaperBroker(cfg, deps);
}

/** The name the IBKR paper face registers itself under in the mode-keyed `makeGate` factory. */
export const IBKR_PAPER_VENUE = "ibkr";

/**
 * Register a constructed IBKR paper face as the venue `makeGate("paper", { venue: IBKR_PAPER_VENUE })`
 * serves (kestrel-7o2.8). This is the composition-root wiring, and it lives HERE rather than in
 * `adapters/broker.ts` on purpose: the core seam must never import a socket transport, so the `sim`
 * gate keeps its zero-dependency, byte-identical path. Registration is PAPER-only by construction —
 * `makeGate("live", …)` still refuses with `LiveGateRefused` no matter what is registered.
 */
export function installIbkrPaperVenue(broker: IbkrBroker): IbkrBroker {
  registerPaperVenue(IBKR_PAPER_VENUE, () => broker);
  return broker;
}
