ADR-0033: Reveal NoshAPI Primitive — Project-Wide ASCII Visual Language
1. Context
Section titled “1. Context”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:
- 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.
- 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. DECRYPT/INTERROGATE/ANALYZEverb response style — cart-author convention for certain TERM verbs.- Cart-load splash — title screens land with a radial reveal; CIPHERgarden, Black Ledger, ICE Breaker, Depthcharge all want this in their respective gameplay specs.
- The boot animation handoff to the first interactive frame —
BOOTSTRA.386lineage, top-left → bottom-right wipe. - CRT power-off shrink —
AetherTunelineage, inverse of radial reveal. - Carousel tab inbound on the Bare Deck Terminal —
blessed-contriblineage.
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.
Forcing functions
Section titled “Forcing functions”- 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 itsreveal-textpredecessor 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. - The
:char-flickeralgorithm has a GPL constraint. The reference implementation inlibnms(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. - The
(reveal …)surface is the natural anchor for the broader ASCII visual language. The fullascii-effects.mddoctrine carries three event types (:hover,:click,:reveal) and a(draw-ascii …)raster-to-text pipeline. Landing(reveal …)is the minimum-viable promotion. The:hoverand:clicksurfaces have no v0.1 use case (KN-86 has no mouse; the “hover” semantic translates to focus-cursor proximity which is itself underspecified — seedocs/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. - CLAUDE.md Spec Hygiene Rule 3 applies. This ADR amends
ADR-0005(primitive catalog) and promotesdocs/influences/effect/ascii-effects.mdto a new canonical home. Both edits need to land in the same PR with a grep sweep that confirms the corpus references resolve cleanly.
Constraints
Section titled “Constraints”- 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-0004is 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-0005is 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.
libnmsis GPL v3; no vendoring. Reimplementation from first principles — the algorithm is small (~50 lines per the source doc).
2. Decision
Section titled “2. Decision”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:
2.1 Primitive family
Section titled “2.1 Primitive family”Four primitives land together as Tier 1, available to every cart:
| Lisp name | Purpose |
|---|---|
reveal | Start an animated reveal on a cell surface. Returns a handle. |
reveal-cancel | Interrupt an in-flight reveal; snap surface to final state. |
reveal-complete? | Non-blocking poll; returns #t once the reveal has finished or been cancelled. |
unreveal | Inverse — 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-flickeris 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-secretsis 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.:radialis 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.:randomdefers because for small surfaces it visually overlaps:char-flickerand for large surfaces it visually overlaps:radial. The marginal aesthetic value is unclear without an operator-eye review on hardware. Reconsider at bring-up.:diagonaldefers 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.:fftdefers 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.
2.4 Cart-callable — confirmed
Section titled “2.4 Cart-callable — confirmed”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.
2.5 Pi Zero 2 W performance budget
Section titled “2.5 Pi Zero 2 W performance budget”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.
3. Options considered
Section titled “3. Options considered”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.
4. Consequences
Section titled “4. Consequences”Positive
Section titled “Positive”- 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.
Negative
Section titled “Negative”- Adds 4 primitives to the
ADR-0005catalog. Count rises from 57 to 61. The FFI surface grows; cart author cognitive load grows commensurately. Mitigated by the canonicalreveal-styles.mddoc and Tier 1 docs inADR-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,:fftnot 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:diagonalcases) will land in one frame until:diagonalships. 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
:hoverand:clickinteraction grammars from the inspiration source remain vapor. Cart authors who readdocs/influences/effect/ascii-effects.mdwill 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/:clicksurfaces if a real caller surfaces.
Neutral
Section titled “Neutral”- The
docs/influences/effect/ascii-effects.mdinspiration 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.
5. Documentation Updates
Section titled “5. Documentation Updates”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.
Edits required in this PR
Section titled “Edits required in this PR”- Create
docs/software/api-reference/grammars/reveal-styles.md— the canonical spec. Done in this PR. - Stub
docs/influences/effect/ascii-effects.mdwith a “promoted to canonical” header pointing atreveal-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. - 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. - Update
docs/adr/README.mdADR index with the ADR-0033 row. Done in this PR. - Run grep sweep:
rg -i "reveal" docs/produces clean results — every existing(reveal …)reference now resolves against either ADR-0033 orreveal-styles.md. Done in this PR; sweep notes in PR description.
Edits explicitly NOT taken in this PR
Section titled “Edits explicitly NOT taken in this PR”- 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 indocs/influences/effect/ascii-effects.md. A future ADR can promote them once a real caller surfaces. docs/influences/inspiration/features-matrix.mdis 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)”- Implementation of the four primitives in the emulator + runtime (gameplay engineering task — separate Notion).
- Pi Zero 2 W bring-up benchmark of
:char-flickerand:radialat full primary-display and CIPHER-LINE OLED surface sizes; updatereveal-styles.md§6 with measured fps and any default revisions (embedded engineering task — separate Notion). - Promote
:diagonalonce the boot-animation handoff and carousel tab inbound surfaces are wired through(reveal …). - Promote
:randomafter operator-eye review at hardware bring-up. - Wire
:fftonce the PSG coprocessor exposes a per-frame frequency-bucket stream (depends on coprocessor-bridge work). - Promote
:hover/:click/(draw-ascii …)if a real cart caller surfaces.
6. References
Section titled “6. References”docs/software/api-reference/grammars/reveal-styles.md— canonical spec, promoted by this ADR.ADR-0005— NoshAPI FFI surface enumeration; amended by this ADR to add the Reveal primitive family.docs/influences/synthesis.md§3 item 3, §6 item 7, §2 L6 — the cross-domain convergence that drove this promotion.docs/influences/effect/ascii-effects.md— inspiration-tier source; stubbed by this ADR with a pointer to the canonical doc.docs/influences/effect/libcaca.md— engine reference for the CPU-only on-device implementation.docs/influences/effect/react-video-ascii.md— interaction-grammar prop-matrix predecessor.docs/influences/research/no-more-secrets.md—:char-flickeralgorithm reference (GPL v3 — reimplement from first principles).ADR-0027— 128×75 cell grid + 128×150 half-block canvas the reveal targets (supersedes ADR-0014’s 960×600 framebuffer).ADR-0015— CIPHER-LINE OLED, secondary reveal target.ADR-0017— coprocessor envelope;:fftstyle depends on PSG-bridge work referenced here.CLAUDE.mdCanonical Hardware Specification — Display, Color, Battery rows constrain the performance budget.
7. Amendment Log
Section titled “7. Amendment Log”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).