Sprint 3 Design Pack — GWP-260 — Universal Deck State CIPHER voice integration
Notion task: GWP-260
Recovered: 2026-04-26 (lost to worktree contention; restored from Notion block children API)
Objective
Section titled “Objective”Implement the CIPHER-LINE Grammar Framework’s full generator engine per docs/software/runtime/cipher-voice.md and ADR-0015 §F3 — event ingestion, memory store with decay, mode selector, weighted s-exp grammar, coherence stack, style controls, cart-contributed fragment merge.
Context
Section titled “Context”ADR-0015 was approved 2026-04-24 and merged in PR #67. Wave-B follow-on work (GWP-203 cipher-emit, GWP-207 main-grid escape) shipped the FFI surface, but the generator ENGINE — the thing that turns events into actual fragments — is the unimplemented core. Today cipher.c contains scaffolding; the grammar evaluator, memory ring decay, and mode selector are stubs.
References: docs/software/runtime/cipher-voice.md, docs/adr/ADR-0015-cipher-line-auxiliary-display.md §F3.
Acceptance Criteria
Section titled “Acceptance Criteria”- src/cipher.c gains: 128-entry decay-weighted event ring (per ADR-0015 spec), 5-slot coherence stack persistence in DeckState, mode selector (observe/annotate/reflect/drift/silent) with weighted distribution by beat parameter
- s-exp grammar evaluator with weighted productions; non-terminals for slots (:subject, :object, :verb-past-participle, :memory-keyword, :affect-word) and memory fragments
- Style controls (terseness, certainty, temporal-blur) applied at fragment composition
- Cart-contributed cipher-grammar block merged at REGISTERED into active grammar tables; cleared at UNMOUNTING
- Voice heuristic check (under-8-words, drops articles, drops connectives) enforced as a soft-validate fallback to silent mode if a fragment fails 3+ checks
- Test test_cipher_engine covers: each beat (bare-deck, mission-brief, active-hack, debrief, cart-swap-lull, null), determinism for fixed cipher_seed, repetition penalty in mode selector, coherence-stack persistence across cart swap
- Beat transitions wired into mission_board events and cartridge-lifecycle FSM (subscribe-only; both upstream stories own emission)
Constraints
Section titled “Constraints”- Owns: src/cipher.c (engine refactor), src/cipher.h (struct extension if needed).
- Touches additively: tests/test_cipher_engine.c (new), nosh_lisp_bridge.c (cipher-extend-grammar wires into engine merge).
- Must NOT modify: oled.c (renderer is downstream), display.c.
- TDD; arena allocation only; cite ADR-0015 §F3 in commits.
Dependencies
Section titled “Dependencies”GWP-{mission-board}, GWP-{cart-lifecycle} — both publish events the engine consumes. Either land the consumers later (feature flag) or sequence after the producers.
Design Pack — Sprint 3 (2026-04-26)
Section titled “Design Pack — Sprint 3 (2026-04-26)”Authored by PM/BA + Gameplay Designer. Full pack on disk at docs/sprints/2026-04-26-sprint3-gwp-260-design.md. This appendix is the structured summary; the on-disk file is load-bearing.
1. PM/BA — Story narrative
Section titled “1. PM/BA — Story narrative”CIPHER-LINE has hardware (Pico-driven SSD1322 per ADR-0017), an FFI surface (nosh_cipher_/nosh_aux_ exist at C-level), Lisp bindings (cipher-emit/cipher-push-event wired in nosh_lisp_bridge.c), a deck-state schema (coherence_stack[400], event_ring[1280], cipher_mode_weights[5]), and an authoritative engine spec (cipher-voice.md §§3-10). What it does NOT have is the runtime that consumes events from the ring, runs the mode selector against beat + cart biases + affect, expands the s-expression grammar, manages the coherence stack, and renders utterances to OLED Rows 2/3 via nosh_oled_*. This story stands up that runtime. Cart authors get a working voice on launch carts; operators get the cockpit-voice-recorder aesthetic.
2. Gameplay Designer — Player-facing semantics
Section titled “2. Gameplay Designer — Player-facing semantics”Engine tick is event-driven: once per OLED-relevant event in the queue, plus once per second when idle. Each tick consumes one event (or nil), computes mode distribution, samples mode via deck-state LFSR, expands grammar, dedupes against coherence stack, then emits utterance (Row 2 push, prior Row 2 → Row 3) or stays silent (Rows 2/3 hold).
Context A — cart-load during low-credit state
Section titled “Context A — cart-load during low-credit state”Event: (:event :type :cart-load :tag :neon-grid :t 12340 :affect (:routine)). Beat :cart-swap-lull = (0.05/0.10/0.20/0.55/0.10). LFSR roll → drift. Rule fired: :mode-drift → (2 (:memory-deictic) ”. ” (:memory-fragment)). Memory store empty, NeonGrid vocab sampled: :memory-deictic → “the last grid”, :memory-fragment → reduced-grammar “sentry routed.” Utterance: “the last grid. sentry routed.” (24 chars).
Context B — successful mission completion (ICE Breaker mirror)
Section titled “Context B — successful mission completion (ICE Breaker mirror)”Cart pushes :result-success :affect (:significant). Beat :active-hack (0.60/0.20/0.05/0.00/0.15) + :significant +0.15 reflect + ICE Breaker bias (:observe +0.05 :silent +0.05). LFSR → reflect. Rule fired: :mode-reflect → (2 “same ” (:memory-keyword) ”. ” (:memory-fragment)). :memory-keyword from coherence stack + ICE Breaker pool: “node”. :memory-fragment samples store, finds :contact :black-ice :sector-7 → “black ice. sector 7.” Utterance: “same node. black ice. sector 7.” (31 chars at budget).
Context C — deck idle >60s
Section titled “Context C — deck idle >60s”No event arrives. Idle tick. Beat :idle (0.05/0.05/0.15/0.40/0.35), last mode reflect → halved. Renormalize. LFSR → silent. Rule fired: NONE. Engine consumes (null) event, advances tick, updates store, does NOT push to coherence stack, does NOT render. Rows 2/3 hold prior content. Operator sees prior reflect line in peripheral, continues working.
3. Acceptance criteria
Section titled “3. Acceptance criteria”- Engine module at kn86-emulator/src/cipher.c implements cipher_tick(SystemState *, uint64_t now_ms). Public API in cipher.h: cipher_init(), cipher_tick(), cipher_set_beat(), cipher_get_current_distribution() (debug), cipher_grammar_load(), cipher_grammar_unload().
- Event ring consumption — engine pops oldest non-decayed event from DeckState.event_ring per tick (priority: :anomalous, :significant, any). If ring empty, idle tick fires after 1s wall clock since last tick.
- Mode selector matches cipher-voice.md §6 exactly — beat default + cart bias deltas (±0.20 cap) + affect bias (±0.15) + repetition penalty (last non-silent halved) → renormalize → weighted_sample via lfsr_next(deck_state.cipher_seed). Determinism load-bearing.
- Grammar expansion runs cipher-line-grammar.md baseline + cart-supplied cipher-grammar blocks. Recursion bounded at depth 5 (return literal at depth 6). Slot fills from triggering event; :memory-* slots from memory store; cart-namespaced non-terminals scoped to that cart.
- Coherence stack writes — every non-silent emission pushes to DeckState.coherence_stack (80 bytes/slot, 5-slot ring). Verbatim text retained per deck-state.md mandate. Up to 3 keywords extracted (longest non-stopword tokens). Persists across cart swaps.
- Dedup — candidate utterance compared (string match) against last 3 stack entries. On match, re-sample up to 3 times; on triple-fail, fall through to silent (don’t render).
- OLED render — non-silent emissions call nosh_oled_set_row(2, surface) and shift previous Row 2 → Row 3 via nosh_oled_set_row(3, prev). Routes through coproc.c per ADR-0017. Render queued via existing OLED command path; respects per-row dirty cadence.
- Cart-grammar merge/rollback — cipher_grammar_load(block, cart_tag) parses cart’s cipher-grammar Lisp form, validates merge rules (cipher-voice.md §10), allocates from per-cart 8 KB grammar arena (ADR-0015 Implementation Notes). cipher_grammar_unload(cart_tag) removes overlay. Errors: :grammar-parse-error, :grammar-too-large.
- Tests at kn86-emulator/tests/test_cipher_engine.c (NEW): golden-file regression on (cipher_seed=0xA7F3, sample event sequence, expected utterances) per cipher-voice.md §12 worked example; mode selector distribution sanity (10k samples per beat → ±2% of expected); ≤32-char strings; cart-grammar merge+unload; coherence stack survives swap (3 utterances → swap → all 3 still in stack); silent does NOT push stack; affect bias overrides default; arena overflow returns :grammar-too-large; recursion depth cap. Target ≥85% coverage.
- Integration with existing CIPHER-LINE FFI — nosh_cipher_emit(), nosh_cipher_push_event(), nosh_cipher_extend_grammar(), nosh_cipher_set_mode_weights() delegate to engine instead of stubbing. No FFI shape change.
4. Edge cases and failure modes
Section titled “4. Edge cases and failure modes”- Cold boot — empty memory store + coherence stack. :reflect/:drift have nothing to sample → fall through to :observe (or :silent if even :observe fails). Never emit empty string.
- Grammar block too large (12 KB vs 8 KB arena): :grammar-too-large returned to cart loader; cart load fails cleanly per ADR-0015 §4. FSM (GWP-259) treats as MOUNTED-sticky — engine never enters REGISTERED for it.
- Cart pushes malformed event (:type not symbol, payload >64 bytes): nosh_cipher_push_event already validates per ADR-0015. Engine ignores; nothing reaches the ring.
- Coherence stack persistence across reboot — power-off writes stack to flash via existing deck-state persister. On boot, 5 slots restored. First tick after boot may sample these — operator’s prior session voice continues. Test verifies byte-identical round-trip.
- Recursion in productions: bounded at depth 6; engine returns literal ”…” + warning.
- Affect bias drives all probabilities to 0: renormalize floors to uniform tail before sampling — never emit NaN.
- OLED queue back-pressure — if nosh_oled_set_row returns busy (Pico UART queue full), drop utterance silently. Coherence stack NOT pushed (we didn’t speak). Re-attempt next tick.
- Two cart cipher-grammar blocks declare same :tag (collision): per cipher-voice.md §14, runtime refuses cart-load on tag collision. Engine state stays clean.
5. Cross-references
Section titled “5. Cross-references”ADRs: ADR-0015 (F3 follow-on; 8 FFI primitives; 8 KB arena; coherence decay), ADR-0017 (OLED via Pico through coproc.c), ADR-0004 (Fe arena discipline), ADR-0005 (cipher-* are Tier 1 all-carts).
Module specs: ice-breaker.md (primary launch consumer, :active-hack/:high-tense heavy), cipher-garden.md (crypto vocab, :reflect heavy), depthcharge.md (sonar/maritime per cipher-voice.md §Vocabulary Registration example), black-ledger.md (financial-forensics, :debrief heavy), neon-grid.md (spatial domain, :cart-swap-lull example), null.md (UNIQUE: reads coherence stack via cipher-stack-head + cipher-emit-main-grid escape; engine must expose stack-head reads), marty-glitch-broadcast-piracy.md (cipher-set-mode-weights toward :drift).
Runtime docs: cipher-voice.md (THE engine spec — match §§3–13 verbatim), cipher-line-grammar.md (wire grammar, baseline productions), deck-state.md (schema for coherence_stack[400]/event_ring[1280]/cipher_mode_weights[5]), orchestration.md §The Cipher Voice.
6. Engineering hand-off notes
Section titled “6. Engineering hand-off notes”Files to touch: cipher.c (extend stub — bulk of work, ~600–800 LOC), cipher.h (extend public API), nosh.c (delegate to engine), coproc.h/coproc.c (confirm/add nosh_oled_set_row), CMakeLists.txt (add test target), tests/test_cipher_engine.c (NEW). Files NOT to touch: nosh_lisp_bridge.c (bindings exist), oled.c (engine targets coproc.c not oled.c), deck.c/schema (fields reserved already).
Expected PR size: 1000–1300 LOC. LARGE. Engine ~700 + tests ~500 + glue ~150. 4 days impl + 2 days tests. If slips: split as Wave-1 (mode selector + grammar engine + tests) and Wave-2 (coherence stack + cross-cart persistence + Null read APIs).
Test strategy: TDD, golden-file heavy. cipher-voice.md §12 worked-example walkthrough IS the canonical golden-file. Mode selector empirical distribution test (10k samples) catches selector drift. Per-cart grammar overflow test catches arena escapes.
Scope guardrail (NOT in scope): SSD1322 hardware driver (Pico, not emulator); style controls (terseness/certainty/temporal-blur from §8) — see open question; Bare Deck Terminal HUD :bare-deck beat behavior beyond consuming distribution; Null’s Cipher Analysis bounty UI (separate story; this ships the read API only); Marty Glitch persona override (separate story).
7. Open questions for Josh
Section titled “7. Open questions for Josh”Style-control constants vs full implementation. §8 specifies three uint8 controls (terseness, certainty, temporal-blur) each with per-beat defaults. Full impl (modify expansions to drop articles, insert hedges, slip tense) ≈ +200 LOC. Recommended: v0.1 stores deck-state values + respects them in a small number of obvious places (article drop at high terseness); leave hedge insertion + temporal-blur slip-tense as follow-up. Confirm or override partial approach.
8. Sequencing dependencies
Section titled “8. Sequencing dependencies”Blocks: Cipher voice on every launch cart; Null Cipher Analysis bounty (depends on stack-head read API); QA Cipher golden-file test plan. Unblocks: ALL launch carts get a working voice; Null bounty becomes implementable. Parallel-safe with GWP-259 (FSM publishes :cart-swap, engine subscribes — wire one-way; FSM should land first). GWP-244 entirely independent. Suggested wave: Sprint 3 Wave 2 (after GWP-259). High priority — build feels incomplete without Cipher.