# OSS-ADR-0052 — The Rendering seam: one kernel/tape walk, format adapters, and the human chart

Status: accepted (2026-07-18, owner grill — 2026-07-17 depth review, G1)

## Context

The Frame has two renderers kept in sync by hand: `src/frame/render.ts` (canonical)
and `src/frame/render-arm.ts` (the token-optimal ARM) each independently walk the
8-section SAFETY/CONTROL kernel (`KERNEL_SECTION_LABELS`) and re-materialize the
40-column tape geometry, with near-verbatim copies of `restingState`, `sizingLine`,
`ENGINE_LOG_BUCKETS`, and the action-id helpers. The only bridge is the
after-the-fact information-parity gate, and the renderer epoch
(`renderer-revision.ts`) is a hand-bumped constant whose own docstring has already
drifted from the value (prose `/5`, constant `/8`) — the behaviour↔epoch link is
prose, not a check.

Meanwhile `RequestedFormat` admits `json` and `html` that nothing produces: the
format seam has exactly one adapter (text). The cost surfaced concretely — the
generated stock/options education videos rendered the *agent's* vertical tape,
not a human candlestick chart, because the agent tape is the only Rendering that
exists. CONTEXT.md's Rendering entry already promises the other half ("the
agent's ASCII screen and the human's HTML chart are two Renderings of one
Frame"); Caller doctrine says a human gets the human Rendering. There was no
human Rendering to serve.

## Decision

1. **One walk.** The kernel/tape model is materialized once, in one deep module.
   Canonical text, the per-tokenizer ARM texts (the `TOKEN_OPTIMAL_ARMS`
   registry), and `json` are **format adapters** at the Rendering seam. The
   information-parity gate becomes a property of the seam, not a bridge between
   hand-kept copies. **Proof obligation: the canonical adapter reproduces
   today's canonical bytes exactly** (golden + parity fixtures) — canonical is
   the frozen leaderboard control; the refactor lands byte-identical or not at
   all.
2. **The `json` Rendering is materialized.** Frame → typed JSON carrying Fields
   whole (`value` + Attribution + SourceWatermark). A Rendering never invents or
   changes a value; `null` Fields cross the wire as explicit UNKNOWN.
3. **The human visualization layer is a Rendering adapter package, shipped from
   this repo** as a subpath export of the published package
   (`kestrel.markets/react`). **Isolation is a requirement, not a hope**: the
   subpath is its own `exports` entry (the CLI/agent entry never resolves it),
   and `react` is a peer dependency marked optional
   (`peerDependenciesMeta.react.optional: true`), so the `npx` funnel installs
   nothing new and loads nothing new — a consumer needs React only if they
   import the subpath. Two component families:
   - the **honest chart** — standard human candlesticks consuming the Frame
     `json` Rendering: UNKNOWN renders as a gap (never an interpolated candle),
     `MODEL` Fields are badged with their receipt, provenance is inspectable.
     No generic-OHLC entry point: a chart that can't prove it invented nothing
     defeats the point.
   - the **terminal view** — displays an *existing text Rendering* (canonical
     or an ARM chosen by a tokenizer/model prop). It renders bytes the engine
     already emits; it is a shell, never a fourth renderer.
4. **React hosts Renderings; it never produces them.** This is the invariant
   uniting the web components above with the already-decided Ink oversight TUI
   (human-oversight PRD 2026-07-14, build decision 11; kestrel-jvr4.4): both
   are shells that *display* engine output — the web terminal-view shows text
   Renderings the engine already emitted; the Ink TUI is a strict view over
   `OversightFrame` that invents no value, while every one-shot verb keeps its
   byte-stable `text`/`json`/`ascii` Rendering untouched ("strip the SGR
   wrappers and the exact text bytes remain"). No React reconciler — DOM or
   Ink — ever sits inside a Rendering: renderers stay pure `Frame → bytes`
   functions on the deterministic path. The earlier spike recommendation
   against Ink (kestrel-5kt, 2026-07-12: @clack over Ink — install weight,
   raw-mode-on-pipes) is superseded on the *choice* by decision 11, but its
   concerns survive as constraints: the TUI rides the same isolation
   discipline as `/react` — its own entrypoint, lazily loaded, never a hard
   dependency the `npx` funnel pays for, and never attached to a piped/
   non-interactive stream (raw-mode fails open on pipes; the Caller check
   already routes agents to machine Renderings).
5. **The renderer epoch is derived, not hand-typed.** The epoch that stamps
   session identity is computed from the renderer's observable behaviour (
   mechanism chosen at implementation: e.g. a hash over the golden-fixture
   render corpus), so a byte-changing edit cannot ship without the epoch
   moving.

## Considered and rejected

- **Formalize a `FrameRenderer` interface but keep both implementations** — the
  seam gets a type, the copy-paste cost stays forever.
- **Freeze canonical untouched; dedupe only ARM + json** — zero byte-risk, but
  two walks remain and every kernel-section change still lands twice.
- **Platform-side visualizer** — the dashboard gets components but OSS
  docs/blog/videos cannot import them, and outsiders cannot render Frames the
  way we do; the trust substrate should ship its own legible visualization.
- **Generic OHLC props on the chart** — maximum reuse, but the chart loses
  Field provenance and with it the honest-chart claim.

## Consequences

- Caller doctrine becomes servable at the content layer: human-facing content
  (dashboard, docs, blog, videos) defaults to the human Rendering; the agent
  screen appears only when the subject *is* the agent's perception.
- Kernel-section and tape-geometry bugs concentrate in one module; the
  duplicated helpers are deleted.
- `TOKEN_OPTIMAL_ARMS` becomes the seam's adapter registry — the terminal
  view's model-selector prop reads it directly.
- The parity gate survives as the seam's regression harness, and doubles as
  the byte-identity check for the refactor itself.
