Skip to content

ADR-0039: Repository topology — split the monorepo into seven focused repos

The non-docs, non-language codebase is a monolith. Everything that is not documentation (kn86-docs) or the language (kec-lisp) lives in the kinoshita repo (github.com/jschairb/kn86-deckline) under kn86-emulator/ plus a root tools/ Rust workspace:

  • kn86-emulator/ — the nOSh runtime substrate (native framebuffer renderer, FFI bridge, event loop, input, audio, OLED, save/economy cores), the Fe system userland, the SDL3 desktop host, and the cart-loader path — all in one tree.
  • tools/ — the Rust workspace for cart packaging (kn86cart) and device/firmware tooling (kn86fw, flasher, sd-provision).

Two structural facts make the monolith increasingly costly:

  1. The runtime is mid-inversion. Per kec-lisp-runtime-architecture.md §1 and §8, nOSh is being inverted from “all C” to C substrate ← Fe userland: the Fe system libraries (render/ board/ deck/ nemacs/ repl/ sys/ ui/ and the supporting lib/) land under kn86-emulator/system-image/lib/, driving a small C substrate (the native renderer per ADR-0036, the FFI bridge, the event loop, the integrity cores). This re-architects the runtime into a clean library + host shape that the monolith’s single-tree layout obscures.
  2. Three concerns ship on three different cadences. The language (kec-lisp), the runtime, the UI kit, the SDK, the launch carts, and the device image each want to release independently. Bundling them forces lockstep versioning where none is warranted.

The precedent already exists. ADR-0037 carved the language out as its own org repo (Kinoshita-Electronics-Consortium/kec-lisp), vendored back into the consumer via copy + a sync.sh that records the source commit (kn86-emulator/vendor/kec-lisp/sync.sh). That pattern — separate org repo → gitignored sibling checkout → vendored into consumers via copy + sync.sh, recording the source commit — is the same pattern kn86-docs itself uses as a gitignored sibling of kinoshita. This ADR extends that proven pattern across the whole codebase.

This ADR records the target repository topology, the dependency direction, the vendoring + versioning contract, the extraction mechanics, and the extraction sequencing. It is a planning decision — no code moves in this ADR or its PR.

Split the monolith into seven focused repositories.

  1. kec-lisp (exists) — the language: Fe kernel + KEC Core + host primitives. Per ADR-0037. Source of the language standard.
  2. nOSh — the runtime library (libnosh). The base of everything below. It contains:
    • The C substrate: the native framebuffer renderer (render.c + phosphor.c + composite.c), the cart cell-API tier (cell_api.c), the system render tier (sys_render.c / sys_context.c), the FFI bridge (nosh_lisp_bridge.c), the event loop, the input classifier, the audio path (PSG / coproc), the OLED driver, and the save / economy integrity cores.
    • The Fe system userland: render/ board/ deck/ nemacs/ repl/ sys/ cipher/ attract/ plus the runtime-tier launch baselines.
    • A headless test backend so nOSh runs its own ctest without a graphical host.
    • The L2 API contract it publishes (see Dependency direction below).
  3. kn86-emulator — the thin SDL3 desktop host. Links nOSh; owns the SDL surface/input backend + main.c. The existing monolith is reduced to exactly this.
  4. kn86-ui — the Fe component kit (frames lists data input glyphs compositing). Targets the render + cell-API contract nOSh publishes. Its spec is ui-design-language.md.
  5. kn86-sdk — cart authoring: the .kn86 format + grammar + the Rust kn86cart packager + the kn86-sdk crate. Pins NoshAPI v1.
  6. kn86-carts — the launch titles (snake, icebreaker, depthcharge, blackledger, neongrid). Built by kn86-sdk, run on nOSh.
  7. kn86-device — the device / OS host: the /dev/fb0 backend linking nOSh + the system image + the Rust kn86fw / packaging / sd-provision tooling + device config (systemd units, device-tree overlays, kiosk setup).
┌───────────┐
│ kec-lisp │ (language: Fe kernel + KEC Core)
└─────┬─────┘
│ vendored (sync.sh)
┌───────────┐
│ nOSh │ libnosh — C substrate + Fe userland
│ (libnosh) │ publishes L2 contract:
│ │ • render/* (privileged system tier)
│ │ • cell-* (constrained cart tier)
│ │ • NoshAPI v1 (cart-facing FFI, ADR-0005)
└─────┬─────┘
code dep (one-way down) ───────────────┐
┌──────────────┬───────────┬─────────────┤
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐
│ kn86-ui │ │ kn86-sdk │ │kn86-carts│ │ two hosts: │
│ (Fe kit) │ │ (cart │ │ (launch │ │ kn86-emulator │
│ │ │ author) │ │ titles) │ │ (SDL3 backend)│
└────┬─────┘ └────┬─────┘ └────┬─────┘ │ kn86-device │
│ │ │ │ (/dev/fb0) │
│ ui vendored back (sync.sh) into └───────┬───────┘
└──────────► nOSh, kn86-sdk, kn86-carts │ link libnosh
beside nOSh as
surface/input backends
  • Code dependency is one-way down: kec-lisp → nOSh → {kn86-ui, kn86-sdk, kn86-carts}.
  • nOSh is the base. It publishes the L2 contract: the privileged render/* system tier, the constrained cell-* cart tier (ADR-0027, preserved verbatim by ADR-0036), and NoshAPI v1 — the cart-facing FFI surface (ADR-0005).
  • kn86-ui is pure Fe built on that contract, and is vendored back into nOSh, kn86-sdk, and kn86-carts (copy + sync.sh). The code dependency points down; the packaging is the reverse (consumers bundle the ui kit). This is not a cycle: kn86-ui depends only on the published contract, never on nOSh’s implementation.
  • The two hosts (kn86-emulator, kn86-device) sit beside nOSh as surface/input backend implementations of the host seam. nOSh ships only a headless backend for its own ctest; the SDL3 backend lives in kn86-emulator and the /dev/fb0 backend lives in kn86-device.

No dependency cycle exists.

  • Every consumer vendors its dependency via a sync.sh that copies the dependency tree and records the source commit. The template is the existing kn86-emulator/vendor/kec-lisp/sync.sh (in the kinoshita repo) — a copy step plus a recorded upstream commit hash, never an edit-in-place mirror.
  • Bridge during extraction. Until nOSh is carved out (phase 4), the satellite repos (kn86-ui, kn86-sdk, kn86-carts) vendor a frozen substrate/contract shim from the monorepo, then flip their sync.sh source to the real nOSh repo once it lands.
  • Versioning. kn86-sdk pins NoshAPI v1. Cross-repo compatibility is governed by the NoshAPI version plus the render-API surface version — these two surface versions are the compatibility coordinates between nOSh and its consumers.

Clean curated move — each repo is seeded with a single curated initial-extraction commit (the kec-lisp style), no git history preservation. History-preserving extraction (git filter-repo) was considered and rejected (see Options Considered).

PhaseRepo / changeNotes
P0This ADRTopology decision recorded. No code moves.
P1kn86-uiCarve the Fe component kit out first (pure Fe, against the published contract; lowest coupling).
P2kn86-sdkCart format + grammar + kn86cart + kn86-sdk crate; pins NoshAPI v1.
P3kn86-cartsLaunch titles; built by kn86-sdk, vendoring ui.
P4nOSh carve + kn86-emulator reduced to thin hostFolded into the in-flight nOSh re-flow: native renderer landed, the SDL display path + termbox2 spike retired, and the types.h legacy 80×25 constants removed. The library/host split (the backend seam) lands with this re-flow; satellite repos flip their sync.sh source to nOSh.
P5kn86-deviceThe /dev/fb0 backend + system image + kn86fw / packaging / sd-provision + device config.

Green-tree invariant. Each step keeps the monorepo greenctest passes and the kn86cart-built *_cart artifacts build — until that step’s extraction lands. Extraction is incremental, never a flag-day cutover.

Option A: 3 repos (fold carts into sdk, fold device/firmware into runtime)

Section titled “Option A: 3 repos (fold carts into sdk, fold device/firmware into runtime)”

A coarser split: one runtime+device repo, one sdk+carts repo, plus the language. Rejected. Josh chose maximal decomposition so each concern releases on its own cadence. Folding carts into the SDK couples title releases to tooling releases; folding device into the runtime couples the system-image cadence to the library cadence.

Option B: nOSh as a single application with two backends (no separate emulator repo)

Section titled “Option B: nOSh as a single application with two backends (no separate emulator repo)”

nOSh stays an app (not a library), compiling in both an SDL3 and a /dev/fb0 backend; no standalone host repos. Rejected. Josh chose nOSh-as-library + thin hosts. The library shape makes the host seam explicit, lets the two hosts (desktop, device) evolve independently, and gives nOSh a clean headless test target.

Option C: history-preserving extraction (git filter-repo)

Section titled “Option C: history-preserving extraction (git filter-repo)”

Extract each repo carrying its full monorepo history. Rejected in favor of the clean curated move. History preservation drags the entire monolith’s churn into each focused repo, complicates the curated seed, and offers little value given the kec-lisp precedent already established a clean-seed pattern. Pre-extraction history stays addressable in the kinoshita monorepo.

Positive

  • Focused repos: each has a single concern, a single owner surface, and a readable tree.
  • Independent CI and release cadence per concern — language, runtime, ui, sdk, carts, device image each ship when ready.
  • Clear contracts: nOSh’s L2 surface (render tier + cell tier + NoshAPI v1) becomes an explicit, versioned API rather than an implicit in-tree coupling.

Costs / follow-ons

  • N vendoring edges to keep synced. Each sync.sh edge (kec-lisp→nOSh, ui→{nOSh, sdk, carts}, …) is a sync surface that must be re-vendored and its recorded commit bumped on dependency updates.
  • The nOSh carve is a real lib/host re-architecture. Introducing the backend seam (headless / SDL3 / /dev/fb0) is non-trivial and is gated on the in-flight re-flow (P4) — it is not a mechanical move.
  • Cross-repo version coordination via NoshAPI v1 + the render-API surface version: a contract change now ripples across repos and must be versioned and re-vendored deliberately.

Documentation Updates (REQUIRED — Spec Hygiene Rule 3)

Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”

Topology references found by the grep across docs/ — fix-now vs. land-with-phase.

The code has not moved yet (P0 is decision-only), so references that accurately describe the current monorepo layout remain accurate until their extraction phase lands. Each is a tracked follow-on landing with its extraction phase, not a fix-now error:

  • Cart tooling under tools/kn86cart/ADR-0006 (the kn86cart packager + tools/kn86cart/format/kn86cart.h as the on-disk-format source of truth). Accurate today; updates land with P2 (kn86-sdk carve), when kn86cart + the cart format move to kn86-sdk.
  • Device / firmware tooling under tools/ADR-0011 (tools/kn86fw/, tools/flasher/, tools/sd-provision/) and the two DOS-easter-egg research memos under docs/research/ (tools/sd-provision/pi-gen-*). Accurate today; updates land with P5 (kn86-device carve), when this tooling moves to kn86-device.
  • The ui kit as an in-tree system-image/lib/ui/ rowkec-lisp-runtime-architecture.md §5 and ui-design-language.md describe ui/ as a monorepo Fe library. Accurate today; the “kn86-ui is its own repo, vendored back” framing lands with P1.
  • nOSh ≡ the emulator tree — the runtime-architecture draft treats kn86-emulator/ as the home of both the substrate and the userland. Accurate today; the library/host split framing lands with P4.

No fix-now stale references were found in docs/ — every topology-describing reference correctly describes the pre-extraction monorepo and stays correct until its phase lands. This section is the tracking ledger for those phase-gated updates.

Out of scope for this PR (kinoshita-repo change): the root CLAUDE.md “Two Compile Targets” and “Emulator Reference” sections describe kn86-emulator/ as the runtime+host monolith. Those update when the nOSh / emulator split lands (P4) — that is a change in the kinoshita repo, not this kn86-docs PR. Noted here as a follow-on.