/**
 * # frame/render — the pure Frame renderer (ADR-0008/0009, DATE-BLIND) + the cockpit lead block
 *
 * Two entry points the stepped day runner calls: {@link renderBriefing} materializes the OPEN
 * **keyframe** (full briefing), {@link renderWakeDelta} materializes a WAKE **delta frame**
 * (only what changed since the author's last vantage). Both return a single string, and both
 * **LEAD** with the non-configurable SAFETY / CONTROL **cockpit kernel** (built in code and
 * prepended OUTSIDE any pane/View selection — see {@link renderKernel}).
 *
 * ## The invariants this renderer lives inside (ADR-0009 — not up for grading)
 * - **Invents no value.** Every number printed is a field on the input; an absent/UNKNOWN
 *   field renders as an explicit `—`/`none`/`UNKNOWN` (never a guess). Layout (column position,
 *   axis bounds derived from the data's own extremes, column widths) is computed, but no *value* is.
 * - **The cockpit kernel LEADS every frame and is non-configurable.** It is built in code and
 *   prepended before any pane/View is selected; it can be neither dropped nor reordered by
 *   configuration, and a reserved pane id (`kernel`/`cockpit`) is rejected fail-closed
 *   ({@link ./types.ts RESERVED_PANE_IDS}, {@link ./types.ts assertPaneIdAllowed}). A fail-closed
 *   lead guard ({@link assertKernelLeads}) throws if the kernel is ever not the first block.
 * - **Absent-not-hidden.** Every one of the kernel's 8 fixed-order sections is ALWAYS present —
 *   an empty section renders as an explicit `none`/`UNKNOWN`, never silently dropped.
 * - **Date-blind.** No date/day/epoch token is ever emitted — only relative time and the HH:MM
 *   ET clock the input carries. The wake deadline is a RELATIVE `T-Nm to close` duration, never
 *   a wall-clock/date.
 * - **Deterministic / pure.** No wall clock, no RNG on the render path: the same inputs render a
 *   byte-identical string.
 *
 * ## Parameterized by a Rendering (CONTEXT: Rendering)
 * A Rendering has two parameters — a **format** and the **tokenizer** its token costs are measured
 * under. The SHIPPED format surface is the text screen and only the text screen: {@link RenderFormat}
 * is `ascii | unicode | md`. {@link RenderOptions} threads both parameters through, defaulting the
 * format to `ascii` and the tokenizer to a documented chars/4 estimate.
 *
 * The structural `json` Rendering IS materialized (ADR-0052 §2) — but as its OWN adapter
 * ({@link ./render-json.ts}), NOT as a text screen: this text renderer refuses `json` via
 * {@link assertTextFormat} (routing it here would silently downgrade a structural ask to text). The
 * remaining structural name (`html`) stays the render-core charter's (`src/render`) — speakable
 * ({@link RequestedFormat}) but refused at the boundary ({@link assertFormatMaterialized}) rather than
 * faked. That refusal is the whole contract: callers MUST NOT answer an unmaterialized ask themselves —
 * the prompt harness once "answered" it by silently rendering `ascii` while the run's ConfigId still
 * recorded `json` (kestrel-4gl.26).
 *
 * ## The tape row (rendering-variants.md candidate 2 — the incumbent, first real impl)
 * A candlestick rotated 90°: price runs horizontally, time flows down, each row is a sideways
 * candle — `─` wick spanning the bucket's low→high, `█` body spanning open→close — and the
 * candle's **column position encodes its price level**. The axis header stamps the level range
 * and the anchor clock once (levels are not re-spent per row); a fresh frame re-stamps a fresh
 * anchor (rescale = new anchor line). This is a graded hypothesis, never blessed by taste
 * (ADR-0009); it is the incumbent only because iteration kept this orientation.
 */

import {
  KERNEL_DELTA_SENTINEL,
  KERNEL_LEAD_SENTINEL,
  KERNEL_SECTION_LABELS,
  KernelHonestyError,
  assertClaimHonest,
} from "./types.ts";
import type {
  Brief,
  BriefingInput,
  Claim,
  EngineAction,
  Kernel,
  Mandate,
  OwnerAct,
  Position,
  RestingOrder,
  SizingHeadroom,
  VehicleHealth,
  WakeDeltaInput,
  WakeRef,
} from "./types.ts";
import { assertBriefHashHonest } from "./brief.ts";
import { DASH, isSpotLeg, num, pad, px, signedInt, toClose, usd } from "./format.ts";
import {
  actionToken,
  bucketEngineLog,
  collapseActionTokens,
  restingLabel,
  walkKernel,
  type BudgetEnvelopeArg,
  type KernelSink,
} from "./kernel-walk.ts";
import {
  materializePanes,
  resolveView,
  type PaneBuildContext,
  type ViewSelection,
} from "./pane-catalog.ts";
import type { SeatId } from "./seat.ts";
import { DEFAULT_TOKENIZER } from "../render/index.ts";
import type { Tokenizer } from "../render/index.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Rendering parameters (CONTEXT: Rendering) — reuse the render-core charter surface
//
// A Rendering is parameterized by a `format` + a `tokenizer`. These types mirror `src/render`'s
// documented charter (that barrel is charter-only and exports nothing yet); the frame renderer
// materializes the concrete TEXT Rendering here and threads both parameters — it does NOT fork a
// second render engine.
// ─────────────────────────────────────────────────────────────────────────────

/** A Rendering's **text-screen format** (CONTEXT: Rendering) — the token-efficient text screen surface
 * THIS renderer produces. `md` is still text.
 *
 * The union names ONLY the TEXT screens this renderer materializes (kestrel-4gl.26): a type that admits
 * a format this renderer cannot produce is an invitation to a silent downgrade, and the harness once
 * took it. The structural `json` Rendering IS materialized ({@link MaterializedFormat},
 * {@link ./render-json.ts}) but is deliberately NOT in this union — it is not a text screen, and the
 * text path refuses it ({@link assertTextFormat}); it is served through the json adapter. `html`
 * remains the render-core charter (`src/render`), refused at the boundary ({@link assertFormatMaterialized})
 * until it exists, never quietly served as a text screen wearing a structured label. */
export type RenderFormat = "ascii" | "unicode" | "md";

/** A **materialized** Rendering format — one the engine can actually produce from a Frame (ADR-0052
 * §2). The text screen ({@link RenderFormat}: `ascii`/`unicode`/`md`) PLUS the structural `json`
 * Rendering ({@link ./render-json.ts}). This is the servable surface: {@link assertFormatMaterialized}
 * vouches for exactly these and refuses everything else (today: `html`, still charter-only). `json` is
 * NOT a {@link RenderFormat} — it is not a text screen, so the TEXT render path ({@link assertTextFormat})
 * refuses it; it is served through the json adapter, never faked as a text screen wearing a json label. */
export type MaterializedFormat = RenderFormat | "json";

/** A format name as it arrives from OUTSIDE the renderer — an authored config, a CLI flag, a JSON
 * file — i.e. before {@link assertFormatMaterialized} has vouched for it. Wider than
 * {@link MaterializedFormat} on purpose: the still-unmaterialized `html` is *speakable* (a config may
 * carry it, and the CLI documents it), it is simply not *servable* — a request for it fails closed. */
export type RequestedFormat = MaterializedFormat | "html";

/** The render **tokenizer** contract + its chars/4 default now live in ONE canonical home — the
 * render barrel {@link ../render/index.ts} (kestrel-2fab) — so the renderer and the pane-budget
 * guard cannot drift apart. Re-exported here for callers that already reach the renderer surface. */
export { DEFAULT_TOKENIZER };
export type { Tokenizer };

/** The two Rendering parameters, both optional (format defaults to `ascii`, tokenizer to the
 * documented chars/4 estimate). A frame renderer stays a pure function of (Frame, RenderOptions).
 *
 * `format` is a {@link RequestedFormat} — what the caller ASKED for — because this interface is the
 * renderer's outer boundary and a caller may honestly ask for a format that does not exist yet. What
 * comes back out is a {@link RenderFormat}: the ask is checked, not coerced. */
export interface RenderOptions {
  readonly format?: RequestedFormat;
  readonly tokenizer?: Tokenizer;
  /** The active View (kestrel-4gl / View-wiring): SELECTS + ORDERS the non-kernel body panes from
   * the pane catalog, honoring its optional token budget. Absent ⇒ the frame kind's DEFAULT View
   * (byte-identical to the pre-catalog hardcoded pane set). The kernel is never a View pane — it
   * LEADS non-configurably, outside this selection. Unknown pane id / over-budget ⇒ fail-closed. */
  readonly view?: ViewSelection;
  /** The acting pod {@link SeatId} for this frame (ADR-0041 §2 / A1, kestrel-wa0j.26). Threaded into
   * {@link resolveView} so a seat with a founder seed for this frame kind reads its graded founder View
   * when NO `view` was authored (the precedence: authored View > seat founder View > phase default).
   * Absent ⇒ no seat is consulted ⇒ byte-identical to before the role axis existed (no-churn). The
   * config opt-in that decides whether a driver supplies this at all lives in `AgentConfig.seatViews`. */
  readonly seat?: SeatId;
}

/** The measured cockpit-kernel block: its text + lines, plus the real token cost under the (labelled)
 * tokenizer and the format it was materialized in. */
export interface KernelPane {
  readonly text: string;
  readonly lines: readonly string[];
  readonly tokenCost: number;
  readonly tokenizer: string;
  readonly format: RenderFormat;
}

/** The token-efficient TEXT screen formats — the {@link RenderFormat} surface {@link assertTextFormat}
 * vouches for. `json` is materialized but is NOT here: it is a STRUCTURAL Rendering, not a text screen. */
export const TEXT_RENDER_FORMATS: readonly RenderFormat[] = Object.freeze(["ascii", "unicode", "md"] as const);

/** Every **materialized** (servable) Rendering format (ADR-0052 §2): the text screens PLUS `json`. The
 * runtime twin of {@link MaterializedFormat}, and the ONE list {@link assertFormatMaterialized} checks
 * against — `json` moved here from speakable-but-not-servable when its adapter landed
 * ({@link ./render-json.ts}); `html` is still absent (charter-only) and stays refused. */
export const MATERIALIZED_FORMATS: readonly MaterializedFormat[] = Object.freeze([
  "ascii",
  "unicode",
  "md",
  "json",
] as const);

const TEXT_FORMATS: ReadonlySet<string> = new Set<string>(TEXT_RENDER_FORMATS);
const MATERIALIZED_FORMAT_SET: ReadonlySet<string> = new Set<string>(MATERIALIZED_FORMATS);

/**
 * The fail-closed format boundary: vouch for a format the engine can MATERIALIZE, or REFUSE it.
 *
 * This is the ONLY sanctioned way to turn a {@link RequestedFormat} (or a raw string off a config)
 * into a {@link MaterializedFormat}. Exported (kestrel-4gl.26) precisely so no caller has to invent
 * its own answer for an unmaterialized format — the harness once invented one, and the one it invented
 * was a silent downgrade to `ascii` (the fail-OPEN mirror of this guard). There is one honest answer
 * and it lives here: refuse, naming the format and the surface that does exist. Since ADR-0052 §2 the
 * structural `json` Rendering IS materialized ({@link ./render-json.ts}), so it PASSES; `html` remains
 * charter-only and is still refused. A caller that then wants a TEXT screen must narrow further via
 * {@link assertTextFormat} (json is materialized but is not a text screen).
 */
export function assertFormatMaterialized(format: string): asserts format is MaterializedFormat {
  if (!MATERIALIZED_FORMAT_SET.has(format)) {
    throw new KernelHonestyError(
      `Rendering format "${format}" is not materialized in this engine (materialized: ` +
        `${MATERIALIZED_FORMATS.join(", ")}). The remaining structural surface (html) is the render-core ` +
        "charter (src/render); an unmaterialized format fails closed rather than fake a materialized Rendering.",
    );
  }
}

/**
 * The fail-closed TEXT-SCREEN boundary: vouch for a text-screen {@link RenderFormat}, or REFUSE it.
 *
 * The text render path ({@link renderKernelPane}, the kernel-delta ops) produces a text screen and ONLY
 * a text screen. `json` is materialized ({@link assertFormatMaterialized}) but is NOT a text screen —
 * routing it here would silently downgrade a structural ask to text (the exact fail-OPEN kestrel-4gl.26
 * closed). So this narrower guard refuses `json` (naming the json adapter) AND `html`, and passes only
 * the text screens. A caller with a materialized `json` ask serves it through {@link ./render-json.ts},
 * never through the text path.
 */
export function assertTextFormat(format: string): asserts format is RenderFormat {
  if (!TEXT_FORMATS.has(format)) {
    const structural =
      format === "json"
        ? ' The `json` Rendering IS materialized, but it is structural, not a text screen — serve it through the json adapter (src/frame/render-json.ts renderBriefingJson/renderWakeDeltaJson), never through the text path.'
        : "";
    throw new KernelHonestyError(
      `Rendering format "${format}" is not a text screen in this renderer (only: ` +
        `${TEXT_RENDER_FORMATS.join(", ")}).${structural}`,
    );
  }
}

// The formatting primitives (`px`/`num`/`money`/`pad`/`toClose`/`DASH`) and the body-pane builders
// (tape/levels/chain/instruments/macro/acting) now live in {@link ./format.ts} + {@link
// ./pane-catalog.ts} (the single-source catalog). The kernel below still formats through the shared
// primitives; the body panes are materialized from the catalog under a resolved View.

// ─────────────────────────────────────────────────────────────────────────────
// The non-configurable SAFETY / CONTROL cockpit kernel — LEADS every frame
//
// The single canonical renderer for the immutable cockpit block (mutual-visibility). It LEADS
// every frame and is NOT a configurable/registry pane. Renders all 8 sections in a FIXED order,
// every section ALWAYS present with an explicit `none`/`UNKNOWN` (absent-not-hidden — a section is
// never silently dropped). Deterministic + PURE: no wall-clock, no RNG. The wake deadline is a
// date-blind RELATIVE `T-Nm to close` DURATION (never a wall-clock/absolute time). A predictor/
// regime claim MUST be an honest MODEL Field carrying source + modelVer + confidence — an OBS/CALC
// "claim" (or a MODEL claim missing its receipt/confidence) is REFUSED (the honesty guard; degraded
// state shows as degraded, never faked as a model claim).
// ─────────────────────────────────────────────────────────────────────────────

/** The engine-log bucket set + classification and the action id token are single-source in
 * {@link ./kernel-walk.ts} (shared with the ARM adapter). Canonical's action token is `id@seqN` +
 * ` (reason)` when present; its resting label vocabulary is the UPPERCASE `LIVE`/`LIVE clamped` form. */
const CANONICAL_ACTION_STYLE = { seqPrefix: "seq", reasonSpace: true } as const;
const CANONICAL_RESTING_LABELS = { live: "LIVE", liveClamped: "LIVE clamped", clamped: "clamped", off: "off" } as const;

/** Render the wake deadline (minutes-to-close, a date-blind DURATION) as a relative
 * `T-Nm to close`. `null` ⇒ `UNKNOWN`. NEVER a wall-clock / absolute time (date-blind). */
function kernelDeadlineStr(deadline: number | null): string {
  if (deadline === null || !Number.isFinite(deadline)) return "UNKNOWN";
  return `T-${Math.round(deadline)}m to close`;
}

/**
 * The **sizing headroom** cell-line (kestrel-m9i.32) — the MAX fillable size the remaining-R budget
 * admits for the exec instrument, so a model sizes WITHIN the bounded-risk envelope (bounded risk =
 * FULL COST BASIS, ADR-0017: `qty × entry_px × mult`) instead of discovering the ceiling by a SILENT
 * fire-time clamp. `maxUnits` = `floor(remainingUsd / basisPerUnit)`. Absent-not-hidden: no sizing ⇒
 * `UNKNOWN`; an unbuildable basis (dark price / an option premium not cleanly surfaced) shows the
 * remaining-$ ceiling + a basis-rule note, never a fabricated cap. The moving values ride stable
 * `field` anchors (delta-encoding substrate); the units word + separators are skeleton. */
function sizingLine(sizing: SizingHeadroom | null | undefined): KernelCellLine {
  if (sizing === undefined || sizing === null) return [sk("  sizing: UNKNOWN (no sizing headroom)")];
  const unitAbbr = sizing.unit === "shares" ? "sh" : "ct";
  // Buildable basis ⇒ the concrete `max ~N units` cue the model sizes against.
  if (sizing.maxUnits !== null && sizing.basisPerUnit !== null) {
    return [
      sk("  sizing: max ~"),
      fld("sizing.max_units", String(sizing.maxUnits)),
      sk(` ${sizing.unit}  (basis `),
      fld("sizing.basis_per_unit", num(sizing.basisPerUnit)),
      sk(`/${unitAbbr} vs remaining 1R $`),
      fld("sizing.remaining_usd", num(sizing.remainingUsd)),
      sk(")"),
    ];
  }
  // Unbuildable basis (dark spot, or an option premium not surfaced): the remaining-$ ceiling + the
  // cost-basis rule, so the model still knows the envelope is notional (never a silent guess).
  const note = sizing.note !== undefined ? ` — ${sizing.note}` : "";
  return [
    sk("  sizing: max ~"),
    fld("sizing.max_units", "UNKNOWN"),
    sk(` ${sizing.unit}  (remaining 1R $`),
    fld("sizing.remaining_usd", num(sizing.remainingUsd)),
    sk(`${note})`),
  ];
}

// ── The kernel as anchored CELLS (kestrel-312 — delta-encoding substrate) ───────────────────────
// The kernel is built once as an ordered list of cell-lines; each line is a sequence of SEGMENTS.
// A segment is either `skel` (byte-stable skeleton: the sentinel, `frame=`, section labels, key
// names, separators, and the `none`/`UNKNOWN` placeholders) or `field` (a moving value carried
// under a STABLE anchor). The full kernel text is the concatenation of every segment — so
// {@link buildKernelLines} stays byte-identical — while the SAME structure yields a delta: only the
// `field` segments whose value MOVED are transmitted (the skeleton lives in the reader's cache).
// This is the single source of truth: full render, skeleton, and delta all read the same cells, so
// a delta can never silently drop a section the full render carries (the fail-closed guarantee).

/** One segment of a kernel line: byte-stable skeleton, or a moving value under a stable anchor. */
type KernelSeg =
  | { readonly kind: "skel"; readonly text: string }
  | { readonly kind: "field"; readonly anchor: string; readonly text: string };
/** One kernel line as an ordered segment list; its text is the concatenation of the segments. */
type KernelCellLine = readonly KernelSeg[];

/** A byte-stable skeleton segment (never transmitted in a delta — held in the reader's cache). */
const sk = (text: string): KernelSeg => ({ kind: "skel", text });
/** A moving-value segment under a stable `anchor` (transmitted in a delta iff its value moved). The
 * `anchor` MUST be free of spaces/newlines (the wire splits an anchor from its value on the first
 * space); the delta encoder fails safe to a keyframe if that ever breaks. */
const fld = (anchor: string, text: string): KernelSeg => ({ kind: "field", anchor, text });
/** The text a line renders to — the concatenation of its segments (skeleton + field values). */
const lineText = (l: KernelCellLine): string => l.map((s) => s.text).join("");

/**
 * The canonical-text {@link KernelSink} — {@link walkKernel}'s adapter that materializes the kernel
 * as anchored CELL-LINES: the lead sentinel + a `frame=<kind>` line + the 8 fixed-order
 * {@link ./types.ts KERNEL_SECTION_LABELS} sections, each ALWAYS present (explicit `none`/`UNKNOWN`
 * when empty — absent-not-hidden is enforced by the walk always CALLING the section method).
 * {@link lineText} over these reproduces the exact canonical kernel bytes; the `field` anchors are
 * the delta-encoding substrate. Refuses a dishonest brief-hash / claim (fail-closed).
 */
class CanonicalKernelSink implements KernelSink {
  readonly out: KernelCellLine[] = [];

  sentinel(): void {
    this.out.push([sk(KERNEL_LEAD_SENTINEL)]);
  }

  frameLine(frameKind: "OPEN" | "WAKE"): void {
    this.out.push([sk(`  frame=${frameKind}`)]);
  }

  // MANDATE (ADR-0026) — the HARD, machine-checkable, narrowing-ONLY channel. Standing context, so
  // it rides the cached prefix as SKELETON (never re-transmitted in a delta). A graded run declaring
  // no/incomplete mandate fails closed where it COMMITS its mandate — the plan-fixture freeze
  // ({@link ../session/harness/plan-fixture.ts capturePlanFixture} calls {@link ./types.ts
  // assertGradedMandate}, kestrel-voy9), NOT here: the renderer stays tolerant so an ungraded/
  // degenerate frame renders unchanged.
  mandate(mandate: Mandate): void {
    this.out.push([sk("-- MANDATE (hard; the only admission input) --")]);
    this.out.push([sk(`  objective=${mandate.objective}`)]);
    this.out.push([sk(`  1R=$${px(mandate.rUsd)}`)]);
    this.out.push([sk(`  success=${mandate.successCriterion}`)]);
    this.out.push([sk(`  bounded-risk=${mandate.riskRule}`)]);
  }

  // BRIEF (ADR-0026) — the SOFT, directional English channel. Rendered as standing context, hashed
  // + attributed, but LABELED as NOT a constraint: it can NEVER enter admission/narrowing (the hard
  // guard). Also SKELETON. Fail-closed brief-hash honesty (kestrel-voy9), defense-in-depth: a
  // supplied `{ text, hash }` whose hash does NOT match its text would ATTRIBUTE THE WRONG THESIS;
  // the load-bearing check is at the plan-fixture freeze, here we also refuse before emitting the
  // `brief=<hash>` line, so no rendered frame can carry a lie.
  brief(brief: Brief): void {
    assertBriefHashHonest(brief);
    this.out.push([sk("-- BRIEF (directional; NOT a constraint, NOT an admission input) --")]);
    for (const line of brief.text.split("\n")) this.out.push([sk(`  ${line}`)]);
    const ver = brief.version !== undefined ? ` v${brief.version}` : "";
    this.out.push([sk(`  brief=${brief.hash}${ver}`)]);
  }

  // 1. WAKE — reason / severity / RELATIVE deadline (`T-Nm to close`).
  wake(w: WakeRef | null | undefined): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[0])]);
    if (w === undefined || w === null) {
      this.out.push([sk("  wake: UNKNOWN (no wake ref)")]);
    } else {
      this.out.push([
        sk("  reason="),
        fld("wake.reason", w.reason),
        sk("  severity="),
        fld("wake.severity", w.severity),
        sk("  deadline="),
        fld("wake.deadline", kernelDeadlineStr(w.deadline)),
      ]);
    }
  }

  // 2. DATA-HEALTH per vehicle + the NAMED unavailable capabilities (never hidden).
  dataHealth(dh: readonly VehicleHealth[], unav: readonly string[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[1])]);
    if (dh.length === 0) {
      this.out.push([sk("  per-vehicle health: UNKNOWN (no vehicle health reported)")]);
    } else {
      for (const h of dh) {
        const a = `data-health.${h.instrument}`;
        this.out.push([
          sk(`  ${h.instrument}: bid_present_rate=`),
          fld(`${a}.bid_present_rate`, num(h.bidPresentRate, 3)),
          sk(" two_sided="),
          fld(`${a}.two_sided`, String(h.twoSided)),
          sk(" stale_s="),
          fld(`${a}.stale_s`, num(h.staleS, 1)),
          sk(" dark="),
          fld(`${a}.dark`, String(h.dark)),
        ]);
      }
    }
    this.out.push([
      sk("  unavailable capabilities: "),
      fld("data-health.unavailable", unav.length > 0 ? unav.join(", ") : "none"),
    ]);
  }

  // 3. POSITIONS / INVENTORY-CLAIMS.
  positions(positions: readonly Position[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[2])]);
    if (positions.length === 0) {
      this.out.push([sk("  flat — no positions / inventory claims")]);
      return;
    }
    positions.forEach((p, i) => {
      // kestrel-c11: the position's running unrealized P&L in DOLLARS (`unrealUsd`), so the agent READS
      // `unreal=-$25.97` rather than deriving it and mis-scaling cents-for-dollars. Absent ⇒ omitted
      // (purely additive; pre-c11 frames byte-identical); `null` ⇒ `unreal=—` (mark UNKNOWN, fail-closed).
      const unreal = p.unrealUsd === undefined ? "" : ` unreal=${usd(p.unrealUsd)}`;
      // ADR-0017 (kestrel-orx): an equity/spot leg has NO strike/right — render it as `<instrument>
      // shares`, never the phantom `<right>@<strike>` (a fictional zero-strike CALL). An option leg
      // (both present) keeps today's `<right>@<strike>` form BYTE-IDENTICAL.
      const leg = isSpotLeg(p) ? `${p.instrument} shares` : `${p.right}@${px(p.strike)}`;
      // kestrel-wa0j.47: an absent structure/claim renders the codebook marker `UNKNOWN`, never a
      // bare `?` — `?` is outside the closed honesty codebook (ADR-0041 §1), so a token-diet or a
      // reader can confuse it with punctuation. A present value renders byte-identically to before.
      this.out.push([
        sk("  "),
        fld(
          `positions.${i}`,
          `${signedInt(p.qty)} ${leg} basis=${px(p.basis)} ` +
            `${p.structure ?? "UNKNOWN"} claim=${p.claimOwner ?? "UNKNOWN"}${unreal}`,
        ),
      ]);
    });
  }

  // 4. RESTING ORDERS — live AND clamped as independent flags (a LIVE label never masks a clamp).
  resting(resting: readonly RestingOrder[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[3])]);
    if (resting.length === 0) {
      this.out.push([sk("  none resting")]);
      return;
    }
    resting.forEach((o, i) => {
      // Lead with the order `ref` — it is the handle `cancelOrder { ref }` needs, so a managing agent can
      // actually pull/replace a resting order (e.g. cancel a reserving TP before placing a closing sell).
      // Omitting it left a watcher naming a guessed ref that the engine rejected (frame-fix-1 finding).
      // ADR-0017 (kestrel-orx): an equity/spot order renders `<instrument> shares`, never the phantom
      // `<right><strike>`; an option order keeps today's `<right><strike>` form BYTE-IDENTICAL.
      const leg = isSpotLeg(o) ? `${o.instrument} shares` : `${o.right}${px(o.strike)}`;
      this.out.push([
        sk("  "),
        fld(
          `resting.${i}`,
          `ref=${o.ref}  ${o.side} ${leg}@${px(o.px)} ${restingLabel(o, CANONICAL_RESTING_LABELS)}  qty=${o.qty}`,
        ),
      ]);
    });
  }

  // 5. BUDGET / REMAINING-R + the SIZING headroom (kestrel-m9i.32 — the max fillable size the
  // remaining-R budget admits, so a model sizes WITHIN the bounded-risk envelope instead of into a
  // SILENT fire-time clamp: bounded risk = FULL COST BASIS, ADR-0017).
  budget(b: BudgetEnvelopeArg): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[4])]);
    if (b === undefined || b === null) {
      this.out.push([sk("  budget: UNKNOWN (no budget state)")]);
      this.out.push([sk("  sizing: UNKNOWN (no budget state)")]);
    } else {
      this.out.push([
        sk("  remaining_R="),
        fld("budget.remaining_R", num(b.remainingR)),
        sk("  plan_envelope="),
        fld("budget.plan_envelope", num(b.planEnvelope)),
        sk("  book_envelope="),
        fld("budget.book_envelope", num(b.bookEnvelope)),
        sk("  owner_envelope="),
        fld("budget.owner_envelope", num(b.ownerEnvelope)),
      ]);
      this.out.push(sizingLine(b.sizing));
    }
  }

  // 6. OWNER ENVELOPE + ACTS (mutual-visibility echo — the envelope echo reads the same §5 budget).
  ownerActs(b: BudgetEnvelopeArg, oacts: readonly OwnerAct[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[5])]);
    this.out.push([
      sk("  owner_envelope="),
      fld("owner.envelope", b !== undefined && b !== null ? num(b.ownerEnvelope) : "UNKNOWN"),
    ]);
    if (oacts.length === 0) {
      this.out.push([sk("  owner acts: none this session")]);
    } else {
      oacts.forEach((a, i) => {
        this.out.push([sk("  owner act: "), fld(`owner.act.${i}`, `${a.kind} (id=${a.id}, @seq${a.asofSeq})`)]);
      });
    }
  }

  // 7. L0/L1 ENGINE LOG — bucketed fired | cancelled | rejected | clamped (+ a catch-all `other`).
  engineLog(elog: readonly EngineAction[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[6])]);
    if (elog.length === 0) {
      this.out.push([sk("  engine actions: none")]);
      return;
    }
    const { buckets, other } = bucketEngineLog(elog);
    for (const { kind, items } of buckets) {
      // Surface the action's `reason` after its id@seq — a refusal (`rejected`, e.g. "uncovered sell
      // refused: never naked") or a clamp cause ("exceeds plan budget") must NEVER be silently dropped
      // at the render seam ('never silently false', kestrel-75n). Reason-WHEN-PRESENT: an action with no
      // reason (fired/cancelled) renders byte-identically to before (determinism / no churn).
      // De-dup a per-tick refusal flood: a run of identical (id, reason) entries collapses to
      // `<count>x <exemplar>` so a plan that fires every tick is one line, not 24 (kestrel-7bip). The
      // bucket count (`items.length`) still reflects EVERY action — the collapse is display-only.
      const ids = collapseActionTokens(items, CANONICAL_ACTION_STYLE).join(", ");
      this.out.push([
        sk(`  ${pad(`${kind}:`, 10)} `),
        fld(`engine-log.${kind}`, `${items.length}${ids !== "" ? `  [${ids}]` : ""}`),
      ]);
    }
    // Catch-all `other`: an action whose kind is outside the four fixed buckets is NEVER silently
    // dropped from this SAFETY/CONTROL surface (absent-not-hidden). EngineActionKind is a closed
    // union, so this only fires for a kind that arrived across an untyped/JSON/`as` boundary — even
    // then it is surfaced, not hidden (fail-closed). Rendered only when non-empty, so a well-typed
    // input's block stays byte-identical (determinism). Kept as SKELETON: its presence is structural,
    // so a frame that carries it re-keyframes the kernel rather than delta-encodes (skeleton change).
    if (other.length > 0) {
      const ids = other.map((e) => actionToken(e, CANONICAL_ACTION_STYLE)).join(", ");
      this.out.push([sk(`  ${pad("other:", 10)} ${other.length}  [${ids}]`)]);
    }
  }

  // 8. PREDICTOR / REGIME CLAIMS — each an HONEST MODEL Field w/ source + modelVer + confidence.
  claims(claims: readonly Claim[]): void {
    this.out.push([sk(KERNEL_SECTION_LABELS[7])]);
    if (claims.length === 0) {
      this.out.push([sk("  no predictor / regime claim wired")]);
      return;
    }
    claims.forEach((c, i) => {
      // Fail-closed honesty guard: refuse a non-MODEL / no-receipt / no-confidence claim (the
      // degraded state must show as degraded, never be faked as an attributed model claim).
      assertClaimHonest(c);
      const f = c.field;
      const val = typeof f.value === "number" ? num(f.value) : String(f.value);
      this.out.push([
        sk("  "),
        fld(`claims.${i}`, `${c.claimType} ${val} (source=${f.source}, modelVer=${f.modelVer}, conf=${num(f.confidence)})`),
      ]);
    });
  }
}

/**
 * Build the cockpit kernel block as anchored CELL-LINES — the ONE walk ({@link walkKernel}) driving
 * the {@link CanonicalKernelSink}. PURE + deterministic. Refuses a dishonest brief-hash / claim
 * (fail-closed). {@link lineText} over the result reproduces the exact canonical kernel bytes.
 */
function buildKernelCellLines(kernel: Kernel, frameKind: "OPEN" | "WAKE"): KernelCellLine[] {
  const sink = new CanonicalKernelSink();
  walkKernel(kernel, frameKind, sink);
  return sink.out;
}

/**
 * Build the cockpit kernel block as text lines (the byte-stable legacy surface): {@link lineText}
 * over {@link buildKernelCellLines}. PURE + deterministic (same kernel ⇒ byte-identical lines);
 * refuses a dishonest claim (fail-closed). Byte-identity with the cell substrate is what lets the
 * delta encoder reconstruct the full kernel byte-for-byte (the composed-completeness guarantee).
 */
function buildKernelLines(kernel: Kernel, frameKind: "OPEN" | "WAKE"): string[] {
  return buildKernelCellLines(kernel, frameKind).map(lineText);
}

/**
 * Render the non-configurable cockpit kernel block as a measured {@link KernelPane} — its text +
 * lines, the real token cost under the (labelled) tokenizer, and the format it materialized in.
 * PURE + deterministic; refuses a dishonest claim (fail-closed). The `format` is fail-closed to the
 * text screen ({@link assertFormatMaterialized}).
 */
export function renderKernelPane(
  kernel: Kernel,
  frameKind: "OPEN" | "WAKE",
  opts: RenderOptions = {},
): KernelPane {
  const format: RequestedFormat = opts.format ?? "ascii";
  assertTextFormat(format);
  const tokenizer: Tokenizer = opts.tokenizer ?? DEFAULT_TOKENIZER;
  const lines = buildKernelLines(kernel, frameKind);
  const text = lines.join("\n");
  return { text, lines, tokenCost: tokenizer.count(text), tokenizer: tokenizer.id, format };
}

/**
 * Render the non-configurable cockpit kernel block to a string (the {@link renderKernelPane} text).
 * This is the block every frame LEADS with: a KERNEL sentinel + the 8 fixed-order sections, each
 * always present. Built in code and prepended OUTSIDE any pane/View selection — a configured pane
 * can neither shadow it nor claim its reserved id ({@link ./types.ts assertPaneIdAllowed}).
 */
export function renderKernel(
  kernel: Kernel,
  frameKind: "OPEN" | "WAKE",
  opts: RenderOptions = {},
): string {
  return renderKernelPane(kernel, frameKind, opts).text;
}

/**
 * The fail-closed **lead guard** (the TS analogue of the render-layer kernel-lead contract): the
 * non-configurable kernel MUST be the first block of a frame — its first line is the lead sentinel.
 * A rendered frame that does not lead with the kernel is a defect (a caller reordered/dropped it),
 * so this THROWS rather than let a headless frame render. Cheap, pure, and always run before a
 * frame is returned.
 */
export function assertKernelLeads(rendered: string): void {
  const nl = rendered.indexOf("\n");
  const firstLine = nl === -1 ? rendered : rendered.slice(0, nl);
  // The reinterpreted lead invariant (kestrel-312): a frame LEADS with the non-configurable kernel
  // as EITHER a full block (keyframe) OR a cache-monotone DELTA block (a WAKE frame in a streaming
  // conversation, whose complete kernel is `cached skeleton + these moved fields`, verified by the
  // fail-closed composed-completeness guard {@link composeKernelDelta}). Any other first line is a
  // defect (a caller reordered/dropped the kernel) — fail closed.
  if (firstLine !== KERNEL_LEAD_SENTINEL && firstLine !== KERNEL_DELTA_SENTINEL) {
    throw new KernelHonestyError(
      `frame does not LEAD with the non-configurable kernel — first line was ${JSON.stringify(firstLine)}, ` +
        "expected either the full kernel lead sentinel or the cache-monotone KERNEL DELTA sentinel " +
        "(fail-closed lead guard; the kernel is prepended in code outside any pane/View selection and " +
        "cannot be dropped or reordered — a delta composes to the complete kernel against the reader's cache).",
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Kernel delta-encoding (kestrel-312) — collapse the re-emitted SAFETY/CONTROL skeleton
//
// In a cache-monotone stream (the `conversation`/`conversation-cached` policies, where the reader
// provably holds the prior full kernel in cached context) a WAKE frame carries ONLY the kernel
// FIELDS that MOVED since the prior frame; the byte-stable skeleton (sentinel, section labels,
// unchanged field values) is NOT re-emitted — it lives in the reader's cached prefix. A KEYFRAME
// (OPEN briefing; every `stateless-redraw` frame) keeps the COMPLETE block ({@link renderKernel}).
//
// The safety invariant is preserved and REINTERPRETED, not weakened: the COMPOSED kernel
// (cached-skeleton + moved fields) must reconstruct the complete current kernel BYTE-IDENTICALLY.
// {@link renderWakeDeltaKernel} verifies this before returning (fail-closed: throws on any
// incompleteness), and it falls back to a full keyframe whenever the kernel's SKELETON changed
// (a section appeared/vanished, a list's membership changed) — a structural change legitimately
// re-keyframes rather than delta-encodes. Absent-not-hidden holds: a field that MOVED to UNKNOWN is
// present-and-marked in the delta; a field that is merely unchanged is simply absent from the
// moved-set (held in cache) — the two are never confused.
// ─────────────────────────────────────────────────────────────────────────────

/** The shape key of a kernel's cell-lines: the ordered skeleton texts + field ANCHORS (NOT values).
 * Two kernels share a skeleton iff their keys match — the precondition for delta-encoding one against
 * the other (a mismatch means a section/list changed shape ⇒ re-keyframe, never a lossy delta). */
function kernelSkeletonKey(lines: readonly KernelCellLine[]): string {
  return lines
    .map((l) => l.map((s) => (s.kind === "skel" ? `s:${s.text}` : `f:${s.anchor}`)).join(""))
    .join("");
}

/** The (anchor → current value) map of every moving field in a kernel's cell-lines. */
function kernelFieldValues(lines: readonly KernelCellLine[]): Map<string, string> {
  const m = new Map<string, string>();
  for (const l of lines) for (const s of l) if (s.kind === "field") m.set(s.anchor, s.text);
  return m;
}

/**
 * Reconstruct the COMPLETE current WAKE kernel text from the reader's cached prior kernel
 * (`prevKernel`) + a delta block (`deltaText`) — the fail-closed composed-completeness operation.
 *
 * The cached prefix supplies the byte-stable skeleton + every UNCHANGED field value; the delta
 * supplies each MOVED field, keyed by its stable anchor. A delta line is `  <anchor> <value>` (the
 * anchor is space-free, so it splits from its value on the first space; the value may contain
 * spaces). FAIL-CLOSED: a missing delta sentinel, a malformed line, or a delta that references an
 * anchor ABSENT from the cached skeleton (a composition that could not be complete) THROWS a
 * {@link KernelHonestyError} — a delta is never allowed to compose to a partial kernel. PURE.
 */
export function composeKernelDelta(prevKernel: Kernel, deltaText: string, opts: RenderOptions = {}): string {
  const format: RequestedFormat = opts.format ?? "ascii";
  assertTextFormat(format);
  const lines = deltaText.split("\n");
  if (lines.length === 0 || lines[0] !== KERNEL_DELTA_SENTINEL) {
    throw new KernelHonestyError(
      "cannot compose kernel delta — the block does not lead with the KERNEL DELTA sentinel (fail-closed).",
    );
  }
  const moved = new Map<string, string>();
  for (let i = 1; i < lines.length; i++) {
    const raw = lines[i]!;
    const body = raw.startsWith("  ") ? raw.slice(2) : raw;
    const sp = body.indexOf(" ");
    if (sp <= 0) {
      throw new KernelHonestyError(`malformed kernel-delta line ${JSON.stringify(raw)} (expected \`  <anchor> <value>\`) — fail-closed.`);
    }
    moved.set(body.slice(0, sp), body.slice(sp + 1));
  }
  const prevLines = buildKernelCellLines(prevKernel, "WAKE");
  const prevAnchors = new Set<string>();
  for (const l of prevLines) for (const s of l) if (s.kind === "field") prevAnchors.add(s.anchor);
  for (const a of moved.keys()) {
    if (!prevAnchors.has(a)) {
      throw new KernelHonestyError(
        `kernel delta references anchor ${JSON.stringify(a)} absent from the cached kernel skeleton — the ` +
          "composition would be INCOMPLETE/incoherent (a moved field cannot land in the reader's cache); fail-closed.",
      );
    }
  }
  return prevLines
    .map((l) =>
      l.map((s) => (s.kind === "skel" ? s.text : moved.has(s.anchor) ? moved.get(s.anchor)! : s.text)).join(""),
    )
    .join("\n");
}

/**
 * Delta-encode the SAFETY/CONTROL kernel of a WAKE frame against the reader's cached prior kernel
 * (`prevKernel`), returning a {@link KERNEL_DELTA_SENTINEL}-led block carrying ONLY the fields that
 * MOVED — or `null` when the kernel's SKELETON changed (a section/list changed shape), signalling the
 * caller to emit a full keyframe instead. Only used where the reader provably holds the prior full
 * kernel (streaming conversation policies), never under `stateless-redraw`.
 *
 * FAIL-CLOSED composed-completeness: before returning, it COMPOSES the delta against `prevKernel`
 * ({@link composeKernelDelta}) and asserts the result is BYTE-IDENTICAL to the full current kernel
 * ({@link renderKernel} `WAKE`); any mismatch (a moved field dropped, an encoding bug) THROWS a
 * {@link KernelHonestyError}. So a delta that ever escapes provably reconstructs the complete kernel.
 * PURE + deterministic. The honesty guard on claims runs via {@link buildKernelCellLines}.
 */
export function renderWakeDeltaKernel(prevKernel: Kernel, curKernel: Kernel, opts: RenderOptions = {}): string | null {
  const format: RequestedFormat = opts.format ?? "ascii";
  assertTextFormat(format);
  const prevLines = buildKernelCellLines(prevKernel, "WAKE");
  const curLines = buildKernelCellLines(curKernel, "WAKE");
  // Skeleton must match (same sections + same list membership) — else a lossless field-delta is
  // impossible, so re-keyframe (return null). This is the honest structural-change escape hatch.
  if (kernelSkeletonKey(prevLines) !== kernelSkeletonKey(curLines)) return null;
  const prevVals = kernelFieldValues(prevLines);
  const curVals = kernelFieldValues(curLines);
  const movedLines: string[] = [];
  for (const [anchor, text] of curVals) {
    // Wire-safety: an anchor with a space/newline (or a value with a newline) would break the
    // `  <anchor> <value>` line framing — fail SAFE to a keyframe rather than emit an ambiguous delta.
    if (anchor.includes(" ") || anchor.includes("\n") || text.includes("\n")) return null;
    if (prevVals.get(anchor) !== text) movedLines.push(`  ${anchor} ${text}`);
  }
  const deltaText = [KERNEL_DELTA_SENTINEL, ...movedLines].join("\n");
  // FAIL-CLOSED: the delta + the cached prior skeleton MUST reconstruct the full current kernel,
  // byte-for-byte. Compose and compare; throw if the composition is ever incomplete or divergent.
  const composed = composeKernelDelta(prevKernel, deltaText, opts);
  const target = renderKernel(curKernel, "WAKE", opts);
  if (composed !== target) {
    throw new KernelHonestyError(
      "kernel delta does not compose to the COMPLETE current kernel — the composed skeleton+delta " +
        "diverges from the full-kernel render (a moved field would be lost). Refusing to emit an " +
        "incomplete SAFETY/CONTROL kernel (fail-closed).",
    );
  }
  return deltaText;
}

// ─────────────────────────────────────────────────────────────────────────────
// Assembly helpers — the body panes are materialized from the catalog under a resolved View
// ─────────────────────────────────────────────────────────────────────────────

/** Join panes with a single blank line between them (readable, and append-friendly). */
function panes(...blocks: readonly string[]): string {
  return blocks.join("\n\n");
}

/** Materialize the (non-kernel) body panes for a frame: resolve the View (or the frame kind's
 * DEFAULT View — byte-identical to the pre-catalog pane set) against the single-source catalog,
 * then build + budget-check each selected pane. Fail-closed on an unknown pane id or over-budget. */
function renderBody(ctx: PaneBuildContext, opts: RenderOptions): readonly string[] {
  const tokenizer: Tokenizer = opts.tokenizer ?? DEFAULT_TOKENIZER;
  // Role-keyed (ADR-0041 §2 / A1): the acting seat is consulted ONLY when no View was authored — the
  // precedence lives in resolveView (authored View > seat founder View > phase default). Absent seat ⇒
  // byte-identical to before the axis existed.
  const resolved = resolveView(opts.view, ctx.frameKind, opts.seat);
  return materializePanes(resolved, ctx, tokenizer);
}

// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Render the OPEN **keyframe** briefing — a full Frame that LEADS with the non-configurable cockpit
 * kernel (built in code, prepended OUTSIDE the pane selection), then the header (relative time
 * only), the instrument specs, the primary instrument's levels, the orientation tape (rotated
 * candlestick), the near-money chain, the macro pane (absent-with-reason in v1), and the acting
 * plan-lifecycle detail. Date-blind: only relative time + the HH:MM ET clock the input carries.
 * PURE (no wall clock / RNG). The lead guard {@link assertKernelLeads} fails closed if the kernel is
 * ever not the first block.
 */
export function renderBriefing(input: BriefingInput, opts: RenderOptions = {}): string {
  const clock = input.clockET !== undefined ? ` · ${input.clockET} ET` : "";
  const phase = input.phase !== undefined && input.phase !== null ? ` · ${input.phase}` : "";
  const header = `KESTREL · OPEN briefing · ${toClose(input.timeToCloseMin)}${phase}${clock}`;
  // The kernel LEADS — prepended in code, OUTSIDE any pane/View selection (non-configurable).
  // The body panes are SELECTED + ORDERED from the single-source catalog under the active View
  // (or the OPEN DEFAULT View — byte-identical to the pre-catalog hardcoded set).
  const body = renderBody(
    {
      instruments: input.instruments,
      market: input.market,
      kernel: input.kernel,
      frameKind: "OPEN",
      // kestrel-wa0j.20: prior-session tapes ride into the context so `tape d-N` serves their rows. The
      // renderer invents no value, so the data enters THROUGH the frame input. Absent ⇒ byte-identical.
      ...(input.priorSessions !== undefined ? { priorSessions: input.priorSessions } : {}),
      // kestrel-wa0j.29: the armed plans' enforced terms ride into the context so the `armed-plan` pane
      // renders them. Typically ABSENT at OPEN (authoring happens AT the open, nothing armed yet) ⇒ the
      // pane fails closed to absent-with-reason, byte-identical to a frame without the field.
      ...(input.armedPlan !== undefined ? { armedPlan: input.armedPlan } : {}),
    },
    opts,
  );
  const out = panes(renderKernel(input.kernel, "OPEN", opts), header, ...body);
  assertKernelLeads(out); // fail-closed: the kernel MUST be the first block.
  return out;
}

/**
 * Render a WAKE **delta frame** — a suffix that LEADS with the non-configurable cockpit kernel,
 * then carries only what changed since the author's last vantage: the wake identity
 * (`wake 3 · 41m since last`), the tape since last (its own anchor), the current levels, the
 * near-money chain, and the acting plan-lifecycle detail (its `fillsSinceLast` are the fills since
 * last). Append-only (ADR-0008) and date-blind (EVALUATION.md). PURE; lead-guarded.
 */
export function renderWakeDelta(input: WakeDeltaInput, opts: RenderOptions = {}): string {
  return assembleWakeFrame(input, renderKernel(input.kernel, "WAKE", opts), opts);
}

/**
 * Render a WAKE **delta frame** for a CACHE-MONOTONE stream (kestrel-312) — identical to
 * {@link renderWakeDelta} except its SAFETY/CONTROL kernel is DELTA-encoded against the reader's
 * cached prior kernel (`prevKernel`): only the fields that MOVED are emitted, the byte-stable
 * skeleton lives in the cached prefix ({@link renderWakeDeltaKernel}). Used ONLY where the reader
 * provably holds the prior full kernel (the `conversation`/`conversation-cached` policies), NEVER
 * under `stateless-redraw` (which gets the self-complete full-kernel {@link renderWakeDelta}).
 *
 * When the kernel's SKELETON changed (a section/list changed shape) the delta encoder returns `null`
 * and this falls back to a full keyframe kernel — a structural change honestly re-keyframes. The
 * fail-closed composed-completeness guard runs inside {@link renderWakeDeltaKernel}; the delta block
 * LEADS the frame ({@link KERNEL_DELTA_SENTINEL}), lead-guarded like any other frame. PURE.
 */
export function renderWakeDeltaStreamed(
  input: WakeDeltaInput,
  prevKernel: Kernel,
  opts: RenderOptions = {},
): string {
  const delta = renderWakeDeltaKernel(prevKernel, input.kernel, opts);
  const kernelBlock = delta ?? renderKernel(input.kernel, "WAKE", opts);
  return assembleWakeFrame(input, kernelBlock, opts);
}

/** Assemble a WAKE frame from a (full or delta) kernel block + the since-last panes. The kernel
 * LEADS — prepended in code, OUTSIDE any pane/View selection (non-configurable); lead-guarded. */
function assembleWakeFrame(input: WakeDeltaInput, kernelBlock: string, opts: RenderOptions = {}): string {
  const clock = input.clockET !== undefined ? ` · ${input.clockET} ET` : "";
  const phase = input.phase !== undefined && input.phase !== null ? ` · ${input.phase}` : "";
  const since = `${input.minutesSinceLast === null ? DASH : String(Math.round(input.minutesSinceLast))}m since last`;
  const reason = input.wakeReason !== undefined ? ` · reason: ${input.wakeReason}` : "";
  const header = `KESTREL · wake ${input.wakeIndex} · ${since} · ${toClose(input.timeToCloseMin)}${phase}${clock}${reason}`;
  // WAKE has no instrument-spec pane by default; a superset View naming `instruments` renders `(none)`.
  // The frame-carried prior vantage + the minutes-since-last ride into the pane context so the `delta`
  // pane (kestrel-wa0j.48) can state WHAT MOVED — the renderer invents no value, so the prior data
  // enters THROUGH the frame input. Absent ⇒ the delta pane fails closed to absent-with-reason.
  const body = renderBody(
    {
      instruments: [],
      market: input.market,
      kernel: input.kernel,
      frameKind: "WAKE",
      ...(input.priorVantage !== undefined ? { priorVantage: input.priorVantage } : {}),
      ...(input.minutesSinceLast !== undefined ? { minutesSinceLast: input.minutesSinceLast } : {}),
      // kestrel-wa0j.20: prior-session tapes ride into the context so `tape d-N` serves their rows.
      // Absent (the v1 single-day wake) ⇒ byte-identical, and `tape d-N` renders the Train 1B absence.
      ...(input.priorSessions !== undefined ? { priorSessions: input.priorSessions } : {}),
      // kestrel-wa0j.29: the armed plans' enforced terms ride into the context so the `armed-plan` pane
      // renders the WHEN/entries/exits/invalidation/size the watcher enforces (the renderer invents no
      // value — the terms enter THROUGH the frame input). Absent ⇒ the pane fails closed to
      // absent-with-reason, byte-identical to a frame without the field.
      ...(input.armedPlan !== undefined ? { armedPlan: input.armedPlan } : {}),
    },
    opts,
  );
  const out = panes(kernelBlock, header, ...body);
  assertKernelLeads(out); // fail-closed: the kernel MUST be the first block.
  return out;
}
