ADR-0005: NoshAPI FFI Surface Enumeration
Supersedes spike: former spikes/ADR-0001-FFI-surface.md
Scope: All-carts, mission-context, and REPL-read-only capability tiers
Related: adr/ADR-0001-embedded-lisp-scripting-layer.md, adr/ADR-0014-display-profile-redesign.md, nosh.h, nosh_stdlib.h, nosh_cart.h
Summary
Section titled “Summary”This document enumerates every C function and primitive that the Lisp runtime must expose as built-in functions to cartridge code. Organized by capability tier: All-carts (universally available), Mission-context (only during active mission phases), and REPL-read-only (Lisp REPL can call for inspection, no mutations).
Source documents:
/kn86-emulator/src/nosh.h— display and sound API/kn86-emulator/src/nosh_stdlib.h— navigation, list ops, deck-state access, procedural generation/kn86-emulator/src/nosh_runtime.h— cell pool and navigation stack management/kn86-emulator/src/nosh_cart.h— macro signatures (now superseded, but API contracts remain valid)
Tier 1: All-Carts Primitives (Available Always)
Section titled “Tier 1: All-Carts Primitives (Available Always)”Display API
Section titled “Display API”Text mode
Section titled “Text mode”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
text-clear | () → nil | nosh_text_clear() | Clear text framebuffer (80x25 cells). Does not affect split-view bitmap area. | — |
text-putc | (col row char) → nil | nosh_text_putc(uint8_t col, uint8_t row, char c) | Place a single character at (col, row) in text grid. col: 0–79, row: 0–24. char: ASCII 0–127. | :out-of-range if col ≥ 80 or row ≥ 25 |
text-puts | (col row string) → nil | nosh_text_puts(uint8_t col, uint8_t row, const char *str) | Print a null-terminated string at (col, row), left-aligned. Stops at grid boundary. | :out-of-range if col ≥ 80 |
text-printf | (col row format-string arg1 arg2 ...) → nil | (Synthesized: formatted text-puts via snprintf + text-puts) | Printf-style formatted string at (col, row). Format specifiers: %d, %u, %x, %c, %s. Max 128 bytes output. | :format-error if format string is invalid; :out-of-range if col ≥ 80 |
text-scroll | (lines) → nil | nosh_text_scroll(int8_t lines) | Scroll text up (positive) or down (negative). lines: -25 to +25. | :out-of-range if abs(lines) > 25 |
text-cursor | (col row visible) → nil | nosh_text_cursor(uint8_t col, uint8_t row, bool visible) | Show/hide text cursor at (col, row). visible: #t or #f. | :out-of-range if col ≥ 80 or row ≥ 25 |
text-invert | (col row len) → nil | nosh_text_invert(uint8_t col, uint8_t row, uint8_t len) | Invert (XOR) text color for len characters starting at (col, row). For emphasis. | :out-of-range if col + len > 80 |
Graphics mode (bitmap, 960×600) (Amended 2026-04-24 per ADR-0014)
Section titled “Graphics mode (bitmap, 960×600) (Amended 2026-04-24 per ADR-0014)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
gfx-clear | () → nil | nosh_gfx_clear() | Clear bitmap framebuffer (all pixels off). | — |
gfx-pixel | (x y on) → nil | nosh_gfx_pixel(uint16_t x, uint16_t y, bool on) | Set or clear a pixel at (x, y). x: 0–959, y: 0–599. on: #t (on) or #f (off). | :out-of-range if x ≥ 960 or y ≥ 600 |
gfx-line | (x0 y0 x1 y1) → nil | nosh_gfx_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) | Draw line from (x0, y0) to (x1, y1) using Bresenham. | :out-of-range if any coord out of bounds |
gfx-rect | (x y w h filled) → nil | nosh_gfx_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool filled) | Draw rectangle at (x, y) with size (w, h). filled: #t → solid, #f → outline. | :out-of-range if x+w > 960 or y+h > 600 |
gfx-circle | (cx cy r) → nil | nosh_gfx_circle(uint16_t cx, uint16_t cy, uint16_t r) | Draw circle centered at (cx, cy) with radius r. | :out-of-range if circle exceeds bounds |
gfx-blit | (x y bitmap-bytes w h) → nil | nosh_gfx_blit(uint16_t x, uint16_t y, const uint8_t *bitmap, uint16_t w, uint16_t h) | Blit packed bitmap (1 bit per pixel, rows left-to-right, top-to-bottom) at (x, y). bitmap-bytes: byte array of (w*h+7)/8 bytes. | :out-of-range if x+w > 960 or y+h > 600; :invalid-bitmap if bytes insufficient |
Display mode control
Section titled “Display mode control”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
split-view | (bitmap-rows) → nil | nosh_split(uint16_t bitmap_rows) | Set split-view mode: top bitmap-rows pixels are graphics, remainder is text. bitmap-rows: 0–599. 0 = all text, 600 = all graphics. | :out-of-range if bitmap-rows > 600 |
display-mode | () → symbol | (Synthesized from internal state) | Query current display mode: :text, :graphics, or :split. Read-only. | — |
Sound API
Section titled “Sound API”PSG Control (YM2149 registers)
Section titled “PSG Control (YM2149 registers)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
psg-write | (reg val) → nil | nosh_psg_write(uint8_t reg, uint8_t val) | Write to PSG register reg (0–14) with value val (0–255). Directly controls 3 tone generators, noise, envelope. Use with caution. | :invalid-register if reg > 14; :invalid-value if val > 255 |
psg-read | (reg) → integer | nosh_psg_read(uint8_t reg) | Read current value of PSG register reg (0–14). Returns 0–255. | :invalid-register if reg > 14 |
Higher-level sound abstractions
Section titled “Higher-level sound abstractions”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
sound-tone | (channel freq vol) → nil | nosh_sound_tone(uint8_t channel, uint16_t frequency, uint8_t volume) | Play tone on channel (0–2) at frequency (1–50000 Hz) at volume (0–15). Clamps to PSG limits. | :invalid-channel if channel > 2; :invalid-freq if freq = 0; :invalid-volume if vol > 15 |
sound-noise | (period) → nil | nosh_sound_noise(uint8_t period) | Set noise period (0–31). 0 = off, higher = lower pitch/slower clock. | :invalid-period if period > 31 |
sound-envelope | (shape period) → nil | nosh_sound_envelope(uint8_t shape, uint16_t period) | Set envelope shape (0–15, bits 0–3 = hold/alternate/attack/hold) and period. PSG-native. | :invalid-shape if shape > 15 |
sound-silence | () → nil | nosh_sound_silence() | Mute all sound (set all channels vol to 0, turn off noise). | — |
Sound Effects (pre-mixed SFX)
Section titled “Sound Effects (pre-mixed SFX)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
sfx-keyclick | () → nil | nosh_sfx_keyclick() | Play short click sound (key confirmation). ~50 ms, 1000 Hz. | — |
sfx-boot | () → nil | nosh_sfx_boot() | Play boot sequence (ascending tones). ~300 ms. | — |
sfx-select | () → nil | stdlib_sfx_select() (via nosh_cart.h) | Play selection sound (short beep). | — |
sfx-confirm | () → nil | stdlib_sfx_confirm() | Play confirmation sound (higher tone). | — |
sfx-error | () → nil | stdlib_sfx_error() | Play error buzz (low, harsh). | — |
sfx-alert | () → nil | stdlib_sfx_alert() | Play alert ping (high tone). | — |
Cell Pool & Navigation
Section titled “Cell Pool & Navigation”Cell spawning and lifecycle
Section titled “Cell spawning and lifecycle”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
spawn-cell | (type-symbol) → cell | runtime_spawn_cell(SystemState *state, uint16_t type_id) | Allocate a new cell of the given type (e.g., ‘contract, ‘datum, ‘link). Type must be registered in cartridge CART_INIT. Returns cell object or nil if pool exhausted. | :unknown-type if type not registered; :pool-exhausted if no more cells available |
destroy-cell | (cell) → nil | runtime_destroy_cell(SystemState *state, void *cell) | Return cell to the free pool. Cell object becomes invalid after this call. | :invalid-cell if cell is null or corrupted |
Navigation stack
Section titled “Navigation stack”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
drill-into | (target-cell) → nil | runtime_drill_into(SystemState *state, CellBase *target) | Push target onto nav stack. Calls ON_EXIT on current cell, ON_ENTER on target. | :invalid-cell if target is null; :nav-stack-overflow if depth exceeds 32 |
navigate-back | () → nil | runtime_navigate_back(SystemState *state) | Pop nav stack. Calls ON_EXIT on leaving cell, ON_ENTER on returning cell. Fails if stack is at root. | :nav-stack-underflow if already at root |
current-cell | () → cell | runtime_current_cell(SystemState *state) | Return the cell at the top of the nav stack. Returns nil if stack is empty. | — |
set-root | (root-cell) → nil | runtime_set_root(SystemState *state, CellBase *root_cell) | Reset nav stack: clear all, push root-cell. Usually called once at cartridge init. | :invalid-cell if root-cell is null |
Navigation helpers
Section titled “Navigation helpers”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
next-sibling | () → nil | stdlib_next_sibling(SystemState *state) | Move cursor to next sibling (CDR direction) in current list. No-op if already at last sibling. | — |
prev-sibling | () → nil | stdlib_prev_sibling(SystemState *state) | Move cursor to previous sibling. No-op if already at first sibling. | — |
List Operations (via sibling chains)
Section titled “List Operations (via sibling chains)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
list-push | (parent cell) → nil | stdlib_list_push(CellBase *parent, CellBase *cell) | Append cell as child of parent. cell is linked into parent’s child chain. | :invalid-cell if parent or cell is null |
list-get | (parent index) → cell | stdlib_list_get(CellBase *parent, int index) | Get Nth child of parent (0-indexed). Returns nil if index out of range. | — |
list-length | (parent) → integer | stdlib_list_length(CellBase *parent) | Count children of parent. Returns 0 if parent is a leaf. | :invalid-cell if parent is null |
is-leaf | (cell) → bool | stdlib_is_leaf(CellBase *cell) | Test if cell has no children. Returns #t or #f. | — |
link-cells | (parent child) → nil | stdlib_link_cells(CellBase *parent, CellBase *child) | Create explicit parent-child relation. Equivalent to list-push. | :invalid-cell if parent or child is null |
unlink-cell | (cell) → nil | stdlib_unlink_cell(CellBase *cell) | Remove cell from its parent and sibling chain. Cell becomes orphaned but not destroyed. | :invalid-cell if cell is null or has no parent |
Procedural Generation (LFSR)
Section titled “Procedural Generation (LFSR)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
lfsr-seed | (seed) → nil | stdlib_lfsr_seed(SystemState *state, uint32_t seed) | Initialize the LFSR with a 32-bit seed (typically seeded from deck state or mission parameters). | — |
lfsr-next | () → integer | stdlib_lfsr_next(SystemState *state) | Advance LFSR, return next pseudo-random 32-bit value (0–4294967295). | — |
lfsr-range | (min max) → integer | stdlib_lfsr_range(SystemState *state, uint32_t min, uint32_t max) | Return pseudo-random integer in [min, max] inclusive. Accepts negative ranges. | :invalid-range if min > max |
lfsr-shuffle | (list) → list | lisp_lfsr_shuffle (bridge wrapper over stdlib_lfsr_range) | Fisher-Yates shuffle over a Fe list. Returns a freshly-allocated list; input list is not mutated. Empty and single-element lists return without advancing LFSR. Capped at 256 elements (returns nil if exceeded). Cost: O(n) arena cons cells. Prefer to call once per phase. (Amended 2026-04-26 per GWP-247 — see Amendment Log) | nil if n > 256 |
Pointing / Trackpoint (Added 2026-06-07 per ADR-0035)
Section titled “Pointing / Trackpoint (Added 2026-06-07 per ADR-0035)”Cart-facing surface for the 2× trackpoint hardware committed in ADR-0032. Both physical trackpoints aggregate at the master KB2040 over QMK split-pointing-device transport and emit as one logical cursor; the cart sees the merged cursor in cell coordinates on the main 128×75 grid. Cursor renders on cartridge content rows (1–73) only — clamped at Row 0 / Row 74 boundaries per CLAUDE.md Spec Hygiene Rule 5; does not render on the CIPHER-LINE auxiliary OLED. Visibility defaults to visible; cart hides via the :pointer-hidden manifest flag (cart-load default) or cursor-visible! runtime FFI. Per-trackpoint differentiation (pointer-a / pointer-b) is queued for v2. Click semantics (tap-vs-drag threshold) are firmware-defined per ADR-0032 §4; right-click and middle-click are deferred to v0.2. See ADR-0035 for the full decision record. (Grid coordinates reconciled to the 128×75 canon 2026-06-12 per ADR-0027 — was 80×25 / rows 1–23 / Row 24; see Amendment Log.)
| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
cursor-position | () → (col row) | nosh_cursor_position() (synthesized; reads from runtime cursor-state record) | Query current cursor cell on the main 128×75 grid. Returns a 2-element list (col row) where col: 0–127 and row: 1–73 (cursor clamped to cartridge content area). Position is logical and independent of visibility — a hidden cursor still has a position. O(1); never raises. | — |
on-trackpoint-move | (handler) → nil | nosh_on_trackpoint_move(...) (synthesized; registers Lisp lambda in runtime cursor-state) | Register handler invoked when the cursor crosses a cell boundary. Handler signature: (handler col row delta-col delta-row). delta-col / delta-row are integer cell deltas from the previous position (can be negative; magnitude > 1 if motion exceeds per-tick sampling). One handler per cart; re-register replaces; pass nil to unregister. Sustained against-boundary push does not re-fire the handler. Handler runs on the runtime event loop; exceptions are silently logged. | :invalid-handler if handler is non-nil and not a callable lambda |
on-trackpoint-click | (handler) → nil | nosh_on_trackpoint_click(...) (synthesized) | Register handler invoked on click. Handler signature: (handler col row). v0.1 fires exactly when the QMK PS/2-mouse driver emits a primary-button click (tap-vs-drag threshold is firmware-defined per ADR-0032 §4 and not re-specified here). One handler per cart; re-register replaces; pass nil to unregister. Right-click / middle-click deferred to v0.2 — no button-index argument in v0.1. | :invalid-handler if handler is non-nil and not a callable lambda |
cursor-visible! | (visible) → nil | nosh_cursor_visible(bool visible) (synthesized) | Show (#t) or hide (#f) the cursor for the lifetime of the cart load. Overrides the manifest :pointer-hidden default. Independent of position — toggling visibility does not move the cursor. Cart unload restores the runtime default (visible) for the next cart. | :invalid-bool if visible is not #t or #f |
Deck State Access (Read-only in this tier; mutations in Mission-context tier)
Section titled “Deck State Access (Read-only in this tier; mutations in Mission-context tier)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
deck-state | () → deck-state-object | stdlib_deck_state(SystemState *state) | Get reference to universal deck state. Object has fields: :handle, :credits, :reputation, :cartridge-history, :phase-chain. | — |
get-handle | () → string | (Synthesized: deck_state()->handle) | Get operator’s handle (callsign). Max 16 chars. Read-only. | — |
get-credits | () → integer | (Synthesized: deck_state()->credit_balance) | Get current credit balance. Read-only. | — |
get-reputation | () → integer | (Synthesized: deck_state()->reputation) | Get current reputation points. Read-only. | — |
has-capability | (bit-index) → bool | stdlib_has_capability(SystemState *state, uint8_t bit) | Test if cartridge capability bit is set (0–31). cartridge_history is a 32-bit bitmask tracking which modules have been completed. | :invalid-bit if bit ≥ 32 |
get-aesthetic-mode | () → symbol | stdlib_get_aesthetic_mode(SystemState *state) | Get the active aesthetic mode. Returns one of :amber, :white, :green (the runtime-locked phosphor roster per ADR-0034 §1 as amended 2026-06-13 per ADR-0036; previous :amber / :amber / :cipher retired). Read-only — carts cannot mutate the mode. Cheap (single SystemState field read). Default if nosh-config.toml [aesthetic].mode is missing or invalid: :amber. | — |
Display Helpers (stdlib)
Section titled “Display Helpers (stdlib)”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
draw-threat-bar | (level max-level col row) → nil | stdlib_draw_threat_bar(uint8_t level, uint8_t max_level, uint8_t col, uint8_t row) | Draw threat bar: level/max_level filled blocks (■) out of max_level total. Max 16 blocks. Placed at (col, row) in text grid. | :out-of-range if max_level > 16 or col+max_level > 80 |
draw-progress-bar | (percentage col row width) → nil | stdlib_draw_progress_bar(uint8_t pct, uint8_t col, uint8_t row, uint8_t width) | Draw progress bar: pct% filled (0–100). width: 1–32 chars. Placed at (col, row) in text grid. | :invalid-percentage if pct > 100; :invalid-width if width > 32 or col+width > 80 |
draw-bordered-box | (col row w h title) → nil | stdlib_draw_bordered_box(uint8_t col, uint8_t row, uint8_t w, uint8_t h, const char *title) | Draw a box outline with optional title. w, h: 3–77, 2–24. title: optional, centered. | :out-of-range if col+w > 80 or row+h > 25; :invalid-size if w < 3 or h < 2 |
Reveal animation primitives (Added 2026-06-07 per ADR-0033)
Section titled “Reveal animation primitives (Added 2026-06-07 per ADR-0033)”Unified (reveal :style …) surface for animated presentation of static text / ASCII content. Canonical contract lives at docs/software/api-reference/grammars/reveal-styles.md; v0.1 styles are :char-flicker, :no-more-secrets (alias), :radial. Asynchronous — calls return a handle; cart polls or ignores. Operator keypress auto-cancels all active reveals for the current cart.
| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
reveal | (surface :style style-sym &key duration rate trail from) → handle | nosh_reveal(...) (synthesized) | Start an animated reveal on the surface (cell handle). :style is one of the symbols in reveal-styles.md §3. Returns an opaque reveal handle. Lifecycle is async per reveal-styles.md §5. Out-of-range scalar params (:duration, :rate, :trail, :from) silently clamp per the silent-clamp convention. | :reveal-style-not-implemented if a deferred style (:random, :diagonal, :fft) is passed in v0.1; :reveal-unknown-style if the symbol is not in the enum; :invalid-surface if surface is nil; :reveal-too-many-active if > 4 handles already active for the current cart |
reveal-cancel | (handle) → nil | nosh_reveal_cancel(...) (synthesized) | Interrupt the animation, snap surface to final state. Idempotent on completed handles. | — |
reveal-complete? | (handle) → bool | nosh_reveal_complete_p(...) (synthesized) | Non-blocking poll; returns #t once the reveal has finished or been cancelled. | — |
unreveal | (surface :style style-sym &key duration) → handle | nosh_unreveal(...) (synthesized) | Inverse — animate removal of a surface. Same style enum as reveal minus :char-flicker / :no-more-secrets (which don’t compose as inverses). | Same as reveal minus the char-flicker-specific paths |
REPL exposure: Tier 3 is read-only per existing rule. REPL invocation of any reveal primitive is a no-op stub returning nil and logging to stderr. See reveal-styles.md §7.
Tier 2: Mission-Context Primitives (Active phase handler or Mission Runner)
Section titled “Tier 2: Mission-Context Primitives (Active phase handler or Mission Runner)”These are available when a mission phase is active and the caller is executing in mission context. Two callers qualify:
- A cart phase handler — invoked from inside a
(load-capability ...)call (the cart’s own ON_PHASE_TICK / ON_PHASE_END / etc.). - The Mission Runner — the operator’s REPL/nEmacs Lisp environment after
[EVAL]-acceptance (per ADR-0029).
Calling from a non-mission context raises :not-in-mission.
Mission progression
Section titled “Mission progression”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
phase-advance | () → nil | nosh_phase_advance() (synthesized) | Advance to next phase in the mission phase chain. Triggers ON_EXIT in current phase, ON_ENTER in next. If this is the last phase, calls mission_complete. | :not-in-mission if not in active phase context; :no-next-phase if already at last phase |
mission-complete | () → nil | nosh_mission_complete() (synthesized) | Alias for complete-mission when called from a cart phase handler with no explicit struct argument. | :not-in-mission if not in active phase context |
award-credits | (amount) → nil | stdlib_credit_add() | Add credits to deck balance. amount can be negative (penalty). | — |
modify-reputation | (delta) → nil | stdlib_rep_modify(SystemState *state, int16_t delta) | Modify reputation by delta (positive = gain, negative = loss). Clamped at [0, 32767]. | — |
Mission Runner — capability calls (added 2026-05-03 per ADR-0029)
Section titled “Mission Runner — capability calls (added 2026-05-03 per ADR-0029)”Available only to the Mission Runner. A cart phase handler that calls these from inside its own load-capability invocation raises :nested-capability-call.
| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
load-capability | (:module module-symbol &key seed input) → result-struct | nosh_load_capability(...) (synthesized) | Invoke the named cart’s gameplay loop. If the cart is registered but not currently inserted, emit a Hot Swap prompt and block until inserted (or aborted). On return, marshal the cart’s outcome into a result struct. See ADR-0029 §3 and mission-control.md §5.3. | :capability-not-registered if the module’s bit is not set in cartridge_history; :capability-aborted if operator aborted Hot Swap; :not-in-mission if no contract is active; :nested-capability-call if invoked from a cart phase handler already running under load-capability |
complete-mission | (mission-struct) → nil | nosh_complete_mission(...) (synthesized) | Resolve the active contract: calculate final payout from accumulated phase outcomes, apply reputation delta, write the cartridge provenance MISSION block, unbind Mission Runner symbols, return to the mission board. | :not-in-mission if no contract is active |
abandon-mission | () → nil | nosh_abandon_mission() (synthesized) | Resolve the active contract as :result-failure, apply the reputation penalty, refresh the board with a lower-threat alternative for the abandoned slot, unbind Mission Runner symbols. | :not-in-mission if no contract is active |
Capability-call result struct
Section titled “Capability-call result struct”The result struct returned by load-capability is a Lisp record / property list with the following canonical shape:
| Field | Type | Meaning |
|---|---|---|
:outcome | symbol — one of :success, :failure, :partial, :abort | Headline result. :partial means the cart finished but did not satisfy all bonus conditions. :abort means the operator interrupted via [NIL] mid-call. |
:trace | integer (0–N, cart-defined) | Final trace / threat / heat counter at exit. Lower is better for most domains; cart docs the scale. |
:extracted | list of symbols | Domain-specific tokens the cart produced (e.g., (:network-map :target-data) from ICE Breaker, (:audit-trail) from Black Ledger). The Mission Runner program branches on these to drive subsequent phases. |
:turns | integer | Number of OODA / cell-tick turns the operator took inside the call. Drives speed bonuses. |
:bonuses | list of symbols | Bonus condition tokens earned (e.g., (:speed :stealth)). Cart-defined; consumed by the contract’s payout formula. |
Future fields are additive — the Mission Runner accesses fields by key, so older Runner programs see new fields as nil. See mission-control.md §5.3 and ADR-0029 §3 OQ-2.
Mission phase metadata
Section titled “Mission phase metadata”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
set-phase-data | (key value) → nil | (Synthesized: mission context dict) | Set arbitrary key-value pair in current phase’s persistent data. Survives phase boundaries within a multi-phase mission. | :not-in-mission if not in active phase context |
get-phase-data | (key) → value | (Synthesized: mission context dict) | Retrieve phase data by key. Returns nil if not set. | :not-in-mission if not in active phase context |
set-capability | (bit-index) → nil | stdlib_set_capability(SystemState *state, uint8_t bit) | Set cartridge capability bit (0–31). Marks this module as completed. | :invalid-bit if bit ≥ 32; :not-in-mission if not in active phase context |
Tier 3: REPL-Read-Only Primitives (Player-facing Lisp REPL, ADR-0002)
Section titled “Tier 3: REPL-Read-Only Primitives (Player-facing Lisp REPL, ADR-0002)”Available in the built-in Lisp REPL only (read-only inspection). No mutations. Subset of Tier 1 + Tier 2 read operations.
| Category | Primitives |
|---|---|
| Display (read-only) | display-mode, get-handle, get-credits, get-reputation, has-capability |
| Navigation (inspect) | current-cell, list-get, list-length, is-leaf, deck-state |
| Procedural (generate) | lfsr-seed, lfsr-next, lfsr-range, lfsr-shuffle |
| Forbidden | All mutations: text-*, gfx-*, sound-*, spawn-cell, destroy-cell, drill-into, navigate-back, phase-advance, mission-complete, award-credits, modify-reputation, set-capability |
Rationale: REPL is for exploration, debugging, and scripted automation (ADR-0002). Mutations would corrupt mission state. Sound and display in REPL are out of scope for MVP.
Type Mapping: Lisp ↔ C
Section titled “Type Mapping: Lisp ↔ C”| C Type | Lisp Representation | Notes |
|---|---|---|
void | nil | No return value |
bool | #t or #f | True/false |
uint8_t, uint16_t, uint32_t | integer (0–2^N-1) | No explicit type; range-checked in C |
int8_t, int16_t, int32_t | integer | Signed; range-checked in C |
float, double | (not supported in MVP) | Deferred to Phase 2 if needed |
const char * (null-terminated) | string | Immutable; null-term guaranteed by C side |
CellBase * | cell (opaque object) | Lisp cannot inspect fields directly; only pass to cell functions |
CellList | (via list-* functions) | Lisp sees lists as parent-child relations, not explicit list objects |
DeckState * | deck-state-object (record) | Lisp reads fields via get-* functions or accessor pattern |
Dispatch Contract: Handler execution model
Section titled “Dispatch Contract: Handler execution model”When a cell event fires (e.g., player presses CAR key on contract cell):
- Runtime looks up cell type → finds handler
- If handler is C function pointer (runtime cells): call directly, blocking
- If handler is Lisp lambda reference (cartridge cells): marshal args, invoke Fe evaluator, return result
- Dispatch latency is transparent: Lisp handlers have same latency contract as C handlers (5 ms target, 10 ms ceiling)
This is the tagged-union approach from ADR-0001. Cell grammar (v2.0 spec) will define how handlers are authored in Lisp; the FFI layer is agnostic to their origin.
Implementation Notes
Section titled “Implementation Notes”Macros vs. Functions
Section titled “Macros vs. Functions”v1.1 cartridge grammar used C macros (e.g., spawn_cell(typename)) for compile-time type safety. In Lisp:
- No compile-time type safety; we rely on runtime argument validation
- Each primitive validates arg count and types at invocation
- Example:
(spawn-cell 'contract)— symbol ‘contract is looked up in type registry at runtime
Cell ownership & arena-reset semantics
Section titled “Cell ownership & arena-reset semantics”With arena allocation (Fe VM — mark-sweep GC over a fixed object pool; no heap growth, no unbounded pause; see ADR-0004 2026-06-14 amendment):
- Cell allocation is cheap (object-pool slot + mark-sweep reclaim, not a heap malloc)
- Cell lifetime is bounded by mission instance or cartridge load boundary
- If a cartridge spawns cells and resets the arena, all cells are implicitly freed
- This is safe as long as cartridges don’t hold references across arena resets
- The mission-phase boundary is the natural reset point
String handling
Section titled “String handling”C strings are null-terminated; Lisp strings are Fe’s native string type (immutable). FFI:
- Cartridge Lisp → C: Lisp string marshaled to C const char * (pointer + length)
- C → Lisp: C string (char *) wrapped in Lisp string object
- Max string length in cartridge: limited by arena size; typically 256–1024 bytes per string. (Per-cart arena default is 256 KB — see ADR-0004 Amendment 2026-04-27 / GWP-233. Working-set fragmentation, not raw arena size, sets the practical per-string ceiling.)
Known Unknowns / Follow-ups
Section titled “Known Unknowns / Follow-ups”-
Array mutation in Lisp (CLOSED 2026-04-26):
lfsr-shuffleoriginally flagged as unknown because the C implementation mutates avoid **array in-place and Lisp is functional. Resolved by GWP-247: the bridge wrapperlisp_lfsr_shufflewalks the Fe list to collect element pointers into a stack-local array, performs Fisher-Yates in-place on that scratch array usingstdlib_lfsr_range, then re-cons’s a fresh list from the shuffled order. Input list is never touched. This is idiomatic Lisp (no mutation) and requires zero changes tonosh_stdlib.c. See Amendment Log 2026-04-26. -
Error handling: how deep are error traces? If a Lisp handler calls
text-putswith invalid args, does the error propagate to the player? Or silent clamping? Current design: silent clamping (defensive). May need refinement based on gameplay patterns. -
Mission phase context: “not in mission” errors are runtime only. Cartridge author cannot check at load time. Documentation and testing are critical.
-
Cipher integration: ADR-0002 scopes Cipher (voice NPC) as a Lisp-integrated system. Cipher handlers are Lisp lambdas too. They’re not explicitly listed here — they’re instances of Tier 1 (all-carts) primitives called from Cipher mission context.
Summary
Section titled “Summary”61 primitives enumerated across three tiers (54 original + 3 added 2026-05-03 per ADR-0028/ADR-0029 + 4 added 2026-06-07 per ADR-0033). All map cleanly to existing or planned C APIs (nosh.h, nosh_stdlib.h, nosh_runtime.h). No new C functions required for the original 54 — just FFI wrapping. The 2026-05-03 capability-call primitives and 2026-06-07 reveal primitives are net-new C entry points (synthesized signatures shown). Lisp cartridges author in kebab-case; C side is snake_case (conventional).
Ready for FFI binding implementation (Phase 2 action item) and Fe integration prototype.
Amendment Log
Section titled “Amendment Log”2026-04-24 — Bitmap-FFI bounds retarget per ADR-0014 F4
Section titled “2026-04-24 — Bitmap-FFI bounds retarget per ADR-0014 F4”Status effect: Accepted (unchanged). Amendment pattern follows the ADR-0006 precedent (2026-04-22): new **Amended:** header line + this log section + in-place contract updates; no change to Decision or Options Considered.
Rationale. ADR-0014 (Accepted 2026-04-22) redefined the KN-86’s canonical logical framebuffer from an implicit 1024×600 (which was never implemented cleanly — see ADR-0014 §Context) to a 960×600 logical framebuffer integer-scaled into the 1024×600 Elecrow panel with a 32 px horizontal letterbox per side and zero vertical letterbox. Cartridges see 960×600 as their full bitmap canvas; the 32 px letterbox is invisible to them. ADR-0014 action item F4 explicitly required sweeping cartridge-authoring docs to restate the canvas as 960×600. ADR-0005 is the normative FFI contract for cartridge Lisp; it qualifies as the primary cartridge-authoring doc in F4’s scope.
Contract changes. Four cells in the #### Graphics mode table (and its header) were retargeted. Original values shown struck-through; current values are live in the table above.
| Location | Before | After |
|---|---|---|
| Section header | #### Graphics mode (bitmap, 1024×600) | #### Graphics mode (bitmap, 960×600) |
gfx-pixel contract | x: 0–1023, y: 0–599 | x: 0–959, y: 0–599 |
gfx-pixel raises | if x ≥ 1024 or y ≥ 600 | if x ≥ 960 or y ≥ 600 |
gfx-rect raises | if x+w > 1024 or y+h > 600 | if x+w > 960 or y+h > 600 |
gfx-blit raises | if x+w > 1024 or y+h > 600 | if x+w > 960 or y+h > 600 |
The uint16_t coordinate type remains correct (960 and 600 both fit in a u16). The vertical axis (0–599, y ≥ 600) is unchanged because ADR-0014’s vertical dimension is unchanged. gfx-line and gfx-circle cite “any coord out of bounds” / “circle exceeds bounds” without naming the numeric limit, so no cell-text change is required; the bounds they reference now derive from the amended gfx-pixel and gfx-rect tables.
Scope of this amendment. This is a value fix to the FFI contract, not a design-commitment change. The FFI’s shape — kebab-case Lisp names, snake_case C references, three capability tiers, 54 primitives — is unchanged. Downstream consumers (cartridge authors, the emulator’s FFI binding implementation, and ADR-0007’s scripted-mission extensions) inherit the tighter x-axis bound automatically; existing cart code that wrote to x ≥ 960 was already out of spec against ADR-0014’s framebuffer even while the ADR-0005 table said otherwise, so the amendment closes an inconsistency rather than creating one.
Authority trail. CLAUDE.md Canonical Hardware Specification (Display modes row: BITMAP (960×600)) and ADR-0014 (Decision, Action Items §F4) are the upstream sources. Prior cartridge-authoring sweeps fixed by Run 2026-04-24T11:17:53Z (see docs/_archive/AUDIT-LOG.md): UI Design System, Null / Drift gameplay specs, Definitive Guide NoshAPI summary, Prototype Architecture bitmap-mode paragraph. ADR-0005 was escalated as E12 in that run and ratified by Josh the same day.
2026-04-25 — Tier 1 graphics + sound primitives bound to Lisp (GWP-242, GWP-243)
Section titled “2026-04-25 — Tier 1 graphics + sound primitives bound to Lisp (GWP-242, GWP-243)”Status effect: Accepted (unchanged). Confirms the binding side of the contract; no contract-text edits.
The thirteen Tier 1 primitives that previously had C implementations in nosh.c / sound.c but no Fe binding are now wired into nosh_lisp_bridge.c and exposed to cartridge Lisp:
- Graphics (6):
gfx-clear,gfx-pixel,gfx-line,gfx-rect,gfx-circle,gfx-blit— GWP-242. - Sound (7):
psg-write,psg-read,sound-tone,sound-noise,sound-envelope,sound-silence,sfx-boot— GWP-243.
Range raises documented in the contract tables (:out-of-range, :invalid-bitmap, :invalid-register, :invalid-value, :invalid-channel, :invalid-freq, :invalid-volume, :invalid-period, :invalid-shape) all land as silent-clamp drops at the bridge layer per Known Unknown #2 — the underlying C entry point is never invoked, the bridge logs to stderr, and the call returns the truthy nil sentinel so caller code stays unbroken. psg-read is the lone exception: a dropped read returns Fe nil rather than the truthy sentinel, since no value can be honestly synthesized for the caller.
Coverage lives in kn86-emulator/tests/test_nosh_lisp_gfx.c (15 cases, including the (w*h+7)/8 byte-budget check for gfx-blit) and kn86-emulator/tests/test_nosh_lisp_sound.c (15 cases, including a per-test PSG/sound mock that asserts register writes reach the underlying state). No edits to nosh.c, nosh.h, sound.c, psg.c, sfx.c, display.c, or oled.c were required — the C side was already correct.
2026-05-03 — Tier 2 capability-call primitives + result struct (ADR-0028, ADR-0029)
Section titled “2026-05-03 — Tier 2 capability-call primitives + result struct (ADR-0028, ADR-0029)”Status effect: Accepted (unchanged). Adds three Tier 2 primitives and the canonical result-struct shape. Extends Tier 2’s authorized callers from “active phase handler” to “active phase handler or Mission Runner.”
Rationale. ADR-0028 introduces Mission Control (named runtime subsystem owning contract generation); ADR-0029 introduces the Mission Runner (Lisp REPL/nEmacs environment with mission-context bindings, the post-acceptance host for ADR-0007 scripted missions). The Mission Runner needs an FFI primitive to invoke a cart’s gameplay loop on demand and receive a structured result back. That primitive is load-capability. Paired with it: complete-mission and abandon-mission resolve the active contract from the Runner.
Contract changes. §“Tier 2: Mission-Context Primitives” gains:
- Authorized callers broadened to (1) cart phase handlers and (2) the Mission Runner. Both raise
:not-in-missionoutside their respective contexts. - Three new primitives in a new §“Mission Runner — capability calls” subsection:
load-capability,complete-mission,abandon-mission. Signatures, raises, and semantic contracts are documented in the table. - Capability-call result struct documented as a canonical Lisp record / property list with five fields:
:outcome,:trace,:extracted,:turns,:bonuses. Future fields are additive — Mission Runner programs access by key, so older code sees new fields as nil. - Mission-progression row clarifications:
mission-completeis now an alias forcomplete-missionwhen called from a cart phase handler with no explicit struct argument.phase-advancesemantics unchanged but its caller migrates from “the runtime” to “the Mission Runner” as part of multi-phase contract orchestration.
:nested-capability-call is added as a new error symbol for the case of a cart phase handler calling load-capability from inside its own load-capability invocation. Reactive handlers from inside the runner during a capability call (mission-control.md §7 OQ-4) remain out of scope; capability calls are opaque to the runner in v1.
Scope. This is an additive amendment. The 54-primitive count from the original ADR rises to 57. The three capability tiers (All-Carts / Mission-Context / REPL-Read-Only) are unchanged. Tier 3 still forbids mutations — the new primitives sit firmly in Tier 2.
Authority trail. ADR-0028 §Decision item 3 (capability registration declares the bit + vocabulary that load-capability keys off); ADR-0029 §Decision items 2 and 3 (Mission Runner bindings; opaque capability calls). mission-control.md §5.3 is the user-facing prose specification for load-capability and the result struct. Engineering follow-up: kn86-emulator/src/nosh.c implementation per ADR-0029 §Follow-on work; tests/test_mission_runner.c for binding lifecycle.
2026-04-26 — lfsr-shuffle wrapper finalized (GWP-247)
Section titled “2026-04-26 — lfsr-shuffle wrapper finalized (GWP-247)”Status effect: Accepted (unchanged). Closes Known Unknown #1. In-place contract row update on §“Procedural Generation (LFSR)”.
Rationale. Multiple in-flight cartridge specs (Depthcharge intel scan ordering, ICE Breaker decoy fan-out, Bareterm contract roll, Null cartridge corruption draws) require a deterministic shuffle that respects the LFSR seed. Cart authors could previously compose this from lfsr-next + manual Fisher-Yates in Lisp, but that wastes Fe arena cycles per draw and risks 14 carts each authoring subtly different variants. One canonical primitive is cleaner.
The known unknown was the shape mismatch: stdlib_lfsr_shuffle mutates a void ** C array in-place, which is not usable directly from Lisp. Resolution: the bridge wrapper lisp_lfsr_shuffle (in nosh_lisp_bridge.c) walks the input Fe list to collect element pointers into a stack-local scratch array (capped at 256), runs Fisher-Yates using existing stdlib_lfsr_range calls, then constructs a fresh cons list from the shuffled pointers. The input list is never modified; element identity is preserved (no deep copy). nosh_stdlib.c is not modified.
Lisp signature: (lfsr-shuffle list) → list
Edge-case contracts:
- Empty list (nil): returns nil immediately; LFSR state is not advanced.
- Single-element list: returns a fresh single-cons; LFSR state is not advanced.
- n > 256: returns nil with a stderr log; no LFSR advance.
- Carts wanting reproducible output must call
(lfsr-seed N)first — same contract aslfsr-next/lfsr-range.
Arena note: each call costs O(n) new cons cells in the active Fe arena. The arena resets at mission-instance boundaries, bounding cost for the expected usage pattern. Prefer to shuffle once per phase and bind the result; do not call in tight redraw loops.
Coverage: kn86-emulator/tests/test_lisp_lfsr_shuffle.c — 7 cases: empty list, single element, two-element determinism, seed variation, 16-element regression guard, mutation guard, permutation integrity. All pass; overall ctest suite remains green.
2026-06-07 — Tier 1 “Pointing / Trackpoint” sub-section added (ADR-0035)
Section titled “2026-06-07 — Tier 1 “Pointing / Trackpoint” sub-section added (ADR-0035)”Status effect: Accepted (unchanged). Additive amendment. Tier 1 (All-Carts) gains four primitives in a new “Pointing / Trackpoint” sub-section placed between §“Procedural Generation (LFSR)” and §“Deck State Access.” Total primitive count: 61 → 65 (applied after the ADR-0033 Reveal-family amendment landed earlier the same day). The three capability tiers (All-Carts / Mission-Context / REPL-Read-Only) are unchanged.
Rationale. ADR-0032 (2026-06-07) committed 2× holykeebs trackpoint modules (Sprintek SK8707-01, one per index finger) as v0.1 hardware, aggregated to one logical cursor at the Pi via QMK split-pointing-device transport. ADR-0032 §5 originally deferred cart-FFI exposure to v2; ADR-0035 (2026-06-07) reversed that deferral and committed the FFI surface for v0.1 — the small surface (one position query + two event handlers + one visibility toggle) doesn’t justify v2 deferral given the hardware ships in v0.1.
Contract changes. New sub-section under §“Tier 1: All-Carts Primitives”:
cursor-position— query the cursor cell on the main 128×75 grid; returns(col row)in cell coordinates (col: 0–127, row: 1–73; cursor clamped to cartridge content area per CLAUDE.md Spec Hygiene Rule 5). Logical position is independent of visibility — a hidden cursor still has a position. Never raises.on-trackpoint-move— register handler invoked on cell-boundary crossing; handler signature(handler col row delta-col delta-row). One handler per cart; pass nil to unregister. Sustained against-boundary push does not re-fire the handler.on-trackpoint-click— register handler invoked on click; handler signature(handler col row). v0.1 fires exactly when QMK emits a primary-button click — right-click and middle-click are deferred to v0.2 per ADR-0032 §4 (no button-index argument in v0.1).cursor-visible!— runtime toggle (#t/#f) overriding the manifest:pointer-hiddendefault for the lifetime of the cart load.
Reservation policy. The cursor renders on the main 128×75 grid only — never on Row 0 (firmware status bar), Row 74 (firmware action bar), or the CIPHER-LINE auxiliary OLED (which has no cursor concept in v0.1 per ADR-0015). Cursor movement into reserved rows clamps at the boundary; wrap is rejected for predictability. See ADR-0035 §3.
Visibility default. Cursor visibility defaults to visible. Carts hide via (a) :pointer-hidden true in the cart manifest (cart-load default; lives in the ADR-0006 manifest schema per F2 of ADR-0035), or (b) (cursor-visible! #f) at runtime. Both mechanisms coexist — manifest for the declarative default, FFI for per-phase overrides. See ADR-0035 §2.
Cell coords, not pixels. Every existing main-grid display primitive in ADR-0005 Tier 1 (text-puts, text-cursor, text-invert, draw-bordered-box, helpers) uses cell coordinates; the cursor follows the same contract for composition. Sub-pixel-coord access (the right surface for hypothetical half-block-canvas cursor work per ADR-0027’s 128×150 model) is out of scope for v0.1. See ADR-0035 §“Options Considered” Option C.
Per-trackpoint differentiation. The two physical trackpoints merge to one logical cursor in v0.1 — cart FFI sees only the merged stream. Per-trackpoint access (pointer-a / pointer-b) is queued as a v2 question per ADR-0035 §“Options Considered” Option D and docs/influences/prototype/trackpoint-module.md §“What this leaves open.”
REPL / nEmacs integration. Out of scope for ADR-0035; the player REPL and nEmacs editor cursor model per ADR-0016 is unchanged in v0.1. Click-to-position in nEmacs / click-to-copy in REPL / scroll-wheel handling are queued as a future-ADR follow-up. See ADR-0035 §5.
Scope. Additive amendment. No existing primitive’s signature or contract changes. Tier 2 and Tier 3 are unchanged. The three capability tiers, the type-mapping table, and the dispatch contract are unchanged.
Authority trail. ADR-0035 (Decision §1–§5); ADR-0032 §4–§5 (hardware-side click semantics + cart-FFI deferral that ADR-0035 reverses); CLAUDE.md Canonical Hardware Specification (Keys row — 2× trackpoint commitment); CLAUDE.md Spec Hygiene Rule 5 (row reservation); CLAUDE.md Spec Hygiene Rule 6 (CIPHER OLED-exclusivity). Engineering follow-up: nosh_lisp_bridge.c bridge implementation per ADR-0035 F1; kn86-emulator/tests/test_nosh_lisp_trackpoint.c per ADR-0035 F1.
Reconciled 2026-06-12 (ADR-0027). This entry was written against the 80×25 drafting grid. ADR-0027 (ratified 2026-06-07) made 128×75 canonical (Row 0 status / rows 1–73 content / Row 74 action, native 8×8 cells, half-block 128×150 canvas — ADR-0014 retired). The cursor-position bounds (col 0–127, row 1–73), the reservation policy (clamp at Row 0 / Row 74), and the cell-vs-sub-pixel note above are reconciled to that canon. The FFI contract shape is unchanged; only the numeric grid bounds move. Emulator code reconciles at the nOSh re-flow (tracked deviation, per CLAUDE.md Spec Hygiene).
2026-06-07 — Tier 1 aesthetic-mode introspection primitive (ADR-0034)
Section titled “2026-06-07 — Tier 1 aesthetic-mode introspection primitive (ADR-0034)”Status effect: Accepted (unchanged). Adds one Tier 1 primitive. Primitive count rises 65 → 66 (applied after the ADR-0033 Reveal-family + ADR-0035 Trackpoint amendments landed earlier the same day).
Rationale. ADR-0034 locks the AMBER / AMBER / CIPHER aesthetic-mode roster, the nosh-config.toml persistence target, the SYS-tab picker contract, and the cart-readability decision. Carts that want to bias their own rendering (sprite variant, stroke width, spacing constant, or CIPHER style bias) against the operator’s active aesthetic preference need a single cheap read primitive. The primitive is read-only; carts cannot mutate the mode (only the operator, via the SYS picker, can).
Contract changes. §“Deck State Access (Read-only in this tier; mutations in Mission-context tier)” gains one row:
| Lisp name | Signature | C reference | Returns |
|---|---|---|---|
get-aesthetic-mode | () → symbol | stdlib_get_aesthetic_mode(SystemState *state) | One of :amber, :white, :green (per ADR-0034 §1 as amended 2026-06-13 per ADR-0036; previous :amber / :amber / :cipher retired). Default fallback (missing or invalid nosh-config.toml value) is :amber. |
Scope. Additive amendment. The three capability tiers (All-Carts / Mission-Context / REPL-Read-Only) are unchanged. Tier 3 (REPL-read-only) inherits the new primitive from Tier 1 — no separate REPL surface needed. No other rows or sections are modified.
Authority trail. ADR-0034 §5 (cart-readability decision); ADR-0034 §1 (locked roster — guarantees the primitive’s return-value domain); ADR-0034 §2 (persistence target — defines the default fallback path). Engineering follow-up: kn86-emulator/src/types.h adds enum AestheticMode and the aesthetic_mode SystemState field; kn86-emulator/src/nosh.c adds the Lisp binding; tests/test_aesthetic_mode.c exercises the three valid returns and the default-fallback path.
2026-06-07 — Tier 1 Reveal primitive family added (ADR-0033)
Section titled “2026-06-07 — Tier 1 Reveal primitive family added (ADR-0033)”Status effect: Accepted (unchanged). Additive: four new Tier 1 primitives + a new ### Reveal animation primitives subsection. Primitive count rises from 57 to 61.
Rationale. Six inspiration-corpus batches surfaced the same recurring observation — animated reveal of static text / ASCII content is a project-wide aesthetic that shows up across at least seven distinct surfaces (CIPHER fragment arrival on the OLED, mission contract-accept, DECRYPT-class verb responses, cart-load splash, boot animation handoff, CRT power-off shrink, carousel tab inbound). docs/influences/synthesis.md §3 item 3 + §6 item 7 + §2 L6 consolidated this into “one verb, multiple styles, one project-wide surface” and queued the promotion as a P1 sprint item. ADR-0033 ratifies the decision: promote the inspiration-tier docs/influences/effect/ascii-effects.md doc to canonical at docs/software/api-reference/grammars/reveal-styles.md, and land (reveal …) plus its lifecycle siblings as Tier 1 NoshAPI primitives.
Contract changes. §“Tier 1: All-Carts Primitives” gains a new subsection ### Reveal animation primitives after ### Display Helpers (stdlib) with four primitive rows: reveal, reveal-cancel, reveal-complete?, unreveal. Signatures, raises, and semantic contracts in the table; full enum / parameter ranges / performance budget / lifecycle in reveal-styles.md.
Style enum at v0.1. Three of six candidate styles ship: :char-flicker, :no-more-secrets (alias for :char-flicker), :radial. Three deferred: :random (low marginal aesthetic value pending operator-eye review), :diagonal (anchor surfaces — boot handoff, carousel tab — not yet wired through reveal), :fft (depends on PSG coprocessor-bridge work). Deferred styles raise :reveal-style-not-implemented rather than silently falling back; cart authors who want them today see the diagnostic and pick a shipping style.
Lifecycle. Asynchronous. The call enqueues and returns a handle; runtime ticks the animation off the existing 20 fps redraw loop. Sync would violate the 5 ms / 10 ms cell-handler latency contract (§“Dispatch Contract”) by two orders of magnitude on a 1-second reveal. Operator keypress auto-cancels all active reveals for the current cart (any-key-resolves UX, matching the nms CLI precedent).
Tier placement. Tier 1 (All-Carts). Available always; no mission-context restriction. Tier 3 (REPL) exposure is a no-op stub returning nil + stderr log, consistent with the broader Tier 3 forbidden-mutation rule.
Engine reference. docs/influences/effect/libcaca.md (algorithmic shape, CPU-only — well-suited to Pi Zero 2 W; no vendoring). The :char-flicker algorithm specifically references docs/influences/research/no-more-secrets.md — GPL v3, reimplement from first principles, ~50 lines of C.
Performance budget. Target ≥ 30 fps steady-state on full primary-display surfaces (128×73 cells) and ≥ 60 fps on the CIPHER-LINE OLED (4×32 cells). Estimates derived from the libcaca / no-more-secrets algorithm shape scaled to KN-86 cell counts — not measured. Bring-up benchmark on Pi Zero 2 W is part of the implementation acceptance criteria; if real fps falls below the 20 fps redraw cap, the implementation degrades by halving the per-cell flicker rate / widening the radial step (no FFI contract change). (Surface dims reconciled to the 128×75 canon 2026-06-12 per ADR-0027 — was 80×23 cells.)
Scope. This is an additive amendment. The 57-primitive count from the 2026-05-03 ADR-0028/ADR-0029 amendment rises to 61. The three capability tiers (All-Carts / Mission-Context / REPL-Read-Only) are unchanged. The :hover / :click interaction grammars and the (draw-ascii …) primitive from the inspiration source remain unpromoted — no v0.1 caller — and stay at the inspiration tier per ADR-0033 §5 “Edits explicitly NOT taken.”
Authority trail. ADR-0033 §2 (Decision); docs/software/api-reference/grammars/reveal-styles.md (canonical contract); docs/influences/synthesis.md §3 item 3 + §6 item 7 + §2 L6 (cross-domain convergence). Engineering follow-up: emulator-side implementation of the four primitives + Pi Zero 2 W bring-up benchmark (separate Notion tasks per ADR-0033 §5 “Follow-on work”).
2026-06-13 — Cell API backend changes from termbox2 to native renderer; cart contract unchanged (ADR-0036)
Section titled “2026-06-13 — Cell API backend changes from termbox2 to native renderer; cart contract unchanged (ADR-0036)”Status effect: Accepted (unchanged). Zero-impact amendment from the cart author’s perspective. No primitive added, no primitive removed, no signature changed, no semantic changed; primitive count unchanged at 66.
What changed. ADR-0036 (2026-06-13) supersedes ADR-0027’s display layer. The constrained KN-86 cell API surface ADR-0027 specified is preserved verbatim; the C implementation backend changes from termbox2 to the KN-86 native framebuffer renderer (render.c painting RGB565 to /dev/fb0 on device, into an SDL3 surface in the desktop emulator). The cell API primitives ADR-0027 introduced and this ADR amended onto Tier 1 — cell-set, cell-print, cell-clear-cart-region, cell-cols, cell-rows-usable, half-block-set, half-block-rect, half-block-clear, present, yield — keep their signatures, return values, semantics, chrome-row reservation, and operator-keypress pre-emption. Cart-facing contract is bit-for-bit identical.
Phosphor-mode FFI rename (the one cart-observable change in this amendment). (get-aesthetic-mode) return values change from :amber / :amber / :cipher to :amber / :white / :green per the ADR-0034 amendment landing in the same cascade. The primitive signature () → symbol, its Tier 1 placement, its read-only semantic, and its O(1) cost are unchanged; only the symbol set rotates. No carts depend on the old symbols (the primitive shipped in this ADR’s 2026-06-07 amendment and has not been used by carts yet). New default return value: :amber.
What is NOT changed. The three capability tiers (All-Carts / Mission-Context / REPL-Read-Only) are unchanged. The dispatch contract, the type-mapping table, the FFI mechanism (bind() + GC-rooting via symlist), and every other Tier 1 / Tier 2 / Tier 3 primitive are unchanged. The half-block 128×150 pseudo-pixel canvas is unchanged. ADR-0027’s two-tier cart-vs-system sandbox model is unchanged. The 5 event-callback shapes (on-key, on-multi-tap, on-hold, on-resize, on-tick) are unchanged.
Implementation impact. The cell-API primitives re-point from cell_api.c (termbox2-backed) to the native renderer in render.c. Each primitive becomes a thin call into the renderer (e.g. cell-set col row ch fg bg → render_blit_glyph(col*8, row*8, ch, 1, fg, bg)). The kn86-nosh-tb spike binary is retained as a reference implementation of the constrained-cart authoring shape; the production runtime is the native renderer per ADR-0036. The nOSh re-flow task ADR-0027 named is reconstituted as “the native-renderer port” — same migration, different destination.
Authority trail. ADR-0036 §“Decision” item 4 (“the cell-API surface from ADR-0027 is preserved as a Fe-facing convention, not a backend”) + §“What ADR-0027 we keep” (cell API authoring shape preserved verbatim) + §“Consequences” — Cartridge-author impact: zero. ADR-0034 (amended) for the (get-aesthetic-mode) return-value rename. Validation: on-glass session 2026-06-13 against ssh://deckline using /home/kn86/fb_restest.py.
2026-06-14 — NoshAPI = device primitives bound onto the vendored standalone language (clarification)
Section titled “2026-06-14 — NoshAPI = device primitives bound onto the vendored standalone language (clarification)”Status effect: Accepted (unchanged). No primitive added, removed, or re-signed; primitive count unchanged. This amendment clarifies where these primitives live and how they reach Lisp, following the 2026-06-14 KEC Lisp language split. Companion to the ADR-0004 and ADR-0001 amendments.
The vendor-as-library model. As of 2026-06-14 the KEC Lisp language is a standalone repo (github.com/Kinoshita-Electronics-Consortium/kec-lisp): the Fe kernel, KEC Core (stdlib in Lisp), portable host primitives (type-of, math, string, I/O), the embedding API, and the kec CLI. None of the NoshAPI primitives enumerated in this ADR are part of that language repo. They are KN-86 device primitives — graphics, audio, save, missions, CIPHER, deck-state, the cell API — that the firmware registers onto a vendored Fe context through one FFI seam: kec_bind_fe(kec_fe(S), "name", fn). “The Lisp runtime must expose these as built-ins” (Summary) means precisely: the firmware binds them on top of the vendored language at context creation. The language/device boundary is specified at the KEC Lisp site; the bind seam, C↔Lisp marshalling, FE_TPTR handle GC, and arena lifetime are at ffi-bridge.
Capability tiers = profiles + binding-set. This ADR’s three tiers (All-Carts / Mission-Context / REPL-Read-Only) are the KN-86 layering of the same mechanism the standalone language ships as profiles (KEC_PROFILE_SANDBOX / KEC_PROFILE_FULL, where FULL adds load/slurp/args/exit). A context can only call what was bound into it — capability is the binding-set chosen at context creation, not a runtime permission check. The firmware’s tiers are additional binding-sets layered on a profile.
The (type-of) follow-on (still tracked, not in this amendment). KEC Core’s number?/string?/symbol?/fn? predicates rest on a host (type-of x) primitive (kec-lisp host/). ADR-0037 §“Costs / follow-ons” tracks promoting it into this ADR’s enumeration (66 → 67) as a separate amendment; this clarification does not make that change.
2026-06-20 — Mission objective FFI: five goal verbs (ADR-0043)
Section titled “2026-06-20 — Mission objective FFI: five goal verbs (ADR-0043)”Status effect: Accepted (unchanged). Additive amendment: +5 Tier-2 (Mission-Context) primitives (66 → 71 by the running enumeration; the separately-tracked ADR-0037 (type-of) promotion is independent of this count).
What changed. ADR-0043 makes the mission objective first-class — the goal / bounty / multi-currency-reward model (companion spec mission-objectives.md). It binds five mission-context goal verbs onto the same Tier-2 surface as load-capability / advance-phase / complete-mission (ADR-0029):
| Primitive | Signature | Effect |
|---|---|---|
goal-complete | (goal-complete sym) → bool | :open → :done; fires :on-complete rewards; re-evaluates mission success |
goal-reveal | (goal-reveal sym) → bool | :latent → :open (a :primary-latent goal becomes briefed on reveal) |
goal-choose | (goal-choose sym) → bool | resolves a :branch — voids the :excludes siblings |
goal-fail | (goal-fail sym) → bool | :open → :failed (a :primary failure fails the mission) |
goal-state | (goal-state sym) → symbol | query a goal’s state (:locked/:open/:done/:failed/:forfeit/:void) |
These are Tier 2 (Mission-Context) — available only to a mission/cart running under an accepted contract, never to the All-Carts tier. :hold constraint predicates are engine-evaluated each tick (not a cart-called primitive); a violated hold transitions the goal to :forfeit with no cart involvement.
What is NOT changed. The three capability tiers, the dispatch contract, the type-mapping table, the FFI mechanism (kec_bind_fe), and every existing primitive are unchanged. Carts that do not use objectives are unaffected. Exact final signatures finalize with the engineering spike per ADR-0043 §Consequences.
Authority trail. ADR-0043 §Decision-4 + §“Documentation Updates”; companion spec mission-objectives.md §7.
2026-06-21 — Program launch + DOSSIER FFI (ADR-0042, gap D, GWP-553)
Section titled “2026-06-21 — Program launch + DOSSIER FFI (ADR-0042, gap D, GWP-553)”Status effect: Accepted (unchanged). Additive amendment: +3 primitives — one Tier 1 (launch-app) and two Tier 2 / Mission-Context (dossier-commit, dossier-has?) — taking the running enumeration 71 → 74. The three capability tiers, dispatch, type-mapping, and every existing primitive are unchanged.
What changed. Three FFI primitives owed by prior decisions now land:
| Primitive | Tier | Signature | Effect |
|---|---|---|---|
launch-app | 1 | (launch-app prog &key payload world) → result | Route to a first-party program (ADR-0042). :payload carries small launch params; :world is a handle to a cart-installed mission-data region the program reads live (the enrichment mechanism). A program is always launchable bare — enrichment never gates access. |
dossier-commit | 2 | (dossier-commit key) → bool | Sanctioned deposit of a typed DOSSIER key (a discovered fact). Routes through the engine’s sanctioned-write path (ADR-0040 §6) — captured to the inbox, then sanctioned-refiled under the matching profile; never a raw write. |
dossier-has? | 2 | (dossier-has? key &key min-conf) → bool | Read-only gating query — is a fact present (at ≥ min-conf confidence)? The recon→mission bridge that hard-gates / de-risks objectives. |
launch-app is the primitive ADR-0042 Decision-4 named but deferred to an amendment; its :payload + :world shape is fixed by enrichment-contract.md §3. dossier-commit / dossier-has? are the DOSSIER write + gate from dossier-data-model.md §4 / §6.
What is NOT changed. The three capability tiers, the dispatch contract, the type-mapping table, the FFI mechanism (kec_bind_fe), and every existing primitive are unchanged. Carts / programs that don’t use these are unaffected. Exact final signatures (keyword vs. positional, the world-handle type, the result / key types) finalize with the engineering spike — consistent with the 2026-06-20 goal-verb amendment.
Authority trail. ADR-0042 Decision-4 (launch-app); enrichment-contract.md §3 / §5 (launch-app shape + dossier-commit); dossier-data-model.md §4 / §6 (dossier-commit / dossier-has?); ADR-0040 §6 (sanctioned-write boundary).