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:
/runtime/src/nosh.h— display and sound API/runtime/src/nosh_stdlib.h— navigation, list ops, deck-state access, procedural generation/runtime/src/nosh_runtime.h— cell pool and navigation stack management/runtime/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. | — |
Cart draw tier (ADR-0027 / ADR-0036; parity amendment 2026-07-27)
Section titled “Cart draw tier (ADR-0027 / ADR-0036; parity amendment 2026-07-27)”The constrained cell surface a cartridge actually draws with, bound in
cell_api_bind.c. ADR-0027 introduced it and ADR-0036 re-pointed it at the
native renderer; both were recorded only in this ADR’s Amendment Log, so the
generated reference carried the retired text-* / bitmap surface above instead.
This section folds the real tier into the body. The text-* and bitmap-mode
sections above it are retired and kept for history.
Coordinates are CELL positions on the 128×75 grid unless a name says
half-block, which addresses the 128×146 sub-pixel canvas (2 sub-rows per cell
row, covering Rows 1–73). Carts own Rows 1–73; Row 0 (status) and Row 74
(action) are firmware chrome. A gated write drops and increments the
chrome-violation counter.
Prefer ui/draw.lsp over these directly — it is the semantic layer (shapes,
cell-addressed vectors, named soft-glyph textures) and, because it calls only
this tier, it loads in a cart context.
| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
cell-set | (col row ch fg bg) → nil | cell_set | Write one cell. Chrome rows drop + count; out-of-range columns clip silently. | — |
cell-print | (col row fg bg str) → nil | cell_print | Write a string left-to-right, clipped at the right edge (no wrap). Chrome rows drop + count once. | — |
cell-clear-cart-region | () → nil | cell_clear_cart_region | Clear Rows 1–73. Never touches chrome; not a violation. | — |
cell-cols | () → 128 | cell_cols | Grid width. Query this rather than hard-coding 128. | — |
cell-rows-usable | () → 73 | cell_rows_usable | Cart-usable row count (Rows 1–73). | — |
Half-block vectors
Section titled “Half-block vectors”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
half-block-set | (x y on) → nil | half_block_set | Light or clear one sub-pixel on the 128×146 canvas. Out-of-canvas ignored, no violation; chrome is unreachable by construction. | — |
half-block-rect | (x y w h on) → nil | half_block_rect | Filled sub-pixel rectangle, clipped to the canvas. | — |
half-block-line | (x0 y0 x1 y1 on) → nil | half_block_line | Added 2026-07-27. Integer Bresenham between two sub-pixels; the cart-tier peer of render/half-block-line. Both endpoints inclusive; a zero-length line lights the single endpoint. on nil clears, so the same call erases a stroke. Routed through half_block_set per pixel, so an off-canvas run draws only its visible part and cannot over-run the buffer. | — |
half-block-clear | () → nil | half_block_clear | Clear the sub-pixel canvas only; the cell grid is untouched. | — |
Soft-glyph plane
Section titled “Soft-glyph plane”Redefine one mutable 8×8 glyph and every cell showing it follows, so a uniform
animated field costs O(1) per frame regardless of area (ADR-0052). Slots are
cart-local 0–7, mapped into the upper half of the renderer’s 16-slot bank,
so a cart cannot name a system slot or corrupt a glyph chrome draws with.
| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
soft-slots | () → 8 | cell_soft_slots | Added 2026-07-27. Cart-addressable slot count. | — |
soft-define | (slot b0 b1 b2 b3 b4 b5 b6 b7) → nil | cell_soft_define | Added 2026-07-27. Overwrite cart slot 0–7 with 8 MSB-first row bytes (same layout as a kn86_font glyph). Eight positional bytes mirror render/soft-define, so (apply soft-define (cons slot bytes)) carries across tiers. Out-of-range slot ignored. | — |
soft-set | (col row slot fg bg) → nil | cell_soft_set | Added 2026-07-27. Show a soft slot in one cell; the soft-plane mirror of cell-set, same chrome gate. Out-of-range slot draws nothing. | — |
soft-fill | (col row cols rows slot fg bg) → nil | cell_soft_fill | Added 2026-07-27. Tile a soft slot across a cell block; one call paints a whole animated field. Clips to the grid; chrome rows inside the block drop + count. | — |
Frame control
Section titled “Frame control”| Lisp name | Signature | C reference | Semantic contract | Raises |
|---|---|---|---|---|
present | () → nil | present | Advisory frame marker. The runtime may auto-present at frame end. | — |
yield | () → nil | yield | Return control to the runtime. | — |
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 canonical-spec.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. 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: 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 runtime/tests/test_nosh_lisp_gfx.c (15 cases, including the (w*h+7)/8 byte-budget check for gfx-blit) and runtime/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: runtime/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: runtime/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 canonical-spec.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); Canonical Hardware Specification (Keys row — 2× trackpoint commitment); canonical-spec.md Spec Hygiene Rule 5 (row reservation); canonical-spec.md Spec Hygiene Rule 6 (CIPHER OLED-exclusivity). Engineering follow-up: nosh_lisp_bridge.c bridge implementation per ADR-0035 F1; runtime/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 canonical-spec.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: runtime/src/types.h adds enum AestheticMode and the aesthetic_mode SystemState field; runtime/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 into an SDL3 surface). (The “on device via /dev/fb0” half of this sentence was retired 2026-07-27: there is one host, hosts/emulator, running on the desktop and on the device alike over KMSDRM. The fbdev path is deleted.). 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 — finalized 2026-06-25 per ADR-0049 (see the 2026-06-25 Amendment Log entry): world-handle type, the result / key types) finalize with the engineering spike:world is an opaque mission-instance-scoped FE_TPTR handle, :payload is a by-value snapshot plist, dossier-has?’s :min-conf is the :rumored/:inferred/:verified enum (default :rumored); result / key types are the existing capability-call result struct / typed colon-path fact-key. This entry is no longer “logged-pending” — it is ADR-ratified.
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); ADR-0049 (signature finalization).
2026-06-25 — Program-launch + DOSSIER signatures finalized; AmberCalc formula model + assert-balance disposition (ADR-0049, GWP-608)
Section titled “2026-06-25 — Program-launch + DOSSIER signatures finalized; AmberCalc formula model + assert-balance disposition (ADR-0049, GWP-608)”Status effect: Accepted (unchanged). Ratification, not enumeration change: 0 net new primitives. Primitive count stays at 74. The three capability tiers, dispatch, type-mapping, and every existing primitive are unchanged. This entry promotes the 2026-06-21 launch-app / dossier-commit / dossier-has? entry from “logged, signatures-pending-the-spike” to ADR-ratified and records two adjacent dispositions (the formula builtins and assert-balance) that explicitly do not enlarge the enumeration.
What changed. ADR-0049 (the AmberCalc + Black Ledger v1 design’s FFI lock) finalizes the shapes the 2026-06-21 entry left open:
| Primitive | Tier | Finalized signature | Finalization detail (ADR-0049) |
|---|---|---|---|
launch-app | 1 | (launch-app prog &key payload world) → result | :world = an opaque, mission-instance-scoped FE_TPTR handle to a cart-installed run-state region (read-only to durable state; resets at the mission-instance boundary; :invalid-world raised on a stale handle; bare launch with no :world opens the program’s empty/default world). :payload = a by-value snapshot plist of launch params (view, scale, threat hint) — never a handle, never the dataset. result = the existing capability-call result struct. Supersedes the walkthrough/stub :payload ledger-dataset shorthand. |
dossier-commit | 2 | (dossier-commit key) → bool | key = a typed colon-path fact-node key (e.g. acct:shell:caribbean-trust:beneficiary) the cart authored as discoverable; the value/prov/conf/:links ride on the authored fact-node the key names. Sanctioned capture half of the two-step write: engine lands it in the inbox then auto-refiles under the matching profile (creating it if new), else it waits for an operator refile. #t on accepted capture, #f on unknown key. |
dossier-has? | 2 | (dossier-has? key &key min-conf) → bool | Read-only gating query. :min-conf is the ordered confidence enum :rumored / :inferred / :verified (default :rumored = present at any confidence). Pure read; hard-gates or de-risks. |
Gap D (DOSSIER refile FFI) — closed by absence-by-design. No dossier-refile primitive is added. Auto-refile is performed by the engine inside dossier-commit’s sanctioned path; the only manual refile is a DOSSIER-program operator action on the inbox (a sanctioned UI action, not cart-callable Lisp). This preserves the ADR-0040 §6 write boundary — carts capture, the engine/operator files. The fact-node :links use the canonical edge-verb set (:owns · :controls · :routes-to · :employs · :located-at · :corroborates · :contradicts), authored on the node and deposited atomically with dossier-commit — no separate link FFI.
assert-balance — cart-side helper, NOT a core primitive. ADR-0049 §4 implements reconcile-by-assertion as Black Ledger cart Lisp over dossier-commit (a :world read + a known-true figure + arithmetic; the non-zero delta is committed as a fact-node). The :acct:<colon-path>:balance:asserted:<iso-date> key schema is normative whether or not assertion is ever promoted, so a future helper→primitive promotion is a no-contract-change drop-in. Not added to this enumeration.
AmberCalc formula builtins — program binding-set, NOT NoshAPI. cell, range, and the sum/avg/min/max/count aggregate floor (+ arithmetic + if) are bound onto the AmberCalc-program Fe context (the 2026-06-14 vendor-as-library model), live only while AmberCalc is the active mode (ADR-0046 program-as-mode). They are not NoshAPI primitives and do not enter this ADR’s count.
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. The 2026-06-20 goal-verb amendment and the 2026-06-21 program/DOSSIER amendment are unchanged except for the 2026-06-21 entry’s status flip (logged → ratified).
Authority trail. ADR-0049 §“Decision” 1–5; companion design docs/plans/2026-06-25-ambercalc-blackledger-design.md (deleted 2026-07-29; recover from git history) §3.3/§3.6/§3.7/§4.4/§9/§10; enrichment-contract.md §3–§4 (:world/:payload); dossier-data-model.md §4–§6 (capture/refile/links/:min-conf); ADR-0040 §6 (write boundary). Engineering follow-up: nosh_lisp_bridge.c binds the three finalized signatures; runtime/programs/ambercalc/ (formula builtins); carts/carts/blackledger.lsp (assert-balance helper); tests per ADR-0049 §“Follow-on work.”
2026-07-17: Mission-end economy contract (outcome-report completion, mission-fail, wallet writes re-tiered) (ADR-0055, GWP-717)
Section titled “2026-07-17: Mission-end economy contract (outcome-report completion, mission-fail, wallet writes re-tiered) (ADR-0055, GWP-717)”Status effect: Accepted (unchanged). Net +1 primitive (74 → 75), one signature change, and two primitives leaving the cart-callable surface. Handled as a pre-freeze correction inside NoshAPI v1: no api_version negotiation is implemented yet, the indexed vtable does not exist (no slot tombstoned; tier membership is a binding-set per the 2026-06-14 amendment), and every consumer is an in-tree launch cart migrated in lockstep. versioning.md records that v1’s cart surface froze after this entry.
What changed (ADR-0055):
| Primitive | Tier | Change |
|---|---|---|
complete-mission | 2 | Signature becomes (complete-mission :outcome sym &key bonuses) with :outcome ∈ :success / :partial. The caller reports the outcome; nOSh prices the award from the accepted contract’s retained MissionInstance snapshot (payout_credits, reputation_delta_pass), settling ADR-0043 banked / escrowed rewards in the same pass. The prior credits/rep parameters are removed. mission-complete remains the no-argument alias (implies :outcome :success). |
mission-fail | 2 | New. (mission-fail) → nil. Resolves the active contract as failure: zero credits, applies the snapshot’s reputation_delta_fail, ends the mission context, publishes NOSH_EVENT_MISSION_FAILED. Raises :not-in-mission outside mission context. |
abandon-mission | 2 | Semantics sharpened: zero credits, reputation penalty = reputation_delta_fail × abandon_rep_ratio (engine balancing variable, currently 0.5), rounded toward zero. |
award-credits | Leaves the cart binding-set. No longer bound into cart-tier Fe contexts; a cart-tier (award-credits 1) raises unbound symbol. System-tier contexts (bare-deck bounties, debrief, the engine’s own award path) retain it. | |
modify-reputation | Same re-tiering as award-credits. |
What is NOT changed. Tier 1, Tier 3, the dispatch contract, the type-mapping table, the FFI mechanism (kec_bind_fe), the ADR-0043 goal verbs, and the capability-call result struct are unchanged. stdlib_credit_add / stdlib_rep_modify remain the C-level wallet helpers; only their Lisp exposure narrows.
Authority trail. ADR-0055 §Decision 1–7; economy-model.md (the one-wallet-write ruling); ADR-0040 §3 (UDS sanctioned-outcome boundary). Engineering follow-up: GWP-719 (runtime side: re-signature, nosh_mission_fail, snapshot retention, event topic, unbinding + enforcement tests); cart migration + committed runtime/assets/*.kn86 regeneration as separate tasks.
2026-07-27 — Cart draw tier reaches parity with the system render tier
Section titled “2026-07-27 — Cart draw tier reaches parity with the system render tier”Status: Accepted (amendment). Primitive count: 75 → 80.
What changed. ADR-0052’s soft-glyph plane and GWP-654’s keystone line
primitive shipped System-tier only, bound in sys_render.c as
render/soft-* and render/half-block-line. The cart tier
(cell_api_bind.c) carried half-block-set / -rect / -clear and nothing
else. Five primitives move into Tier 1 so a cartridge can draw what a system
screen can.
| Primitive | Signature | C reference | Semantics |
|---|---|---|---|
half-block-line | (x0 y0 x1 y1 on) → nil | half_block_line | Integer Bresenham between two sub-pixels on the cart’s 128×146 half-block canvas. Both endpoints inclusive; a zero-length line lights the single endpoint. on nil clears, so the same call erases a stroke. Every pixel routes through half_block_set, so an off-canvas run is dropped per pixel and cannot over-run the buffer. |
soft-slots | () → 8 | cell_soft_slots | Number of cart-addressable soft-glyph slots. |
soft-define | (slot b0 … b7) → nil | cell_soft_define | Overwrite cart soft slot slot (0–7) with 8 MSB-first row bytes, the same layout as a kn86_font glyph. Eight positional bytes rather than a list, mirroring render/soft-define, so the (apply soft-define (cons slot bytes)) idiom carries across tiers. Out-of-range slot or arity is ignored. |
soft-set | (col row slot fg bg) → nil | cell_soft_set | Show a soft slot in one cell. The soft-plane mirror of cell-set, with the same chrome gate: Row 0 / Row 74 drop and increment the violation counter. |
soft-fill | (col row cols rows slot fg bg) → nil | cell_soft_fill | Tile a soft slot across a cell block. One call paints a whole animated field. Clips to the grid; chrome rows inside the block drop and count. |
Why it matters (the cost model was inverted). Soft-glyph animation costs
O(1) per frame regardless of field area: rewrite the slot’s 8 bytes and every
cell showing it follows. Glyph-cycling costs O(cells). Before this amendment
the cheap technique belonged to system code while carts — full-screen action
surfaces, the ones that need cheap animation most — had only the expensive one,
and no way to draw a line short of reimplementing Bresenham in Lisp pixel by
pixel, per cart.
Slot allocation is a contract. The renderer’s bank is
RENDER_SOFT_SLOTS = 16, split RENDER_SOFT_SYSTEM_SLOTS = 8 system /
8 cart. A cart names its own 0–7 and the runtime maps them into the upper
half; naming 8 or above from a cart is out of range and ignored. A cart
therefore cannot corrupt a glyph the status bar or action bar draws with, by
construction rather than by convention. Raising RENDER_SOFT_SLOTS widens both
halves; the split point is a constant, not a ceiling.
Representation. A cell displaying a soft glyph stores
CELL_API_SOFT_TAG | slot in its character field. Soft slots do not occupy
KN-86 Code Page codepoints, so any consumer of cell_peek_ch must test
cell_ch_is_soft() before treating the value as a glyph code; the rasterizer
does. Cart soft-glyph bytes live in cell_api.c rather than delegating to
render_soft_define, keeping the cart tier’s dependencies narrow: it must not
pull the System render tier into its link line.
Canvas difference. The cart half-block canvas is 128×146 (Rows 1–73 doubled); the system tier’s is 128×150 (all 75 rows). Carts do not own Row 0 or Row 74, so vectors cannot address chrome at all.
Authoring layer. ui/draw.lsp is the semantic layer over both families
(cell-addressed draw/line / polyline / polygon / box / circle, named
soft-glyph textures, draw/retexture!). It calls only this tier and never
render/*, so the same module loads in a cart context and in a System one.
Author against it rather than these primitives directly. See
animation.md §“Both tiers,
same functionality”.
What is NOT changed. The three capability tiers, the dispatch contract, the
type-mapping table, the FFI mechanism (kec_bind_fe), the chrome-row
reservation, and every existing primitive are unchanged. cell-set,
cell-print, cell-clear-cart-region, cell-cols, cell-rows-usable,
half-block-set, half-block-rect, half-block-clear, present, and yield
keep their signatures. The System-tier render/* names are unchanged and stay
System-tier.
Authority trail. ADR-0052 (the
soft-glyph plane + the two-style model), ADR-0036
(the renderer these extend), GWP-654 (the keystone line primitive),
ADR-0027 (the constrained cart surface
this widens). Implementation: runtime/src/cell_api.{c,h},
runtime/src/cell_api_bind.c, runtime/src/recorder.c (rasterizer
resolution), ui/ui/draw.lsp. Tests: runtime/tests/test_cell_api.c (nine
cases) + ui/tests/lisp/test-draw.lsp (15 cases).