/**
 * # frame/render-arm — the token-optimal percept renderer ARM (kestrel-4gl.9 / wa0j.27 / 5ine)
 *
 * A SECOND, explicit rendering of the SAME typed Frame inputs, restructured to spend fewer
 * tokens under a BPE tokenizer while carrying the IDENTICAL fact set. This module NEVER touches
 * the canonical renderer ({@link ./render.ts}) — canonical stays the frozen leaderboard control
 * (wa0j.27). The arm is selectable as a HARNESS PROPERTY ({@link RenderArm} / {@link renderPercept})
 * so a run picks its renderer the way it picks its model — the renderer rides the harness bundle,
 * it is never a leaderboard facet (kestrel-5ine).
 *
 * ## Why an arm (the hypothesis, wa0j.27)
 * The percept is dense numeric/tabular data — exactly where BPE fragmentation bites. Three
 * measured hotspots (see `docs/results/gemini-token-optimal/report.md`, measured on the real
 * `src/frame` output under the o200k_base + cl100k_base BPE tables):
 *   1. **Alignment padding** — the chain table's `pad(...)` runs and the kernel's fixed gutters
 *      emit runs of spaces; a run of 2+ spaces is often its own BPE token, spent per row per column.
 *   2. **Non-ASCII glyphs** — `·` `→` `–` `—` `█` `─` are multi-byte and frequently 1+ token each;
 *      the rotated-candlestick tape is a WALL of `█`/`─` glyphs (dozens of tokens per row) that
 *      encodes price only approximately (column position), so it is both the costliest pane AND
 *      lossy vs the numbers it is drawn from.
 *   3. **Repeated skeleton** — the verbose banners (`==== SAFETY / CONTROL KERNEL … ====`) and the
 *      re-spent column labels cost tokens on every frame.
 *
 * The arm's fixes are UNIVERSAL to BPE tokenizers (Gemini's SentencePiece, GPT's o200k, Claude's
 * proprietary BPE all fragment padded decimals + rare glyphs): collapse alignment to a single
 * delimiter, render ASCII, render the tape as compact numeric OHLC (cheaper AND exact), and shorten
 * the skeleton. See the report for per-family caveats (only the GPT-family o200k/cl100k counts are
 * measured precisely here; the Gemini/Claude hosted count endpoints were unreachable in-env).
 *
 * ## Information parity is a GATE wired into the RUNTIME emit path (review F1)
 * The arm carries the SAME facts as canonical — a compaction that DROPS a level, a strike, a P&L, or
 * a wake reason is a safety regression, not a token win. {@link assertInformationParity} extracts the
 * numeric fact MULTISET from BOTH renders and asserts the arm carries every occurrence canonical
 * does, plus every free-text fact collected from the typed INPUT. The gate runs INLINE inside
 * {@link renderTokenOptimal} — the production entry point — so a lossy arm REFUSES to emit at
 * runtime, not only under test (the house guard-wiring rule: a guard present but unwired is inert).
 * `tests/render-arm.parity.test.ts` ships DELIBERATELY-LOSSY fixtures that the gate must catch
 * through that runtime path — unwire the inline assert and those tests go red.
 */

import type {
  Brief,
  BriefingInput,
  Budget,
  ChainRow,
  Claim,
  EngineAction,
  FillRecord,
  InstrumentSpec,
  Kernel,
  LevelSet,
  Mandate,
  OwnerAct,
  Position,
  RestingOrder,
  SizingHeadroom,
  TapeRow,
  VehicleHealth,
  WakeRef,
  WakeDeltaInput,
} from "./types.ts";
import { KERNEL_DELTA_SENTINEL, KERNEL_LEAD_SENTINEL, KERNEL_SECTION_LABELS } from "./types.ts";
import { DASH, isSpotLeg, num, px, signedInt, toClose, usd } from "./format.ts";
import { renderBriefing, renderWakeDelta } from "./render.ts";
import {
  actionToken,
  bucketEngineLog,
  collapseActionTokens,
  restingLabel,
  walkKernel,
  type BudgetEnvelopeArg,
  type KernelSink,
} from "./kernel-walk.ts";
import { candleLine, tapeBounds } from "./tape-geometry.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The arm registry (kestrel-5ine — the renderer is a harness PROPERTY)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A renderer arm id. `canonical` is the frozen leaderboard control ({@link ./render.ts}); the
 * `token-optimal-*` arms are this module's compaction, NAMED per target model family so the harness
 * can default a family's renderer (kestrel-5ine: claude-code→fable, codex→gpt, …).
 *
 * MEASURED DIVERGENCE (2026-07-15, native Gemini `countTokens` — the follow-up leg): the two family
 * arms share the universal compaction (ASCII separators, single-space delimiting, compact skeleton,
 * one-line engine log) but DIVERGE on the TAPE:
 *   - `token-optimal-fable` renders the tape as exact numeric OHLC (the comprehension winner —
 *     preferred 9–1 by the reader models, +2.7pp quiz accuracy);
 *   - `token-optimal-gemini` KEEPS the canonical rotated-candlestick glyph tape, because Gemini's
 *     tokenizer prices the glyph runs CHEAP and the OHLC decimals DEAR (candlestick row 18 native
 *     tokens vs 39 for the same bucket as OHLC; the OHLC-tape arm LOST 26.9% on the tape-heavy
 *     percept under native Gemini counts). Data: docs/results/gemini-token-optimal/
 *     gemini-token-audit.json.
 */
export type RenderArm = "canonical" | "token-optimal-gemini" | "token-optimal-fable";

/** The arm ids this module materializes (canonical is served by {@link ./render.ts}, not here). */
export const TOKEN_OPTIMAL_ARMS: readonly RenderArm[] = ["token-optimal-gemini", "token-optimal-fable"];

/** Is `arm` one of the token-optimal arms this module renders? */
export function isTokenOptimalArm(arm: RenderArm): boolean {
  return (TOKEN_OPTIMAL_ARMS as readonly string[]).includes(arm);
}

/** How an arm renders the TAPE pane — the one measured per-family divergence (see {@link RenderArm}). */
export type ArmTapeStyle = "ohlc" | "candlestick";

/** The measured tape style per token-optimal arm (native-count driven, never taste). */
export const ARM_TAPE_STYLE: Readonly<Record<Exclude<RenderArm, "canonical">, ArmTapeStyle>> = {
  "token-optimal-gemini": "candlestick",
  "token-optimal-fable": "ohlc",
};

// ─────────────────────────────────────────────────────────────────────────────
// Compact formatting primitives (ASCII-only; single-delimiter; shares format.ts value fns)
// ─────────────────────────────────────────────────────────────────────────────

/** The arm's ASCII unknown token. Canonical uses `—` (U+2014, a multi-byte rare glyph); the arm
 * uses `na`. {@link normalizeFact} maps BOTH to one canonical UNKNOWN fact so the parity guard
 * never mistakes a re-encoding of "unknown" for a dropped fact. */
const ARM_DASH = "na";

/** px/num but with the ASCII unknown token — a value renders identically to canonical (same digits,
 * same precision, same integer/2dp rule) so numeric facts match byte-for-byte across arms. */
const apx = (x: number | null | undefined): string => {
  const s = px(x);
  return s === DASH ? ARM_DASH : s;
};
const anum = (x: number | null | undefined, prec = 2): string => {
  const s = num(x, prec);
  return s === DASH ? ARM_DASH : s;
};
const ausd = (x: number | null | undefined): string => {
  const s = usd(x);
  return s === DASH ? ARM_DASH : s;
};

/** Compact tape/deadline reused from canonical (relative, date-blind). */
const armToClose = (min: number | null): string => toClose(min).replace(" to close", "");

// ─────────────────────────────────────────────────────────────────────────────
// Kernel (compact) — the SAME 8 fixed-order sections + optional mandate/brief, absent-not-hidden
// ─────────────────────────────────────────────────────────────────────────────

/** The engine-log bucket set + classification, the resting-state label, and the action id token are
 * single-source in {@link ./kernel-walk.ts} (shared with the canonical adapter). The arm's action
 * token is `id@N(reason)` (no `seq` prefix, no space before the reason); its resting-label vocabulary
 * is the lowercase `live`/`live+clamped` form. */
const ARM_ACTION_STYLE = { seqPrefix: "", reasonSpace: false } as const;
const ARM_RESTING_LABELS = { live: "live", liveClamped: "live+clamped", clamped: "clamped", off: "off" } as const;

function sizingLine(s: SizingHeadroom | null | undefined): string {
  if (s === undefined || s === null) return "sizing=na";
  const unit = s.unit === "shares" ? "sh" : "ct";
  if (s.maxUnits !== null && s.basisPerUnit !== null) {
    return `sizing=max~${s.maxUnits}${unit} basis=${anum(s.basisPerUnit)}/${unit} rem1R=${anum(s.remainingUsd)}`;
  }
  const note = s.note !== undefined ? ` ${s.note}` : "";
  return `sizing=max~na${unit} rem1R=${anum(s.remainingUsd)}${note}`;
}

/**
 * The token-optimal {@link KernelSink} — {@link walkKernel}'s ARM adapter. SAME 8 fixed-order
 * sections, SAME facts, compacted to ASCII + single-space delimiting. Leads with a short sentinel
 * that still marks the safety block unambiguously; the `frame=` line is folded into that sentinel
 * (so {@link frameLine} is a no-op). This module NEVER touches the canonical adapter — it only
 * shares the ONE walk and the parameterized kernel helpers.
 */
class ArmKernelSink implements KernelSink {
  readonly L: string[] = [];

  sentinel(frameKind: "OPEN" | "WAKE"): void {
    this.L.push(`#KERNEL ${frameKind} (safety/control; leads every frame)`);
  }

  frameLine(): void {
    // The arm folds the frame kind into its sentinel; no separate `frame=` line.
  }

  mandate(mandate: Mandate): void {
    this.L.push(`#mandate obj=${mandate.objective} 1R=$${apx(mandate.rUsd)} success=${mandate.successCriterion} risk=${mandate.riskRule}`);
  }

  brief(brief: Brief): void {
    const ver = brief.version !== undefined ? ` v${brief.version}` : "";
    this.L.push(`#brief(directional; NOT a constraint) ${brief.text.replace(/\n+/g, " ")} [${brief.hash}${ver}]`);
  }

  // 1. WAKE
  wake(w: WakeRef | null | undefined): void {
    this.L.push(w == null
      ? `#wake na`
      : `#wake reason=${w.reason} sev=${w.severity} ${armToClose(w.deadline ?? null)}`);
  }

  // 2. DATA-HEALTH
  dataHealth(dh: readonly VehicleHealth[], unav: readonly string[]): void {
    if (dh.length === 0) {
      this.L.push(`#health na`);
    } else {
      for (const h of dh) {
        this.L.push(`#health ${h.instrument} bid=${anum(h.bidPresentRate, 3)} 2sided=${h.twoSided} stale=${anum(h.staleS, 1)} dark=${h.dark}`);
      }
    }
    this.L.push(`#unavail ${unav.length > 0 ? unav.join(", ") : "none"}`);
  }

  // 3. POSITIONS
  positions(positions: readonly Position[]): void {
    if (positions.length === 0) {
      this.L.push(`#pos flat`);
      return;
    }
    for (const p of positions) {
      const unreal = p.unrealUsd === undefined ? "" : ` unreal=${ausd(p.unrealUsd)}`;
      const leg = isSpotLeg(p) ? `${p.instrument}sh` : `${p.right}${apx(p.strike)}`;
      this.L.push(`#pos ${signedInt(p.qty)} ${leg} basis=${apx(p.basis)} ${p.structure ?? "?"} claim=${p.claimOwner ?? "?"}${unreal}`);
    }
  }

  // 4. RESTING
  resting(resting: readonly RestingOrder[]): void {
    if (resting.length === 0) {
      this.L.push(`#rest none`);
      return;
    }
    for (const o of resting) {
      const leg = isSpotLeg(o) ? `${o.instrument}sh` : `${o.right}${apx(o.strike)}`;
      this.L.push(`#rest ref=${o.ref} ${o.side} ${leg}@${apx(o.px)} ${restingLabel(o, ARM_RESTING_LABELS)} qty=${o.qty}`);
    }
  }

  // 5. BUDGET + sizing
  budget(b: BudgetEnvelopeArg): void {
    if (b == null) {
      this.L.push(`#budget na`);
      this.L.push(`sizing=na`);
    } else {
      this.L.push(`#budget remR=${anum(b.remainingR)} plan=${anum(b.planEnvelope)} book=${anum(b.bookEnvelope)} owner=${anum(b.ownerEnvelope)}`);
      this.L.push(sizingLine(b.sizing));
    }
  }

  // 6. OWNER envelope + acts
  ownerActs(b: BudgetEnvelopeArg, oacts: readonly OwnerAct[]): void {
    const owner = b != null ? anum(b.ownerEnvelope) : ARM_DASH;
    const acts = oacts.length === 0 ? "none" : oacts.map((a) => `${a.kind}(id=${a.id},@${a.asofSeq})`).join(" ");
    this.L.push(`#owner envelope=${owner} acts=${acts}`);
  }

  // 7. ENGINE LOG
  engineLog(elog: readonly EngineAction[]): void {
    if (elog.length === 0) {
      this.L.push(`#engine none`);
      return;
    }
    const { buckets, other } = bucketEngineLog(elog);
    const parts: string[] = [];
    for (const { kind, items } of buckets) {
      // De-dup a per-tick refusal flood: a run of identical (id, reason) entries collapses to
      // `<count>x <exemplar>` (kestrel-7bip). The bucket count still counts EVERY action.
      const ids = collapseActionTokens(items, ARM_ACTION_STYLE).join(",");
      parts.push(`${kind}=${items.length}${ids !== "" ? `[${ids}]` : ""}`);
    }
    if (other.length > 0) parts.push(`other=${other.length}[${other.map((e) => actionToken(e, ARM_ACTION_STYLE)).join(",")}]`);
    this.L.push(`#engine ${parts.join(" ")}`);
  }

  // 8. CLAIMS
  claims(claims: readonly Claim[]): void {
    if (claims.length === 0) {
      this.L.push(`#claims none`);
      return;
    }
    for (const c of claims) {
      const f = c.field;
      const val = typeof f.value === "number" ? anum(f.value) : String(f.value);
      this.L.push(`#claims ${c.claimType}=${val} (src=${f.source},ver=${f.modelVer},conf=${anum(f.confidence)})`);
    }
  }
}

/** Render the compact cockpit kernel — the ONE walk ({@link walkKernel}) driving {@link ArmKernelSink}.
 * SAME sections, SAME facts, ASCII, single-space delimited. */
function armKernel(kernel: Kernel, frameKind: "OPEN" | "WAKE"): string {
  const sink = new ArmKernelSink();
  walkKernel(kernel, frameKind, sink);
  return sink.L.join("\n");
}

// ─────────────────────────────────────────────────────────────────────────────
// Body panes (compact)
// ─────────────────────────────────────────────────────────────────────────────

function armInstruments(instruments: readonly InstrumentSpec[]): string {
  if (instruments.length === 0) return "instruments: none";
  const rows = instruments.map((i) => {
    const role = i.role !== undefined ? ` ${i.role}` : "";
    return `  ${i.symbol} ${i.assetClass}${role} mult=${apx(i.multiplier)} tick=${apx(i.tick)}`;
  });
  return ["instruments:", ...rows].join("\n");
}

function armLevels(instrument: string, lv: LevelSet): string {
  // Range separator is `/`, never `-`: a `-` between two numbers reads as a negative sign (to a BPE
  // reader AND to the numeric-parity extractor), so `5112.50-5125` would mis-say "-5125".
  return `levels ${instrument}: spot=${apx(lv.spot)} pc=${apx(lv.priorClose)} hod=${apx(lv.hod)} lod=${apx(lv.lod)} vwap=${apx(lv.vwap)} or=${apx(lv.orLow)}/${apx(lv.orHigh)}`;
}

/**
 * The tape under the arm — TWO measured styles ({@link ArmTapeStyle}):
 *
 *   - `ohlc` (the FABLE arm): exact numeric OHLC per bucket. The comprehension winner (readers
 *     preferred it 9–1; +2.7pp quiz accuracy) — MORE information than the glyph picture — but it
 *     costs MORE tokens on every tokenizer measured (decimals fragment; glyph runs compress).
 *   - `candlestick` (the GEMINI arm): the canonical rotated-candlestick GLYPH rows under the arm's
 *     compact ASCII header. Native Gemini counts price a candlestick bucket at 18 tokens vs 39 as
 *     OHLC, and the OHLC-style arm LOST 26.9% on the tape-heavy percept — so the Gemini arm keeps
 *     the glyph tape (measured, not designed). The glyph geometry is the SHARED
 *     {@link ./tape-geometry.ts candleLine} (the ONE 40-column geometry the canonical pane draws too,
 *     ADR-0052 §1) — the arm owns only its own compact header + row prefix, never a second geometry.
 *
 * Both carry the same facts the canonical tape does (axis lo/hi + per-row clocks); `volume` when
 * present is carried on the ohlc style (parity superset).
 */
function armTape(rows: readonly TapeRow[], bucketMin: number, style: ArmTapeStyle): string {
  if (rows.length === 0) return `tape ${bucketMin}m: no prints`;
  const { lo, hi } = tapeBounds(rows);
  const header = `tape ${bucketMin}m axis=${apx(lo)}/${apx(hi)} anchor=${rows[0]!.clock}`;
  if (style === "ohlc") {
    const body = rows.map((r) => {
      const vol = r.volume !== null && r.volume !== undefined && Number.isFinite(r.volume) ? ` v=${apx(r.volume)}` : "";
      return `  ${r.clock} o=${apx(r.open)} h=${apx(r.high)} l=${apx(r.low)} c=${apx(r.close)}${vol}`;
    });
    return [header, ...body].join("\n");
  }
  // candlestick: the canonical glyph rows via the SHARED geometry (wick `─` low→high, body `█`
  // open→close, price = column) — {@link ./tape-geometry.ts candleLine}, drawn identically to the
  // canonical pane; the arm owns only its compact ASCII header + `  <clock> ` row prefix.
  const body = rows.map((r) => `  ${r.clock} ${candleLine(r, lo, hi)}`);
  return [header, ...body].join("\n");
}

function armDarkFlag(row: ChainRow): string {
  const bidDark = row.bid === null;
  const askDark = row.ask === null;
  if (bidDark && askDark) return "dark";
  if (bidDark) return "bid-dark";
  if (askDark) return "ask-dark";
  return "";
}

function armChain(underlier: string, chain: readonly ChainRow[], dte?: number | null): string {
  const dteTag = dte !== undefined && dte !== null && Number.isFinite(dte) ? ` ${dte}dte` : "";
  if (chain.length === 0) return `chain ${underlier}${dteTag}: no legs`;
  const rows = chain.map((r) => {
    const fair = r.fairNote !== undefined && r.fair !== null && r.fair !== undefined
      ? `${apx(r.fair)} ${r.fairNote}`
      : apx(r.fair);
    const flag = armDarkFlag(r);
    return `  ${apx(r.strike)}${r.right} bid=${apx(r.bid)} ask=${apx(r.ask)} fair=${fair}${flag !== "" ? ` ${flag}` : ""}`;
  });
  return [`chain ${underlier}${dteTag} (strike/right bid ask fair):`, ...rows].join("\n");
}

const ARM_MACRO = "macro: unavailable (v1 harness)";

function planTag(plan: string | undefined): string {
  return plan === undefined ? "" : ` (${plan})`;
}

function armPositionsDetail(positions: readonly Position[]): string {
  if (positions.length === 0) return "positions: none";
  const rows = positions.map((p) => {
    const unreal = p.unrealUsd === undefined ? "" : ` unreal=${ausd(p.unrealUsd)}`;
    return isSpotLeg(p)
      ? `  ${signedInt(p.qty)} ${p.instrument}sh basis=${apx(p.basis)}${unreal}${planTag(p.plan)}`
      : `  ${signedInt(p.qty)} ${p.instrument} ${apx(p.strike)}${p.right} basis=${apx(p.basis)} fair=${apx(p.fair)}${unreal}${planTag(p.plan)}`;
  });
  return ["positions:", ...rows].join("\n");
}

function armResting(resting: readonly RestingOrder[]): string {
  if (resting.length === 0) return "resting: none";
  const rows = resting.map((o) => {
    const note = o.note !== undefined ? ` [${o.note}]` : "";
    const leg = isSpotLeg(o) ? "sh" : `${apx(o.strike)}${o.right}`;
    return `  ${o.side.toUpperCase()} ${o.qty} ${o.instrument} ${leg} @${apx(o.px)}${note}${planTag(o.plan)}`;
  });
  return ["resting:", ...rows].join("\n");
}

function armFills(fills: readonly FillRecord[]): string {
  if (fills.length === 0) return "fills since last: none";
  const rows = fills.map((f) => {
    const at = f.clock !== undefined ? ` @${f.clock}` : "";
    const leg = isSpotLeg(f) ? "sh" : `${apx(f.strike)}${f.right}`;
    return `  ${f.side.toUpperCase()} ${f.qty} ${f.instrument} ${leg} @${apx(f.px)}${at}${planTag(f.plan)}`;
  });
  return ["fills since last:", ...rows].join("\n");
}

function armBudget(budget: Budget | null | undefined): string {
  if (budget == null) return `budget: ${ARM_DASH}`;
  const total = budget.total !== undefined ? ` total=${apx(budget.total)}` : "";
  const maxR = budget.maxConcurrentR !== undefined ? ` maxR=${apx(budget.maxConcurrentR)}` : "";
  return `budget: used=${anum(budget.used)} rem=${anum(budget.remaining)}${total}${maxR}`;
}

function armPlans(kernel: Kernel): string {
  if (kernel.plans.length === 0) return "plans: none";
  const rows = kernel.plans.map((p) => {
    const outcome = p.outcome !== undefined ? ` (${p.outcome})` : "";
    const blocked = p.blockedReason !== undefined ? ` (blocked: ${p.blockedReason})` : "";
    const note = p.note !== undefined ? ` - ${p.note}` : "";
    return `  ${p.name}: ${p.state}${outcome}${blocked}${note}`;
  });
  return ["plans:", ...rows].join("\n");
}

function armActingDetail(kernel: Kernel): string {
  return [
    "#ACTING",
    armPositionsDetail(kernel.positions),
    armResting(kernel.resting),
    armFills(kernel.fillsSinceLast),
    armBudget(kernel.budget),
    armPlans(kernel),
  ].join("\n");
}

// ─────────────────────────────────────────────────────────────────────────────
// Public API — the arm's OPEN keyframe + WAKE delta (default View; mirrors ./render.ts entry points)
// ─────────────────────────────────────────────────────────────────────────────

const joinPanes = (...blocks: readonly string[]): string => blocks.join("\n\n");

/** Render the OPEN keyframe under a token-optimal arm (default View). Same facts as
 * {@link ./render.ts renderBriefing}, compacted; the tape style is the arm's ONE measured
 * divergence ({@link ArmTapeStyle}, default `ohlc` = the Fable arm). Kernel leads. PURE. */
export function renderBriefingArm(input: BriefingInput, tape: ArmTapeStyle = "ohlc"): string {
  const clock = input.clockET !== undefined ? ` ${input.clockET}ET` : "";
  const phase = input.phase != null ? ` ${input.phase}` : "";
  const header = `#OPEN ${armToClose(input.timeToCloseMin)}${phase}${clock}`;
  return joinPanes(
    armKernel(input.kernel, "OPEN"),
    header,
    armInstruments(input.instruments),
    armLevels(input.market.instrument, input.market.levels),
    armTape(input.market.tape, input.market.tapeBucketMin, tape),
    armChain(input.market.instrument, input.market.chain, input.market.chainDte),
    ARM_MACRO,
    armActingDetail(input.kernel),
  );
}

/** Render the WAKE delta under a token-optimal arm (default View). Same facts as
 * {@link ./render.ts renderWakeDelta}, compacted; tape style per the arm ({@link ArmTapeStyle},
 * default `ohlc` = the Fable arm). Kernel leads. PURE + deterministic. */
export function renderWakeDeltaArm(input: WakeDeltaInput, tape: ArmTapeStyle = "ohlc"): string {
  const clock = input.clockET !== undefined ? ` ${input.clockET}ET` : "";
  const phase = input.phase != null ? ` ${input.phase}` : "";
  const since = `${input.minutesSinceLast === null ? ARM_DASH : String(Math.round(input.minutesSinceLast))}m-since-last`;
  const reason = input.wakeReason !== undefined ? ` reason=${input.wakeReason}` : "";
  const header = `#WAKE ${input.wakeIndex} ${since} ${armToClose(input.timeToCloseMin)}${phase}${clock}${reason}`;
  return joinPanes(
    armKernel(input.kernel, "WAKE"),
    header,
    armTape(input.market.tape, input.market.tapeBucketMin, tape),
    armLevels(input.market.instrument, input.market.levels),
    armChain(input.market.instrument, input.market.chain, input.market.chainDte),
    armActingDetail(input.kernel),
  );
}

/** The frame kind + its typed input, tagged so {@link renderPercept} can dispatch canonically. */
export type PerceptInput =
  | { readonly kind: "OPEN"; readonly input: BriefingInput }
  | { readonly kind: "WAKE"; readonly input: WakeDeltaInput };

// ─────────────────────────────────────────────────────────────────────────────
// Information-parity GATE (kestrel-4gl.9 — no guard without a failing fixture)
// ─────────────────────────────────────────────────────────────────────────────

/** Thrown when a token-optimal arm render DROPS a fact the canonical render carried — a safety
 * regression masquerading as a token win. The gate fails CLOSED (the arm is refused, never shipped). */
export class InformationParityError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "InformationParityError";
  }
}

/** Canonical SKELETON strings that carry DIGITS but are NOT facts (e.g. `-- L0/L1 ENGINE LOG --`) —
 * removed before numeric extraction so a fixed label's digits are never mistaken for a market value.
 * The arm's own skeleton is digit-free, so this only needs to neutralize canonical's labels. */
const SKELETON_WITH_DIGITS: readonly string[] = [
  KERNEL_LEAD_SENTINEL,
  KERNEL_DELTA_SENTINEL,
  ...KERNEL_SECTION_LABELS,
];

function stripSkeleton(text: string): string {
  let out = text;
  for (const label of SKELETON_WITH_DIGITS) out = out.split(label).join(" ");
  return out;
}

/** The numeric literals in a rendered percept as a MULTISET (token → occurrence count) — a strike,
 * a level, a budget, a P&L. Signs are kept (`-25.97` ≠ `25.97`). A true multiset (review F4): the
 * arm must carry every occurrence canonical does, so a dropped duplicate can never hide behind
 * another rendering of the same number (the Set version's blind spot — e.g. a strike shown in both
 * the kernel and the acting detail). Fixed skeleton labels are stripped first ({@link stripSkeleton})
 * so a digit-bearing label (`-- L0/L1 ENGINE LOG --`) is never mistaken for a market value. */
export function numericFacts(text: string): Map<string, number> {
  const m = new Map<string, number>();
  for (const tok of stripSkeleton(text).match(/-?\d+(?:\.\d+)?/g) ?? []) {
    m.set(tok, (m.get(tok) ?? 0) + 1);
  }
  return m;
}

/** Collect the FREE-TEXT (non-numeric) facts from a typed percept input — the values a numeric-only
 * check cannot see (wake reasons, plan names/states, order refs, notes, unavailable capabilities,
 * claim sources, instrument symbols). Driven from the INPUT (not re-parsed from text) so it is a
 * precise, non-brittle list of exactly what the arm MUST still contain. */
export function freeTextFacts(p: PerceptInput): string[] {
  const facts: string[] = [];
  const k = p.input.kernel;
  const push = (s: string | null | undefined): void => {
    if (s !== null && s !== undefined && s !== "" && !/^-?\d+(?:\.\d+)?$/.test(s)) facts.push(s);
  };
  if (p.kind === "WAKE") push(p.input.wakeReason);
  push(p.input.market.instrument);
  if (k.wake != null) {
    push(k.wake.reason);
    push(k.wake.severity);
  }
  for (const h of k.dataHealth ?? []) push(h.instrument);
  for (const u of k.unavailable ?? []) push(u);
  for (const o of k.resting ?? []) {
    push(o.ref);
    push(o.side);
    push(o.instrument);
    push(o.note);
    push(o.plan);
  }
  for (const pos of k.positions ?? []) {
    push(pos.instrument);
    push(pos.plan);
  }
  for (const f of k.fillsSinceLast ?? []) {
    push(f.side);
    push(f.instrument);
    push(f.plan);
  }
  for (const e of k.engineLog ?? []) {
    push(e.id);
    push(e.reason);
  }
  for (const c of k.claims ?? []) {
    push(c.claimType);
    push(c.field.source);
    push(c.field.modelVer);
  }
  for (const pl of k.plans ?? []) {
    push(pl.name);
    push(pl.state);
    push(pl.blockedReason);
    push(pl.note);
  }
  for (const inst of p.kind === "OPEN" ? p.input.instruments : []) {
    push(inst.symbol);
    push(inst.assetClass);
  }
  return facts;
}

/**
 * The information-parity GATE. Given the typed input, the canonical render, and the arm render,
 * assert the arm carries EVERY fact canonical does:
 *   (1) numeric multiset: every numeric literal in canonical appears in the arm at least as often;
 *   (2) free-text: every free-text fact from the INPUT appears as a substring of the arm.
 * Fails CLOSED ({@link InformationParityError}) on the FIRST dropped fact. PURE. This is the driver
 * a deliberately-lossy arm fixture must trip (the house rule: a guard ships with a failing fixture).
 */
export function assertInformationParity(p: PerceptInput, canonicalText: string, armText: string): void {
  const canon = numericFacts(canonicalText);
  const arm = numericFacts(armText);
  for (const [tok, need] of canon) {
    const have = arm.get(tok) ?? 0;
    if (have < need) {
      throw new InformationParityError(
        `token-optimal arm DROPPED a numeric fact: canonical shows "${tok}" ×${need}, the arm shows it ×${have}. ` +
          "A compaction that loses a level/strike/budget/P&L occurrence is a safety regression, not a token win (fail-closed).",
      );
    }
  }
  // Case-insensitive: a side ("buy") renders uppercased ("BUY") in BOTH renderers — the FACT is
  // present, only the case differs. A dropped fact is absent in any case.
  const armLower = armText.toLowerCase();
  for (const fact of freeTextFacts(p)) {
    if (!armLower.includes(fact.toLowerCase())) {
      throw new InformationParityError(
        `token-optimal arm DROPPED a free-text fact from the input: ${JSON.stringify(fact)} is absent from the arm render ` +
          "(fail-closed — the arm must carry the same facts as canonical).",
      );
    }
  }
}

/** A token-optimal render implementation — the injectable seam {@link renderTokenOptimal} gates.
 * Production uses {@link renderTokenOptimalUnchecked}; a test fixture injects a deliberately-lossy
 * impl to prove the runtime gate is WIRED (never bypassed by production callers). */
export type TokenOptimalRenderImpl = (p: PerceptInput, arm: Exclude<RenderArm, "canonical">) => string;

/** The UNGATED arm render (frame-kind dispatch + the arm's measured tape style). INTERNAL BUILDING
 * BLOCK: production callers MUST go through {@link renderTokenOptimal}, which runs the fail-closed
 * information-parity gate on every emit — this function exists as the emitter's default impl and as
 * the base a parity-fixture wraps. Exported for those two uses only. */
export function renderTokenOptimalUnchecked(p: PerceptInput, arm: Exclude<RenderArm, "canonical"> = "token-optimal-fable"): string {
  const tape = ARM_TAPE_STYLE[arm];
  return p.kind === "OPEN" ? renderBriefingArm(p.input, tape) : renderWakeDeltaArm(p.input, tape);
}

// ─────────────────────────────────────────────────────────────────────────────
// Pane-population detector (cross-session finding 2026-07-15: the failed-breaks pane rendered as
// all-dashes on ALL 11,809 production frames because OR levels were null UPSTREAM — a data bug the
// render made invisible-in-plain-sight). A DETECTOR, not a refusal: absent-not-hidden is a renderer
// INVARIANT (a degraded frame legitimately renders `—`/`na`/`none`), so an all-placeholder pane is
// only WRONG when it is all-placeholder on EVERY frame — fleet-scale telemetry, not a per-frame
// throw. This function is the per-frame primitive that telemetry (and the percept-lab audits) call.
// ─────────────────────────────────────────────────────────────────────────────

/** One pane's population status: its first line (the pane label) + whether it carries ANY numeric
 * fact (a digit-token after skeleton stripping). A DEAD pane is structure + placeholders only. */
export interface PanePopulation {
  readonly pane: string;
  readonly dead: boolean;
}

/**
 * Classify each blank-line-separated pane of a rendered percept as POPULATED (carries at least one
 * numeric fact) or DEAD (skeleton + placeholders only — `—`/`na`/`none`/`no prints`/`no legs`).
 * Works on BOTH the canonical and the arm render (the pane separator is the same `\n\n`). A frame
 * header pane (clock digits) counts as populated — the detector's target is FACT panes that show no
 * fact. Pure. A monitoring consumer flags a pane that is dead across ALL frames of a run (the
 * failed-breaks failure mode); a single degraded frame's dead pane is honest rendering, not a defect.
 */
export function panePopulation(rendered: string): readonly PanePopulation[] {
  // A pane FACT is a STANDALONE number — not a digit embedded in a label token (`5m` bucket width,
  // `v1` version, `b76` receipt, `T-385m`): those are skeleton, and counting them was exactly how an
  // all-dash pane could masquerade as populated. Lookarounds exclude letter-adjacent digits.
  const FACT = /(?<![A-Za-z0-9])-?\d+(?:\.\d+)?(?![A-Za-z0-9])/;
  return rendered.split("\n\n").map((block) => {
    const firstLine = block.split("\n", 1)[0] ?? "";
    return { pane: firstLine.slice(0, 60), dead: !FACT.test(stripSkeleton(block)) };
  });
}

/** The dead panes of a rendered percept (see {@link panePopulation}) — convenience projection. */
export function deadPanes(rendered: string): readonly string[] {
  return panePopulation(rendered)
    .filter((p) => p.dead)
    .map((p) => p.pane);
}

/**
 * Render a percept under a token-optimal arm — the PRODUCTION entry point, with the
 * information-parity gate WIRED INLINE (review F1; the house guard-wiring pattern): every emit
 * renders the canonical control from the SAME typed input and runs {@link assertInformationParity}
 * before the arm text is returned. A parity failure THROWS — the arm REFUSES to emit a percept that
 * dropped a fact, in production, not only under test. The `impl` parameter is the injectable seam a
 * failing fixture uses to prove this gate is wired through THIS path (unwire the assert below and
 * `tests/render-arm.parity.test.ts`'s runtime-gate fixture goes red). PURE + deterministic.
 */
export function renderTokenOptimal(
  p: PerceptInput,
  arm: Exclude<RenderArm, "canonical"> = "token-optimal-fable",
  impl: TokenOptimalRenderImpl = renderTokenOptimalUnchecked,
): string {
  const canonical = p.kind === "OPEN" ? renderBriefing(p.input) : renderWakeDelta(p.input);
  const armText = impl(p, arm);
  assertInformationParity(p, canonical, armText); // fail-closed: refuse a lossy emit (F1)
  return armText;
}
