Skip to content

ADR-0033: Reveal NoshAPI Primitive — Project-Wide ASCII Visual Language

Six inspiration-corpus batches have surfaced the same recurring observation: animated character-by-character or wave-shaped reveal of static text / ASCII content is a project-wide aesthetic, not a per-cartridge novelty. The pattern shows up in at least seven distinct surfaces:

  1. CIPHER fragment arrival on the CIPHER-LINE OLED — every new fragment from the voice engine “decrypts into view” rather than landing in one frame. The Sneakers-style reveal is a built-in delivery mechanism, paired with the CIPHER voice style guide which governs what the revealed text says.
  2. Mission contract-accept on the primary display — when the operator commits a contract via [EVAL], the contract title and parameters reveal one character at a time.
  3. DECRYPT / INTERROGATE / ANALYZE verb response style — cart-author convention for certain TERM verbs.
  4. Cart-load splash — title screens land with a radial reveal; CIPHERgarden, Black Ledger, ICE Breaker, Depthcharge all want this in their respective gameplay specs.
  5. The boot animation handoff to the first interactive frameBOOTSTRA.386 lineage, top-left → bottom-right wipe.
  6. CRT power-off shrinkAetherTune lineage, inverse of radial reveal.
  7. Carousel tab inbound on the Bare Deck Terminalblessed-contrib lineage.

docs/influences/synthesis.md §2 L6 (“ASCII is a first-class rendering capability — not a font”) consolidates this into a single doctrine: one verb, multiple styles, one project-wide surface. §3 item 3 and §6 item 7 promoted the work to a sprint queue item: promote docs/influences/effect/ascii-effects.md from inspiration-tier to canonical, land (reveal …) in ADR-0005, and add a libcaca-engine reference path.

Three convergent agent reviews (embedded systems, gameplay design, architect) flagged this as critical-path during the synthesis pass. The cross-domain agreement is the forcing function: gameplay wants the verb as a primitive so missions can dispatch reveals without each cart re-authoring the algorithm; embedded wants the engine reference path locked early so the Pi Zero 2 W performance envelope can be planned against a known shape; architect wants the primitive in the FFI catalog so the 14 cart specs that mention “reveal” stop referencing a vapor surface.

  1. Multiple cart specs already cite (reveal …). Searching the corpus surfaces 14 module specs (ICE Breaker, Depthcharge, Black Ledger, CIPHERgarden, Synth-Fence, Shellfire, Null, others) that name-drop the (reveal …) call or its reveal-text predecessor without it actually existing in NoshAPI. Every additional sprint that lands cart code amplifies the cost of an eventual rename. Land the primitive now; the specs become real.
  2. The :char-flicker algorithm has a GPL constraint. The reference implementation in libnms (docs/influences/research/no-more-secrets.md) is GPL v3. KN-86 cannot vendor it. A clean-room reimplementation from first principles is the right path; doing it as part of a one-shot ADR commitment is cheaper than doing it ad-hoc when the first cart needs it.
  3. The (reveal …) surface is the natural anchor for the broader ASCII visual language. The full ascii-effects.md doctrine carries three event types (:hover, :click, :reveal) and a (draw-ascii …) raster-to-text pipeline. Landing (reveal …) is the minimum-viable promotion. The :hover and :click surfaces have no v0.1 use case (KN-86 has no mouse; the “hover” semantic translates to focus-cursor proximity which is itself underspecified — see docs/influences/effect/ascii-effects.md §“Open questions”). (draw-ascii …) is downstream of a cart actually shipping with raster source imagery — none planned for v0.1. Scope this ADR to (reveal …) only.
  4. CLAUDE.md Spec Hygiene Rule 3 applies. This ADR amends ADR-0005 (primitive catalog) and promotes docs/influences/effect/ascii-effects.md to a new canonical home. Both edits need to land in the same PR with a grep sweep that confirms the corpus references resolve cleanly.
  • Pi Zero 2 W is the sole on-device target. No GPU. Implementation has to be CPU-only; the libcaca lineage is the engine reference.
  • The cell-handler latency contract from ADR-0005 §“Dispatch Contract” is 5 ms target / 10 ms ceiling. A synchronous reveal that blocks the cart for the full duration would violate this by orders of magnitude. Asynchronous-only is the only viable lifecycle.
  • Per-cart arena from ADR-0004 is 256 KB. The reveal scheduler’s per-cell state has to be runtime-side, not Fe-arena-side; allocating per-cell state in the cart’s arena would balloon working-set on full-content-area reveals.
  • The redraw loop from ADR-0005 is event-driven with a 20 fps animation cap. Continuous-animation styles (e.g., the deferred :fft) would force the runtime into animation mode indefinitely; v0.1 styles must complete and return to event-driven idle.
  • GPL on the no-more-secrets reference. libnms is GPL v3; no vendoring. Reimplementation from first principles — the algorithm is small (~50 lines per the source doc).

Land (reveal :style …) as a first-class Tier 1 (All-Carts) NoshAPI primitive per ADR-0005. Promote docs/influences/effect/ascii-effects.md from inspiration-tier to canonical at docs/software/api-reference/grammars/reveal-styles.md. Stub the inspiration-tier doc with a “promoted to canonical” pointer.

Concrete commitments:

Four primitives land together as Tier 1, available to every cart:

Lisp namePurpose
revealStart an animated reveal on a cell surface. Returns a handle.
reveal-cancelInterrupt an in-flight reveal; snap surface to final state.
reveal-complete?Non-blocking poll; returns #t once the reveal has finished or been cancelled.
unrevealInverse — animate the removal of a surface (CRT-shrink lineage).

Full signatures, parameter ranges, validation, and error symbols live in docs/software/api-reference/grammars/reveal-styles.md §2 + §4. Tier 1 placement matches the existing text-puts / draw-ascii siblings — every cart needs them, no mission-context restriction.

2.2 Lifecycle — asynchronous, cart-interruptible

Section titled “2.2 Lifecycle — asynchronous, cart-interruptible”

(reveal …) is asynchronous. The call enqueues an animation, returns a handle, and continues; the runtime ticks the animation off the existing 20 fps redraw loop. Cart authors fire-and-forget, poll-for-completion, or cancel mid-flight.

Rationale. The cell-handler latency contract is 5 ms / 10 ms. A synchronous reveal at :duration 1.0 would block the cart handler for 1000 ms — two orders of magnitude over the ceiling. Async + handle-returned + cart-polls is the only model that respects the existing contract without forcing cart authors to hand-author an animation loop in Lisp. See docs/software/api-reference/grammars/reveal-styles.md §5 for the full lifecycle semantics including operator-pre-empt behaviour (any keypress cancels all active reveals for the current cart).

2.3 Style enum — ship three, defer three

Section titled “2.3 Style enum — ship three, defer three”

v0.1 ships :char-flicker, :no-more-secrets (alias for :char-flicker), and :radial.

Deferred for post-v0.1: :random, :diagonal, :fft. Deferred styles raise :reveal-style-not-implemented rather than silently behaving as a default — cart authors who want a deferred style today see the diagnostic and fall back to a shipping style.

Rationale.

  • :char-flicker is mandatory. It is the no-more-secrets / Sneakers decryption reveal that drives the CIPHER fragment arrival, mission contract-accept, and DECRYPT-class verb responses — the three highest-leverage uses across the cart corpus. Skipping it leaves the most-cited surface in the v0.1 catalog vapor.
  • :no-more-secrets is an alias. Same runtime behaviour as :char-flicker; the named alias lets cart sources self-document intent (the cart authors a Sneakers-style decryption, not a generic per-cell flicker). Zero engineering cost.
  • :radial is mandatory. It is the canonical cart-load splash effect and the natural fit for CIPHER-LINE OLED fragment arrival as an alternative to :char-flicker. Without it, every cart-load splash reverts to “land in one frame,” which loses the birth aesthetic that the synthesis flags as identity-forming.
  • :random defers because for small surfaces it visually overlaps :char-flicker and for large surfaces it visually overlaps :radial. The marginal aesthetic value is unclear without an operator-eye review on hardware. Reconsider at bring-up.
  • :diagonal defers because its two anchor use cases (boot-animation handoff, carousel tab inbound) are themselves not yet wired through (reveal …). Wiring those is its own engineering task; the deferral lets this ADR ship without taking on that scope.
  • :fft defers because it requires a per-frame frequency-bucket stream from the PSG coprocessor that does not yet exist. The coprocessor bridge work is a multi-sprint commitment of its own. Out of scope here.

Pick three; defer three. Reconsider the deferrals at the next post-bring-up review.

The synthesis recommendation argues (reveal …) is cart-callable. Confirmed. The primitive family lives in Tier 1, available to every cart’s Fe runtime as a kebab-case symbol. No mission-context restriction; the cart can call (reveal …) from any handler (cart load, cell event dispatch, REPL invocation, mission phase tick).

REPL exposure is restricted by the existing Tier 3 read-only rule: the REPL has a no-op stub that returns nil and logs to stderr if invoked. The full surface is for cart code only; REPL operators who want to see a reveal demo should run a debug cart that fires it on every keypress.

Both shipping styles target ≥ 30 fps steady-state on full primary-display surfaces (128×73 cells) and ≥ 60 fps on the CIPHER-LINE OLED (4×32 cells).

These are estimates derived from the libcaca and no-more-secrets reference algorithms scaled to KN-86 cell counts; they have not been measured on hardware. The bring-up benchmark is part of the implementation acceptance criteria — see §4. If real performance falls below the 20 fps redraw cap, the implementation degrades by halving per-cell flicker rate / widening the radial step; the FFI contract does not change. Full performance discussion in docs/software/api-reference/grammars/reveal-styles.md §6.

Both shipping styles are transition-only — they animate the appearance of a static surface and complete, returning the runtime to event-driven idle. None hold animation mode indefinitely. Continuous-animation styles (the deferred :fft) carry separate performance characterization that this ADR does not commit.

2.6 Engine reference — libcaca-shaped, CPU-only

Section titled “2.6 Engine reference — libcaca-shaped, CPU-only”

On-device implementation references the libcaca algorithmic shape: input cell rectangle → per-tick state update → write into the 128×75 cell grid (or the 128×150 half-block canvas for sub-pixel styles) per ADR-0027. No GPU dependency; CPU-only. KN-86 ships its own implementation against the algorithmic shape; libcaca is not vendored (license-mixing concern from docs/influences/effect/libcaca.md §“License notes” is best avoided by a clean reimplementation, and the algorithm is small enough that the engineering cost is trivial).

The :char-flicker algorithm specifically references libnms as the technique source — GPL v3, reimplement from first principles, do not copy code. Per-cell state struct (target glyph, current-flicker glyph, remaining-trail counter, settle-deadline tick), tick advances each cell’s flicker counter, settle-deadline triggers the snap-to-target. ~50 lines of C per the source doc.


A. Status quo — (reveal …) stays inspiration-tier, individual carts re-author the reveal in cart Lisp

Section titled “A. Status quo — (reveal …) stays inspiration-tier, individual carts re-author the reveal in cart Lisp”

Rejected. Each cart’s hand-authored reveal would diverge in defaults (duration, rate, trail length), violating the “one project-wide doctrine” principle that drove the synthesis. The 14 cart specs already citing (reveal …) would each pick a slightly different shape; the operator’s perceived KN-86 identity would fragment. The arena cost of cart-Lisp-side animation loops is also non-trivial — each per-cell state allocation eats into the 256 KB cart arena per ADR-0004.

B. Land the full ascii-effects.md surface — (reveal …) + (ascii-effect …) + (draw-ascii …) all at once

Section titled “B. Land the full ascii-effects.md surface — (reveal …) + (ascii-effect …) + (draw-ascii …) all at once”

Rejected for v0.1, kept as future scope. The :hover and :click interaction grammars from docs/influences/effect/ascii-effects.md §“Interaction grammar” have no v0.1 use case: KN-86 has no mouse, and the “hover” semantic translates to focus-cursor proximity which is itself underspecified across surfaces. (draw-ascii …) is downstream of a cart actually shipping with raster source imagery — none are planned for v0.1. Landing those primitives now would commit the runtime to an API surface no caller exists for. Scope to (reveal …) only; promote the others in a follow-on ADR once a real caller surfaces.

C. Synchronous (reveal …) — call blocks until animation completes

Section titled “C. Synchronous (reveal …) — call blocks until animation completes”

Rejected. Violates the cell-handler latency contract from ADR-0005 §“Dispatch Contract” (5 ms target, 10 ms ceiling) by two orders of magnitude. A 1-second blocking reveal stalls the entire cart, freezing input processing, freezing other handlers, freezing the CIPHER tick. Operator UX would be unrecoverable. No path forward.

D. Continuous-animation styles in v0.1 (:fft, perpetual :radial pulse)

Section titled “D. Continuous-animation styles in v0.1 (:fft, perpetual :radial pulse)”

Rejected for v0.1. Continuous styles force the runtime into animation mode indefinitely, defeating the event-driven idle that the ADR-0005 redraw loop is built around. Battery envelope from CLAUDE.md Battery row already tightened post-coprocessor (ADR-0017); adding a perpetual-animation source on the Pi takes that envelope further down. Defer continuous styles until power-budget headroom is measured at bring-up.

E. Per-style FFI primitives — (reveal-flicker …), (reveal-radial …), separate functions

Section titled “E. Per-style FFI primitives — (reveal-flicker …), (reveal-radial …), separate functions”

Rejected. The synthesis explicitly argues for one verb, multiple styles. Per-style primitives would mean three FFI symbols at v0.1 and grow to six once deferred styles ship — six symbols where one suffices. Cart-source readability suffers ((reveal-flicker …) reads less semantically than (reveal … :style :char-flicker)). The per-style approach also closes off the natural growth path where a new style is purely additive to the enum without an FFI surface change.

F. Vendor libnms directly for the :char-flicker algorithm

Section titled “F. Vendor libnms directly for the :char-flicker algorithm”

Rejected. GPL v3 (per docs/influences/research/no-more-secrets.md §“License note”). KN-86’s runtime ships under a permissive license to keep cart-author downstream licensing flexible; vendoring libnms would force a GPL boundary into the runtime. The algorithm is ~50 lines; reimplement.


  • One project-wide reveal doctrine. The 14 cart specs citing (reveal …) resolve against a real surface. Future cart authors get one place to look.
  • Cart arena pressure relieved. Per-cell animation state is runtime-side, not Fe-arena-side; carts pay for the surface region (cell handle) and nothing else.
  • The CIPHER fragment delivery mechanism is no longer vapor. docs/software/runtime/cipher-voice-style-guide.md (which governs CIPHER content) now has a real delivery mechanism to pair with.
  • Asynchronous + handle-returned matches the existing handler-latency contract. No special-case “long-running primitive” carve-out; the contract works as-is.
  • Engine reference path locked. Embedded engineering can plan the Pi Zero 2 W implementation against the libcaca + no-more-secrets algorithmic shape without waiting for further design rounds.
  • Cart-callable + Tier 1 placement. Carts get the primitive without mission-context gating; mission-runner code paths and one-off cart-load splashes both work.
  • Adds 4 primitives to the ADR-0005 catalog. Count rises from 57 to 61. The FFI surface grows; cart author cognitive load grows commensurately. Mitigated by the canonical reveal-styles.md doc and Tier 1 docs in ADR-0005.
  • Bring-up benchmark required. The Pi Zero 2 W performance estimates are not measured. If real fps falls below the 20 fps redraw cap, implementation degradation paths kick in; the spec’d defaults may need a hardware-time revision.
  • Three styles deferred to post-v0.1. :random, :diagonal, :fft not available at launch. Carts that want them today raise :reveal-style-not-implemented; cart authors must fall back to a shipping style. The boot animation handoff and carousel tab inbound surfaces (both :diagonal cases) will land in one frame until :diagonal ships. Acceptable for v0.1.
  • Operator-pre-empt policy is global. Any keypress cancels all active reveals for the current cart. A cart that wants per-key granularity has no opt-in. The cost is a possible “I wanted to type SYS but the contract-accept reveal got cancelled” surprise; mitigated by the reveal completing in ≤ 1 second by default. If the surprise pattern shows up in playtesting, ADR-update with an opt-in flag.
  • The :hover and :click interaction grammars from the inspiration source remain vapor. Cart authors who read docs/influences/effect/ascii-effects.md will see those grammars and have to discover via the stub pointer that only (reveal …) is canonical. The stub pointer is the mitigation; a future ADR can promote the :hover / :click surfaces if a real caller surfaces.
  • The docs/influences/effect/ascii-effects.md inspiration doc is preserved (not deleted) with a “promoted” notice pointing at the canonical doc. The interaction-grammar prose remains useful as historical / future-scope reference.
  • The libcaca and no-more-secrets influence-tier docs are unchanged; they remain the canonical engine + algorithm references and are cited from the canonical spec.

Per CLAUDE.md Spec Hygiene Rule 3, all canonical-spec edits land in this PR alongside the ADR. Grep sweep is a hard PR requirement.

  1. Create docs/software/api-reference/grammars/reveal-styles.md — the canonical spec. Done in this PR.
  2. Stub docs/influences/effect/ascii-effects.md with a “promoted to canonical” header pointing at reveal-styles.md. Done in this PR. The body of the inspiration doc is preserved below the stub header because the :hover / :click / (draw-ascii …) prose remains useful as future-scope reference; only the (reveal …) portions have moved.
  3. Amend ADR-0005 §“Tier 1: All-Carts Primitives” to add the four-row Reveal primitive family. Add an Amendment Log entry dated 2026-06-07. Done in this PR. Primitive count rises 57 → 61.
  4. Update docs/adr/README.md ADR index with the ADR-0033 row. Done in this PR.
  5. Run grep sweep: rg -i "reveal" docs/ produces clean results — every existing (reveal …) reference now resolves against either ADR-0033 or reveal-styles.md. Done in this PR; sweep notes in PR description.
  • Cart specs that name-drop (reveal …) (ICE Breaker, Depthcharge, Black Ledger, CIPHERgarden, Synth-Fence, Shellfire, Null, ~7 others) are not updated here. Their references resolve against this ADR by name; no rewording is required, and a cart-spec sweep would balloon the PR surface beyond Spec Hygiene Rule 3’s reasonable scope. Cart-spec updates happen lazily when each cart’s spec is next opened for other work.
  • The :hover / :click / (draw-ascii …) interaction grammars are not promoted. They remain inspiration-tier in docs/influences/effect/ascii-effects.md. A future ADR can promote them once a real caller surfaces.
  • docs/influences/inspiration/features-matrix.md is not updated. Items 45, 58–63 in that matrix already point at the inspiration source; the stub at the inspiration source now points operators forward to the canonical. The matrix is a tracking artifact, not a normative spec; lazy-update at next features-matrix revision.

Follow-on work (separate ADRs / Notion tasks)

Section titled “Follow-on work (separate ADRs / Notion tasks)”
  1. Implementation of the four primitives in the emulator + runtime (gameplay engineering task — separate Notion).
  2. Pi Zero 2 W bring-up benchmark of :char-flicker and :radial at full primary-display and CIPHER-LINE OLED surface sizes; update reveal-styles.md §6 with measured fps and any default revisions (embedded engineering task — separate Notion).
  3. Promote :diagonal once the boot-animation handoff and carousel tab inbound surfaces are wired through (reveal …).
  4. Promote :random after operator-eye review at hardware bring-up.
  5. Wire :fft once the PSG coprocessor exposes a per-frame frequency-bucket stream (depends on coprocessor-bridge work).
  6. Promote :hover / :click / (draw-ascii …) if a real cart caller surfaces.


2026-06-12 — Grid coordinates reconciled to the 128×75 canon (ADR-0027)

Section titled “2026-06-12 — Grid coordinates reconciled to the 128×75 canon (ADR-0027)”

What changed. This ADR was drafted against the 80×25 text grid / 960×600 logical framebuffer, which were the canonical surface at drafting time. ADR-0027 was ratified 2026-06-07 (the same day this ADR was accepted) and made 128 columns × 75 rows the canonical grid — Row 0 status / rows 1–73 content / Row 74 action, native 8×8 cells — and replaced ADR-0014’s BITMAP mode with a half-block 128×150 pseudo-pixel canvas (U+2580 / U+2584). The reveal targets are reconciled accordingly:

  • The performance-budget primary surface is 128×73 cells (was 80×23). The CIPHER-LINE OLED surface (4×32 cells) is unchanged.
  • The engine-reference target (§2.6) writes into the 128×75 cell grid (or the 128×150 half-block canvas for sub-pixel styles) rather than the retired 960×600 framebuffer.
  • The §“Related” and §6 References ADR-0014 entries are replaced with ADR-0027.
  • The matching ADR-0005 amendment entry (Tier 1 Reveal primitive family) is reconciled in the same PR.

The (reveal …) FFI contract — async lifecycle, style enum, surface-handle shape — is unchanged; only the display-surface dimensions the reveal renders into move.

Implementation note. Emulator code reconciles to 128×75 at the nOSh re-flow (tracked deviation, per CLAUDE.md Spec Hygiene).

Authority trail. ADR-0027 §“Decision” items 3–4 (128×75 grid, half-block 128×150 canvas); CLAUDE.md Canonical Hardware Specification (updated 2026-06-07).