docs(adr): consolidate repository conventions
This commit is contained in:
parent
e66fb87bde
commit
cd6ffc82ff
12
CLAUDE.md
12
CLAUDE.md
@ -10,12 +10,12 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not add a standalone Lua test harness unless asked. Local execution happens through the CraftOS-PC harness (see [`docs/install-craftos-pc.md`](docs/install-craftos-pc.md), [`docs/craftos_pc_glossary.md`](docs/craftos_pc_glossary.md), and [ADR-0005](docs/adrs/adr-0005-craftos-pc-harness.md)); code otherwise executes in-game.
|
||||
- Do not run `just repl` as an LLM agent; it is a human-only interactive CraftOS-PC wrapper. Use `just trapos-exec '<lua>'` for automated probes against the TrapOS dev environment, or `just craftos-exec '<lua>'` for probes against vanilla CraftOS (no TrapOS mounts). These wrappers shut down the machine and include a host watchdog. Headless probes are the recommended way to verify hypotheses about CC:Tweaked behavior; see [ADR-0012](docs/adrs/adr-0012-headless-craftos-pc-as-hypothesis-probe.md).
|
||||
- Do not add a standalone Lua test harness unless asked. Local execution happens through the CraftOS-PC harness (see [`docs/install-craftos-pc.md`](docs/install-craftos-pc.md), [`docs/craftos_pc_glossary.md`](docs/craftos_pc_glossary.md), and [ADR-0005](docs/adrs/adr-0005-craftos-pc-harness-and-probes.md)); code otherwise executes in-game.
|
||||
- Do not run `just repl` as an LLM agent; it is a human-only interactive CraftOS-PC wrapper. Use `just trapos-exec '<lua>'` for automated probes against the TrapOS dev environment, or `just craftos-exec '<lua>'` for probes against vanilla CraftOS (no TrapOS mounts). These wrappers shut down the machine and include a host watchdog. Headless probes are the recommended way to verify hypotheses about CC:Tweaked behavior; see [ADR-0005](docs/adrs/adr-0005-craftos-pc-harness-and-probes.md).
|
||||
- When changing behavior, add as many useful CraftOS-PC tests as practical. It is acceptable to skip tests that require human-only validation, such as complex turtle motion, in-game UX feel, or visual approval, but still add unit-style non-regression tests for deterministic parts when possible.
|
||||
- Use `/apis/libtest.lua` for test scripts under `tests/`; `/programs/runtest.lua` prints `__TRAPOS_TEST_OK__` only after the suite passes.
|
||||
- `libtest` cancels each case after `3`s (`--timeout <s>` / `--no-timeout` to override); never commit a hanging test to `tests/`. Slow harness fixtures go in `tests/harness/` behind dedicated recipes. See [`docs/adrs/adr-0009-layered-test-timeouts.md`](docs/adrs/adr-0009-layered-test-timeouts.md).
|
||||
- Git hooks own commit/push verification: pre-commit runs `just check test`, and pre-push runs `just ci`. When explicitly asked to commit and/or push, do not run `just test` manually first; rely on the hooks. See [`docs/adrs/adr-0011-git-hooks-own-commit-push-verification.md`](docs/adrs/adr-0011-git-hooks-own-commit-push-verification.md).
|
||||
- `libtest` cancels each case after `3`s (`--timeout <s>` / `--no-timeout` to override); never commit a hanging test to `tests/`. Slow harness fixtures go in `tests/harness/` behind dedicated recipes. See [ADR-0007](docs/adrs/adr-0007-test-framework.md).
|
||||
- Git hooks own commit/push verification: pre-commit runs `just check test`, and pre-push runs `just ci`. When explicitly asked to commit and/or push, do not run `just test` manually first; rely on the hooks. See [ADR-0011](docs/adrs/adr-0011-repo-conventions.md).
|
||||
- After editing Lua or Markdown, run `just check` and fix all `luacheck` warnings and `lychee` broken-link reports (markdown link validation is offline-only via `just lint-markdown`).
|
||||
- Use 2-space indent, semicolons, and `local function`.
|
||||
- `require` paths are absolute ComputerCraft paths, for example `require('/apis/net')()`.
|
||||
@ -24,7 +24,7 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS
|
||||
## Architecture
|
||||
|
||||
- `apis/eventloop.lua` is the single-threaded event loop around `os.pullEventRaw`; consider using it everywhere async behavior is needed. A handler that returns `api.STOP` auto-unregisters.
|
||||
- `startup/servers.lua` creates a single boot eventloop at `_G.bootEventLoop` and runs it alongside the shell via `parallel.waitForAny`. Servers register handlers on it and return; programs call services via `os.pullEvent` without touching the loop. See [ADR-0015](docs/adrs/adr-0015-unified-boot-eventloop-and-service-bus.md).
|
||||
- `startup/servers.lua` creates a single boot eventloop at `_G.bootEventLoop` and runs it alongside the shell via `parallel.waitForAny`. Servers register handlers on it and return; programs call services via `os.pullEvent` without touching the loop. See [ADR-0002](docs/adrs/adr-0002-eventloop-and-service-bus.md).
|
||||
- `apis/libtest.lua` is the lightweight test helper used by scripts under `tests/`; `/programs/runtest.lua` discovers tests, renders suite output, and owns the `__TRAPOS_TEST_OK__` success marker.
|
||||
- `apis/net.lua` is a service-name bus on a single channel (`10`). `net.serve(name, handler)` registers a server handler; `net.call(name, payload, { destId, timeout })` and `net.send(name, payload, { destId })` are the client surface. `require('/apis/net')()` returns a singleton bound to `_G.bootEventLoop` when present, otherwise an ephemeral instance.
|
||||
- A router (`/programs/router.lua`) must be running somewhere on the network; it registers a `modem_message` handler that stamps `routerId`, resolves label-addressed packets via a TTL map populated by `servers/net-registrar.lua`, and rebroadcasts. Without it, packets stay unrouted and consumers ignore them.
|
||||
@ -46,6 +46,6 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS
|
||||
- Programs support `-version`/`--version` and `-help`/`--help`; router also supports `-silent`/`--silent`.
|
||||
- French or English comments are fine; match surrounding code.
|
||||
- Commit messages use lightweight conventional style: `topic(scope): description` or `topic: description`.
|
||||
- Reference other `.md` files (and `ADR-####`) with `[text](path)` link syntax so `just lint-markdown` (lychee) can validate them. See [ADR-0013](docs/adrs/adr-0013-markdown-link-syntax-for-cross-references.md).
|
||||
- Reference other `.md` files (and `ADR-####`) with `[text](path)` link syntax so `just lint-markdown` (lychee) can validate them. See [ADR-0011](docs/adrs/adr-0011-repo-conventions.md).
|
||||
|
||||
See [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup.
|
||||
|
||||
@ -8,18 +8,11 @@ Future ADRs can reuse the shape of the existing files when it is useful.
|
||||
|
||||
## Records
|
||||
|
||||
- [`adr-0001-target-computercraft.md`](adr-0001-target-computercraft.md) - Target ComputerCraft.
|
||||
- [`adr-0002-use-eventloop-for-async-code.md`](adr-0002-use-eventloop-for-async-code.md) - Use eventloop for async code.
|
||||
- [`adr-0003-current-net-api-state.md`](adr-0003-current-net-api-state.md) - Current net API state.
|
||||
- [`adr-0004-trapos-branding-and-manifest.md`](adr-0004-trapos-branding-and-manifest.md) - TrapOS branding and manifest-driven installs.
|
||||
- [`adr-0005-craftos-pc-harness.md`](adr-0005-craftos-pc-harness.md) - CraftOS-PC as the local harness.
|
||||
- [`adr-0006-simplify-periphemu-bootstrap.md`](adr-0006-simplify-periphemu-bootstrap.md) - Simplify periphemu bootstrap.
|
||||
- [`adr-0007-use-libtest-for-craftos-tests.md`](adr-0007-use-libtest-for-craftos-tests.md) - Use libtest for CraftOS tests.
|
||||
- [`adr-0008-keep-tests-runnable-in-craftos-and-in-game.md`](adr-0008-keep-tests-runnable-in-craftos-and-in-game.md) - Keep tests runnable in CraftOS and in-game.
|
||||
- [`adr-0009-layered-test-timeouts.md`](adr-0009-layered-test-timeouts.md) - Layered test timeouts (libtest per-case + shell watchdog).
|
||||
- [`adr-0010-ccpm-package-manager.md`](adr-0010-ccpm-package-manager.md) - ccpm package manager (packages, registries, package-aware bootstrap).
|
||||
- [`adr-0011-git-hooks-own-commit-push-verification.md`](adr-0011-git-hooks-own-commit-push-verification.md) - Git hooks own commit/push verification.
|
||||
- [`adr-0012-headless-craftos-pc-as-hypothesis-probe.md`](adr-0012-headless-craftos-pc-as-hypothesis-probe.md) - Headless CraftOS-PC as the canonical hypothesis probe (rename `just craftos` → `just trapos`, add vanilla `just craftos`).
|
||||
- [`adr-0013-markdown-link-syntax-for-cross-references.md`](adr-0013-markdown-link-syntax-for-cross-references.md) - Cross-reference markdown files with `[]()` syntax (so lychee can validate them).
|
||||
- [`adr-0014-prefer-eventloop-settimeout-over-os-sleep.md`](adr-0014-prefer-eventloop-settimeout-over-os-sleep.md) - Prefer `eventloop.setTimeout` over `os.sleep` in application code.
|
||||
- [`adr-0015-unified-boot-eventloop-and-service-bus.md`](adr-0015-unified-boot-eventloop-and-service-bus.md) - Unified boot eventloop and service-name bus (replaces per-server eventloops, channel constants, and `_G.isRouterEnabled`).
|
||||
- [`adr-0001-target-computercraft.md`](adr-0001-target-computercraft.md) — Target ComputerCraft.
|
||||
- [`adr-0002-eventloop-and-service-bus.md`](adr-0002-eventloop-and-service-bus.md) — Eventloop substrate, service-name bus on a single channel, and `os.sleep` discipline.
|
||||
- [`adr-0005-craftos-pc-harness-and-probes.md`](adr-0005-craftos-pc-harness-and-probes.md) — CraftOS-PC as the local harness, minimal periphemu bootstrap, and headless probes as the canonical hypothesis-test pattern.
|
||||
- [`adr-0007-test-framework.md`](adr-0007-test-framework.md) — `libtest` per-case helper, `runtest` suite orchestration, and the two-layer timeout (libtest + shell watchdog).
|
||||
- [`adr-0010-ccpm-package-manager.md`](adr-0010-ccpm-package-manager.md) — `ccpm` package manager (packages, registries, package-aware bootstrap).
|
||||
- [`adr-0011-repo-conventions.md`](adr-0011-repo-conventions.md) — Git hooks own commit/push verification; markdown link syntax for cross-references.
|
||||
|
||||
Gaps in numbering (0003, 0004, 0006, 0008, 0009, 0012, 0013, 0014, 0015) are records that were either superseded by later decisions or consolidated into the surviving ADRs above.
|
||||
|
||||
77
docs/adrs/adr-0002-eventloop-and-service-bus.md
Normal file
77
docs/adrs/adr-0002-eventloop-and-service-bus.md
Normal file
@ -0,0 +1,77 @@
|
||||
# ADR 0002: Eventloop Substrate, Service Bus, and Async Discipline
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-07
|
||||
|
||||
## Context
|
||||
|
||||
ComputerCraft is event-driven. Direct `os.pullEvent` loops are easy to write but hard to compose when multiple things need to happen at the same time. Without a single substrate the repo accumulated several distinct problems:
|
||||
|
||||
- Each long-lived process owned a private event loop, including the router (`programs/router.lua` was a hand-rolled `while true / os.pullEvent`). With N autostart servers, `parallel.waitForAll` ran N coroutines each pumping an independent `os.pullEventRaw`. Events were broadcast to every coroutine but only one would have a relevant handler — wasteful and conceptually awkward.
|
||||
- `_G.isRouterEnabled` mutated send behavior across the codebase. [`apis/net.lua`](../../apis/net.lua) `sendRaw` switched its transmit path based on a global flag set by the router program, so the same function call meant different things depending on which machine ran it.
|
||||
- Channel numbers leaked into every client. `servers/ping-server.lua` and `programs/ping.lua` both duplicated a `PING_CHANNEL` constant; there was no service registry. Adding a new service meant picking a free integer and replicating it on both ends.
|
||||
- Label collision was a silent footgun. Two machines sharing the same label both accepted and rebroadcast packets addressed to that label, producing duplicate responses and duplicate retransmits.
|
||||
- `os.sleep` looked innocent but broke the substrate. Its CC:Tweaked implementation yields via `os.pullEvent("timer")`. While the sleep is in flight, the enclosing eventloop's `os.pullEventRaw` is paused; non-`timer` events are silently discarded; even `eventloop.setTimeout` callbacks scheduled before the sleep cannot fire until it returns. This bit `apis/libai.lua` `pollMessage`, which used a sleep-based throttle and froze the whole loop the moment a caller invoked it from inside a handler.
|
||||
|
||||
Net's blast radius at the time of the bus rewrite was small (only ping consumed it), so a clean break was cheaper than incremental patching.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Eventloop is the async substrate
|
||||
|
||||
New async code uses [`apis/eventloop.lua`](../../apis/eventloop.lua). Event handlers, timers, server listeners, and UI behavior compose through the eventloop instead of each feature owning its own blocking loop.
|
||||
|
||||
- Prefer `eventloop.register`, `setTimeout`, `onStart`, `onStop`, and `startLoop` for async behavior.
|
||||
- APIs that listen for events accept an existing event loop as a constructor argument, the way [`apis/net.lua`](../../apis/net.lua) does. Do not create a private loop inside a module.
|
||||
- Direct `os.pullEvent` loops should be rare and justified (CLI programs waiting for a single reply are the main exception).
|
||||
- A handler that returns `api.STOP` auto-unregisters.
|
||||
|
||||
### 2. One boot eventloop and a service-name bus
|
||||
|
||||
`startup/servers.lua` creates a single `createEventLoop()` instance, stores it at `_G.bootEventLoop`, runs autostart server files (which register handlers and return without blocking), then runs `parallel.waitForAny(shellFn, eventLoopFn)`. The shell and the eventloop are the only two coroutines.
|
||||
|
||||
[`apis/net.lua`](../../apis/net.lua) exposes a service-name bus on a single channel:
|
||||
|
||||
- `net.serve(name, handler)` — register a server handler (server-side).
|
||||
- `net.call(name, payload, opts)` — request/response with timeout (client-side).
|
||||
- `net.send(name, payload, opts)` — fire-and-forget (client-side).
|
||||
- `net.listen(name, handler)` — passive listener.
|
||||
|
||||
All traffic flows on channel `10` and is demultiplexed inside the packet body via a `service` field. Channel numbers stop being a public concept. `require('/apis/net')()` returns a singleton bound to `_G.bootEventLoop` when present, otherwise an ephemeral instance. CLI programs stay standalone: `net.call` internally uses `os.pullEvent` with a timer, so programs do not need the boot eventloop to receive a response.
|
||||
|
||||
[`programs/router.lua`](../../programs/router.lua) registers handlers on the same boot eventloop everything else uses. It owns a TTL-based label map extracted into [`apis/librouter.lua`](../../apis/librouter.lua) for testability. Machines with a label autostart [`servers/net-registrar.lua`](../../servers/net-registrar.lua), which periodically broadcasts `(id, label)` so the router can resolve label-addressed packets. Duplicate label registrations are rejected with a printed warning. `_G.isRouterEnabled` is gone; the router service flips a local flag via `net.setRouter(true)` instead.
|
||||
|
||||
### 3. `os.sleep` discipline
|
||||
|
||||
In library, server, and program code that may run inside an eventloop (directly or transitively), use `eventloop.setTimeout` for any waiting, throttling, polling, or retry-with-delay. Libraries that need to temporize must take an eventloop factory through their constructor rather than baking a hardcoded sleep call. [`apis/net.lua`](../../apis/net.lua) `sendRequest` is the canonical private-eventloop pattern: create a private eventloop, schedule the wait through `setTimeout`, then `runLoop` until the work resolves — synchronous from the caller's perspective, but the dispatcher stays alive internally so handlers can compose around it via `parallel.waitForAll`.
|
||||
|
||||
`os.sleep` remains acceptable only in narrow cases:
|
||||
|
||||
1. One-shot programs that are purely sequential and register no event handlers — a `programs/foo.lua` that prints, sleeps, prints again, and exits.
|
||||
2. `parallel.waitForAny(task, function() sleep(t); end)` used as an isolated guard to bound an inner task (e.g. the AI Lua-exec sandbox in `apis/libai.lua` and the `parallel.waitForAny`-driven per-case timer in `apis/libtest.lua`). The guard sleep is private to its own coroutine group; it does not block anything external.
|
||||
3. Tests that are themselves driven by `libtest`'s per-case timeout (see [ADR-0007](adr-0007-test-framework.md)).
|
||||
|
||||
New code must not expose a `sleep` injection point on its constructor. If a wait is needed, accept an `eventloop` factory and schedule through `setTimeout`. Tests substitute a synchronous deterministic eventloop fake the same way they substitute `http` or `settings`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Adding a new networked service is now: write a `servers/foo.lua` that calls `net.serve('foo', handler)` and returns, then add it to a package's `autostart`. No channel allocation, no `.start()` blocking call.
|
||||
- The router program returns immediately instead of blocking the shell. Users type `router` once on the chosen machine and continue using the shell.
|
||||
- Label collisions are detected and rejected at registration time, with a clear warning, instead of causing silent duplicate delivery.
|
||||
- A router must still be running somewhere on the network for cross-machine label-addressed packets; without one, non-router senders produce packets with `routerId = nil` and consumers drop them on receive.
|
||||
- Programs that need to wait for events still work by direct `os.pullEvent`, but if a program registers a long-lived handler on `_G.bootEventLoop` and exits, the handler keeps firing with a stale closure. Programs should prefer `call`/`send` over `serve`/`listen`. This is documented in [`apis/net.lua`](../../apis/net.lua) but not enforced.
|
||||
- Tests for the router state machine live in [`tests/router.lua`](../../tests/router.lua) and exercise [`apis/librouter.lua`](../../apis/librouter.lua) with an injected clock. Tests for the net packet shape and dispatch live in [`tests/net.lua`](../../tests/net.lua) with a fake modem.
|
||||
- Slightly more ceremony in "synchronous-looking" library functions that wait: a private eventloop plus a small `attempt`/`finish` pair. The benefit is clean composition with any caller's eventloop.
|
||||
- Test fakes shift from a `sleep` stub to a synchronous eventloop double. Ergonomics are comparable; the eventloop fake additionally lets tests observe `pending` and `stopped` state, catching leaks the sleep stub would have missed.
|
||||
- Existing call sites are migrated opportunistically when they cause observable bugs. The first `os.sleep` migration is `apis/libai.lua`.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Multi-router topologies. The single-router assumption stays; a network is expected to run `router` on exactly one machine.
|
||||
- Retry and acknowledgement primitives beyond the existing per-call `timeout`.
|
||||
- Unifying `libtui`, `libai`, and `tuidemo` eventloops. They remain private; they are presentation/AI concerns, not network plumbing.
|
||||
@ -1,28 +0,0 @@
|
||||
# ADR 0002: Use Eventloop For Async Code
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-07
|
||||
|
||||
## Context
|
||||
|
||||
ComputerCraft is event-driven. Direct `os.pullEvent` loops are easy to write, but they are hard to compose when multiple things need to happen at the same time.
|
||||
|
||||
This matters for servers, network listeners, timers, peripheral events, and future UI code. UI code especially will need to handle input, redraws, network replies, and timers together.
|
||||
|
||||
## Decision
|
||||
|
||||
New async code should use `/apis/eventloop`.
|
||||
|
||||
Event handlers, timers, server listeners, and future UI behavior should compose through the event loop instead of each feature owning its own blocking event loop.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Prefer `eventloop.register`, `setTimeout`, `onStart`, `onStop`, and `startLoop` for async behavior.
|
||||
- APIs that listen for events should accept an existing event loop as a constructor argument, the way `/apis/net` already takes one. Do not create a private loop inside a module.
|
||||
- Direct `os.pullEvent` loops should be rare and justified.
|
||||
- Existing code can stay as-is for now, but future async, server, and UI code should move toward eventloop composition.
|
||||
@ -1,36 +0,0 @@
|
||||
# ADR 0003: Current Net API State
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-07
|
||||
|
||||
## Context
|
||||
|
||||
`/apis/net` is the current networking abstraction in this repository.
|
||||
|
||||
It wraps modem messages with packet metadata and uses `/apis/eventloop` for listeners and request/response flows. It is useful for today's basic routed messages and RPC-like requests, but it is not a final protocol design.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep using `/apis/net` for simple program and server messaging.
|
||||
|
||||
Document the current behavior as the baseline, without over-designing the future protocol before real needs appear.
|
||||
|
||||
## Current State
|
||||
|
||||
- Default routing channel is `10`.
|
||||
- Ping channel is `9`.
|
||||
- Packets include `sourceId`, `sourceLabel`, `routerId`, `destId`, and `message`.
|
||||
- Main convenience APIs include `send`, `listen`, `sendRequest`, `sendMultipleRequests`, `listenRequest`, `createEvent`, `createRequest`, and `openChannel` (alias `open`). Listening on a non-default channel requires `openChannel` first.
|
||||
- `sendRequest` and `sendMultipleRequests` run a private event loop, default to a `0.5s` timeout, and return `ok, result, packet` (or `ok, results, packets` for the multi variant).
|
||||
- Router behavior currently lives separately in `/programs/router.lua`. A router must be running on the network — otherwise non-router senders produce packets with `routerId = nil` and `isPacketOk` drops them on receive, so cross-machine messages silently fail.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Use `/apis/net` for current basic messaging needs.
|
||||
- Keep duplicated well-known channel constants in sync while they remain duplicated.
|
||||
- Future ADRs can replace or refine this one if the network protocol gains discovery, retries, schemas, versioning, auth, or a different routing model.
|
||||
@ -1,47 +0,0 @@
|
||||
# ADR 0004: TrapOS Branding And Manifest-Driven Installs
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-07
|
||||
|
||||
## Context
|
||||
|
||||
The project started as a loose collection of ComputerCraft / CC:Tweaked APIs and programs. As the surface area grew (eventloop, net, router, ping, events, upgrade) and the install/upgrade flow gained a beta channel, three pain points emerged:
|
||||
|
||||
- `install.lua` carries a hardcoded `LIST_FILES` table that has to be edited every time a file is added or removed. Adding a single shipped file means editing the file itself, the installer, and sometimes `startup/servers.lua`.
|
||||
- The `--beta` flag is not persisted. The user has to remember to pass it on every `upgrade`, which makes the beta channel awkward to live on.
|
||||
- The system has no visible identity at boot. There is no name, no version line, nothing that confirms which branch is installed.
|
||||
|
||||
At the same time, the codebase has outgrown the "Trap's ComputerCraft APIs" framing. It is closer to a small in-game operating system than a library, and treating it as one unlocks a clearer story (a name, a version, a manifest, a boot banner).
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the name **TrapOS** and a manifest-driven install architecture.
|
||||
|
||||
- A single `manifest.json` at the repo root is the source of truth for the project: name, version, branch, list of shipped files, and the list of servers to autostart at boot. Parsed with `textutils.unserializeJSON` / `textutils.serializeJSON` (built-in to CC:Tweaked).
|
||||
- A local copy of that manifest is written to `/trapos/manifest.json` at the end of every install. This local file is the authoritative system state on the computer:
|
||||
- `branch` is the persisted beta opt-in. Once a user installs with `--beta` (and confirms a one-time `(y/N)` prompt), subsequent `upgrade` calls auto-target the `next` branch with no flag needed.
|
||||
- `version` is what the boot MOTD displays.
|
||||
- `files` and `autostart` drive the next install and the boot sequence.
|
||||
- A new `startup/motd.lua` prints a colored `TrapOS v<version>` line at boot — lime for stable, orange with a `[BETA]` tag for beta. Guarded on `term.isColor()` so monochrome terminals still get the text.
|
||||
- `startup/servers.lua` reads `autostart` from the local manifest instead of carrying its own hardcoded list.
|
||||
- The shipped install URL stays at `https://raw.githubusercontent.com/guillaumearm/cc-libs/...` for now. Renaming the GitHub repository is a follow-up, tracked in the "Future Work" section below.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Adding a new file or autostart server is now a one-line edit to `manifest.json`. Both `install.lua` and `startup/servers.lua` pick it up automatically.
|
||||
- The beta channel becomes a real opt-in: a single confirmed `upgrade --beta` is enough to live on `next`. A new `--stable` flag exists for the symmetric opt-out.
|
||||
- The boot banner gives users an immediate sanity check ("am I on the right branch, the right version?").
|
||||
- The system gains an explicit local state directory (`/trapos/`) and a clear contract for what lives there.
|
||||
- The installer takes a hard dependency on `textutils.serializeJSON` / `unserializeJSON`, which require CC:Tweaked ≥ 1.79. This is well within any reasonable target version for Minecraft 1.21.
|
||||
- Existing computers running the old installer still upgrade cleanly: the old `upgrade` fetches the new `install.lua`, which then creates `/trapos/manifest.json` from the manifest it just downloaded.
|
||||
|
||||
## Future Work
|
||||
|
||||
- **Repository rename.** Once the install flow has stabilized on `TrapOS`, the GitHub repository will be renamed from `cc-libs` to a name that matches (likely `trapos`). The install URL inside `install.lua` and `programs/upgrade.lua` will be updated in the same PR, and a redirect at the old URL is sufficient for in-the-wild installs.
|
||||
- **Package manager.** The longer-term direction floated during planning was a small package manager where each directory in the repo is a package. The current manifest is a deliberate step toward that — same shape (name, version, files), single-package case — and can grow into multi-manifest discovery without changing the install/upgrade contract.
|
||||
- **Per-version migration system.** Today the installer carries a static list of legacy files to delete. A future change can replace this with a per-version migration block driven by the manifest.
|
||||
86
docs/adrs/adr-0005-craftos-pc-harness-and-probes.md
Normal file
86
docs/adrs/adr-0005-craftos-pc-harness-and-probes.md
Normal file
@ -0,0 +1,86 @@
|
||||
# ADR 0005: CraftOS-PC Harness, Periphemu Bootstrap, and Headless Probes
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
This repository targets CC:Tweaked on Minecraft 1.21. The Lua we ship runs inside the ComputerCraft sandbox: it depends on `os.pullEventRaw`, `peripheral`, `rednet`, `textutils.serializeJSON`, modem channels, and CC-specific globals. Standard Lua cannot execute this code as-is, so a normal local test harness was never a serious option.
|
||||
|
||||
Contributors have been running the code in two places:
|
||||
|
||||
- In-game on a real Minecraft server, which is slow to iterate on.
|
||||
- In **CraftOS-PC** (https://www.craftos-pc.cc/), a desktop emulator that ships the same ROM/BIOS as CC:Tweaked, supports modem peripherals via `periphemu`, and can run fully headless (`--cli --headless --script <file>`).
|
||||
|
||||
CraftOS-PC was the *de facto* local harness for months but lived only as a single line in [`CLAUDE.md`](../../CLAUDE.md). There was no install guide, no minimum version, and `just install` did not check the binary was present. Two related concerns emerged on top of that:
|
||||
|
||||
- `startup/servers.lua` historically called `periphemu.create` four times on computer 0 (a top modem, two `computer` peers at ids 1 and 2 both labelled `Trap`, and a router peer at id 10). In CraftOS-PC GUI mode this opened **four windows** on every launch, and the duplicate `Trap` label plus persistent per-id state across versions caused recurring confusion.
|
||||
- Headless CraftOS-PC is also a cheap, deterministic *interactive* tool: it boots the emulator, runs an arbitrary Lua snippet against the real CC:Tweaked ROM, prints output, and exits in well under a second. Humans and agents can use it to verify hypotheses about CC:Tweaked behavior *before* writing code or tests. That usage was implicit; no document framed headless exec recipes as the recommended first move when an agent is unsure about CC:Tweaked behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. CraftOS-PC as a first-class local dev dependency
|
||||
|
||||
- **Minimum version `v2.8.3`** — recent enough to have the current CC:Tweaked ROM, old enough that contributors already on a 2.8.x build will not be forced to upgrade again.
|
||||
- **Documented install** in [`docs/install-craftos-pc.md`](../install-craftos-pc.md), with a SHA-256-verified macOS flow and pointers to the official Windows/Linux artifacts.
|
||||
- **Documented upstream navigation** in [`docs/craftos_pc_glossary.md`](../craftos_pc_glossary.md), covering CLI flags, mounts, `periphemu`, save data, and troubleshooting pages.
|
||||
- **Verified by `just install`** via `check-install`, which checks `craftos`, `jq`, `luacheck`, and `openssl`. `check-craftos` runs `craftos --version` and requires v2.8.3 or newer. Failure prints a one-line pointer to the install guide.
|
||||
- **Repository-local TrapOS launch.** `just trapos` runs CraftOS-PC with `--directory .craftos`, keeps the macOS `--rom /Applications/CraftOS-PC.app/Contents/Resources` workaround, mounts the repository root read-only at `/trapos`, and mounts each top-level source directory read-only at its ComputerCraft root path.
|
||||
- **Vanilla launch.** `just craftos` launches CraftOS-PC under `.craftos-vanilla/` with no mounts and no startup, for probes that should not see TrapOS files and for the `just trapos-install` end-to-end install verification.
|
||||
- **`just ci` is the local verification entry point.** It runs `check-craftos`, `check`, and `test`. Local Git hooks are installed by `just install`; see [ADR-0011](adr-0011-repo-conventions.md) for the commit/push split.
|
||||
|
||||
The existing [`CLAUDE.md`](../../CLAUDE.md) constraint ("Do not run Lua locally or add a test harness unless asked") is reframed rather than removed: there is still no standalone Lua harness, and we are not adding a Busted-style test runner. The harness *is* CraftOS-PC, invoked deliberately.
|
||||
|
||||
### 2. Periphemu bootstrap stays minimal
|
||||
|
||||
`startup/servers.lua` attaches **only a top modem** under `periphemu`:
|
||||
|
||||
```lua
|
||||
if periphemu then
|
||||
periphemu.create('top', 'modem');
|
||||
end
|
||||
```
|
||||
|
||||
Extra emulated computers are spawned manually from the CraftOS-PC shell when actually needed (e.g. `periphemu create 10 computer` to bring up a router peer for cross-VM testing). The pattern is documented in [`docs/periphemu.md`](../periphemu.md). The `if periphemu then` guard is preserved so in-game behavior is unchanged.
|
||||
|
||||
### 3. Headless probes as the canonical hypothesis pattern
|
||||
|
||||
Two safe-exec recipes wrap raw `--headless --exec` with `xpcall`, call `os.shutdown()` on success or Lua error, and use `TRAP_CCLIBS_HEADLESS_TIMEOUT_SECONDS` (default `10`) as a host watchdog:
|
||||
|
||||
- `just trapos-exec '<lua>'` — probe against the **TrapOS dev environment**. Mounts of `/apis`, `/programs`, `/servers`, `/startup`, `/tests`, and the repo root at `/trapos` are live, so `require('/apis/eventloop')` and friends work against the current branch. Use this when the question involves repo code.
|
||||
|
||||
- `just craftos-exec '<lua>'` — probe against **vanilla CraftOS-PC**. No mounts, no startup scripts. Use this when the question is purely about CC:Tweaked behavior and TrapOS files would be a distraction, or to confirm a behavior is upstream rather than something the dev env layered on.
|
||||
|
||||
- `just trapos-install` — drive the full real install (`install-ccpm.lua` → `ccpm update` → `ccpm install trapos`) on a fresh ephemeral state. This is the probe to run when changing anything in the install path itself.
|
||||
|
||||
Conventions:
|
||||
|
||||
- Prefer the safe exec recipes over raw `--headless --exec`.
|
||||
- Keep snippets minimal and side-effect-free. If a probe reveals a fact worth defending, add a `libtest` case under `tests/` — probes are not a substitute for committed tests.
|
||||
- LLM agents SHOULD prefer a quick headless probe over speculation when answering "does X work in CC:Tweaked?" or "does my refactor still load?". The cost is one extra emulator boot (~1s); the benefit is grounded answers instead of plausible-sounding ones.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Contributors must install CraftOS-PC before `just install` succeeds. The install guide makes this a 4-step copy/paste on macOS.
|
||||
- Headless tests live under `tests/` and are driven by `just test`. See [ADR-0007](adr-0007-test-framework.md) for the runner and timeout layering.
|
||||
- The macOS install symlinks the binary into `/usr/local/bin`, which makes CraftOS-PC unable to auto-discover the ROM that ships inside the `.app` bundle (`Could not mount ROM`). The `test:` recipe works around this by passing `--rom /Applications/CraftOS-PC.app/Contents/Resources` on Darwin. Linux (AppImage) and Windows (installer) auto-discover correctly.
|
||||
- `just trapos` uses repository-local save data under `.craftos/config/` and `.craftos/computer/`. This keeps emulator state out of `~/Library/Application Support/CraftOS-PC` during repository work and keeps repo files visible through read-only mounts instead of copying them into the VM save.
|
||||
- `just repl` is a human-only interactive wrapper around `just trapos --cli`; automation and LLM agents must use `just trapos-exec '<lua>'` or `just craftos-exec '<lua>'` instead.
|
||||
- `craftos` (GUI) now opens a single unlabelled `Computer 0` window with a top modem attached. Cross-machine testing requires an explicit `periphemu create` call from the shell rather than being implicit on boot — one extra command when you need a peer, no surprise windows or persisted ghost VMs.
|
||||
- `.craftos-vanilla/` is in `.gitignore` alongside `.craftos/`.
|
||||
- `just trapos-install` is *not* part of `just ci`: it is network-dependent and slower than `just test`. Run it manually when touching `install-ccpm.lua` or ccpm package descriptors.
|
||||
- Higher CraftOS-PC invocation traffic during agent sessions; cheap enough that this is a good trade.
|
||||
- The harness version becomes a project-level concern. When CC:Tweaked ships breaking changes that require a newer CraftOS-PC build, we bump the minimum version in [`docs/install-craftos-pc.md`](../install-craftos-pc.md) and `check-craftos` keeps contributors honest.
|
||||
- No CI integration yet. Running CraftOS-PC headless in GitHub Actions is feasible (the AppImage works on Ubuntu runners) but is out of scope; the contract is local-only for now.
|
||||
|
||||
## Future Work
|
||||
|
||||
- **API-loading smoke test.** Extend `tests/` with a script that `require`s `/apis/eventloop`, `/apis/net`, `/apis/libtest`, and the router, asserting the wiring loads without errors.
|
||||
- **CI.** Run `just test` on push using the Linux AppImage.
|
||||
- **Pinned ROM.** Point CraftOS-PC at a vendored ROM via `--rom` if we ever need to test against a specific in-game version.
|
||||
- If `tests/` grows a multi-VM scenario, drive peer creation from the test script itself (each `tests/*.lua` already owns its setup) rather than re-adding peers to `startup/servers.lua`.
|
||||
@ -1,52 +0,0 @@
|
||||
# ADR 0005: CraftOS-PC As The Local Harness
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
This repository targets CC:Tweaked on Minecraft 1.21. The Lua we ship runs inside the ComputerCraft sandbox: it depends on `os.pullEventRaw`, `peripheral`, `rednet`, `textutils.serializeJSON`, modem channels, and a handful of CC-specific globals. Standard Lua (or LuaJIT) cannot execute this code as-is, so a normal local test harness was never a serious option.
|
||||
|
||||
In practice contributors have been running the code in two places:
|
||||
|
||||
- In-game on a real Minecraft server, which is slow to iterate on.
|
||||
- In **CraftOS-PC** (https://www.craftos-pc.cc/), a desktop emulator that ships the same ROM/BIOS as CC:Tweaked, supports modem peripherals via `periphemu`, and can run fully headless (`--cli --headless --script <file>`).
|
||||
|
||||
CraftOS-PC has been the *de facto* local harness for months, but that fact lived only as a single line in [`CLAUDE.md`](../../CLAUDE.md). There was no install guide, no minimum version, and `just install` did not check that the binary was present. The recent upgrade from v2.6.6 → v2.8.3 (the first one in years) made it obvious that this dependency needed to be made explicit.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat CraftOS-PC as a first-class local development dependency.
|
||||
|
||||
- **Minimum version `v2.8.3`** — recent enough to have the current CC:Tweaked ROM, old enough that contributors already on a 2.8.x build will not be forced to upgrade again immediately.
|
||||
- **Documented install** in [`docs/install-craftos-pc.md`](../install-craftos-pc.md), with a SHA-256-verified macOS flow and pointers to the official Windows/Linux artifacts.
|
||||
- **Documented upstream navigation** in [`docs/craftos_pc_glossary.md`](../craftos_pc_glossary.md), covering CLI flags, mounts, `periphemu`, save data, and troubleshooting pages.
|
||||
- **Verified by `just install`** via `check-install`, which checks `craftos`, `jq`, `luacheck`, and `openssl`. `check-craftos` runs `craftos --version` and requires v2.8.3 or newer. Failure prints a one-line pointer to the install guide instead of a long stack trace.
|
||||
- **Repository-local launch recipe.** `just trapos` runs CraftOS-PC with `--directory .craftos`, keeps the macOS `--rom /Applications/CraftOS-PC.app/Contents/Resources` workaround, mounts the repository root read-only at `/trapos`, and mounts each manifest top-level directory read-only at its ComputerCraft root path. (Originally `just craftos`; renamed when a separate vanilla-emulator recipe was added — see [ADR-0012](adr-0012-headless-craftos-pc-as-hypothesis-probe.md).)
|
||||
- **Vanilla launch recipe.** `just craftos` launches CraftOS-PC under `.craftos-vanilla/` with no mounts, for probes that should not see TrapOS files and for the `just trapos-install` end-to-end install verification. See [ADR-0012](adr-0012-headless-craftos-pc-as-hypothesis-probe.md).
|
||||
- **`just ci` is the local verification entry point.** Today it runs `check-craftos`, `check`, and `test`. Local Git hooks are installed by `just install`; see [ADR-0011](adr-0011-git-hooks-own-commit-push-verification.md) for the current commit/push split.
|
||||
|
||||
The existing [`CLAUDE.md`](../../CLAUDE.md) constraint ("Do not run Lua locally or add a test harness unless asked") is reframed rather than removed: there is still no standalone Lua harness, and we are not adding a Busted-style test runner. The harness *is* CraftOS-PC, invoked deliberately.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Contributors must install CraftOS-PC before `just install` succeeds. The install guide makes this a 4-step copy/paste on macOS.
|
||||
- Contributors should use the local CraftOS-PC glossary first when researching emulator behavior, then follow the linked upstream pages for details.
|
||||
- Headless tests live under `tests/` and are driven by `just test`. Basic boot tests still prove CraftOS-PC boots and the event queue works, and API behavior tests use `/apis/libtest.lua` for named cases and assertions. The `Justfile` launches `/programs/runtest.lua`, which discovers tests, invokes them with `shell.run`, and prints `__TRAPOS_TEST_OK__` only after the suite passes.
|
||||
- Each test process is guarded by `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`, defaulting to `3`, so a blocked ComputerCraft event loop fails quickly and prints captured output.
|
||||
- The macOS install symlinks the binary into `/usr/local/bin`, which makes CraftOS-PC unable to auto-discover the ROM that ships inside the `.app` bundle (`Could not mount ROM`). The `test:` recipe works around this by passing `--rom /Applications/CraftOS-PC.app/Contents/Resources` on Darwin. Linux (AppImage) and Windows (installer) auto-discover correctly, so no flag is passed there.
|
||||
- `just trapos` uses repository-local save data under `.craftos/config/` and `.craftos/computer/`. This keeps emulator state out of `~/Library/Application Support/CraftOS-PC` during repository work and keeps repo files visible through read-only mounts instead of copying them into the VM save.
|
||||
- `just repl` is a human-only interactive wrapper around `just trapos --cli`; automation and LLM agents must use `just trapos-exec '<lua>'` (TrapOS dev env) or `just craftos-exec '<lua>'` (vanilla emulator) instead. [ADR-0012](adr-0012-headless-craftos-pc-as-hypothesis-probe.md) frames these as the canonical hypothesis-probe pattern.
|
||||
- The harness version becomes a project-level concern. When CC:Tweaked ships breaking changes that require a newer CraftOS-PC build, we bump the minimum version in [`docs/install-craftos-pc.md`](../install-craftos-pc.md) and `check-craftos` keeps contributors honest.
|
||||
- No CI integration yet. Running CraftOS-PC headless in GitHub Actions is feasible (the AppImage works on Ubuntu runners) but is out of scope here; the contract is local-only for now.
|
||||
|
||||
## Future Work
|
||||
|
||||
- **API-loading smoke test.** Extend the `tests/` set with a script that `require`s `/apis/eventloop`, `/apis/net`, `/apis/libtest`, and the router, asserting the wiring loads without errors.
|
||||
- **CI.** Run `just test` on push using the Linux AppImage.
|
||||
- **Pinned ROM.** CraftOS-PC ships its own copy of the CC:Tweaked ROM per release. If we ever need to test against a specific in-game version, point CraftOS-PC at a vendored ROM via `--rom` (the same flag we already pass on macOS for a different reason).
|
||||
@ -1,47 +0,0 @@
|
||||
# ADR 0006: Simplify `periphemu` Bootstrap
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
`startup/servers.lua` historically called `periphemu.create` four times on computer 0: a top modem, two `computer` peers at ids 1 and 2 (both labelled `Trap` in saved per-computer config), and a router peer at id 10. In CraftOS-PC GUI mode this opened **four windows** on every launch (`Computer 0`, two duplicate `Trap`s, and `Router`).
|
||||
|
||||
That setup predates the current workflow:
|
||||
|
||||
- The install path is now manifest-driven (`install.lua` v3.0.0 fetches files listed in `manifest.json` and writes a marker at `/trapos/manifest.json`). The legacy `apis/`, `programs/`, `servers/`, `install.lua` files hand-copied into each per-computer dir are obsolete.
|
||||
- Local verification runs **headless** through `just test` against scripts in `tests/`; the smokes don't need persistent emulated peers.
|
||||
- The duplicate `Trap` label and persistent stale computer state across versions caused recurring confusion.
|
||||
|
||||
The CraftOS-PC data directory (`~/Library/Application Support/CraftOS-PC/`) had also accumulated years of stale state in `computer/`, `old_computer/`, and per-id `config/*.json` files (labels like `test`, two `Trap`s, etc.).
|
||||
|
||||
## Decision
|
||||
|
||||
- `startup/servers.lua` attaches **only a top modem** under `periphemu`:
|
||||
|
||||
```lua
|
||||
if periphemu then
|
||||
periphemu.create('top', 'modem');
|
||||
end
|
||||
```
|
||||
|
||||
- Extra emulated computers are spawned manually from the CraftOS-PC shell when actually needed (e.g. `periphemu create 10 computer` to bring up a router peer for cross-VM testing). The pattern is documented in [`docs/periphemu.md`](../periphemu.md).
|
||||
- Wiped the live CraftOS-PC data dir back to a clean slate (one-time, manually). Removed `computer/`, `old_computer/`, and all per-id `config/*.json`. Preserved `config/global.json` (user-level CraftOS-PC settings). A full pre-wipe backup lives at `~/Backups/craftos-pc-2026-06-08/CraftOS-PC/` outside any repo.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `craftos` (GUI) now opens a single unlabelled `Computer 0` window with a top modem attached. No more duplicate `Trap` windows.
|
||||
- `just test` is unchanged: the headless smokes set up whatever they need per script.
|
||||
- New contributors and fresh machines get a clean state by default — no inherited per-computer config to chase.
|
||||
- Cross-machine testing now requires an explicit `periphemu create` call from the shell rather than being implicit on boot. That is a deliberate trade-off: the cost is one extra command when you genuinely need a peer; the benefit is no surprise windows and no persisted ghost VMs.
|
||||
- The `if periphemu then` guard (called out in [CLAUDE.md](../../CLAUDE.md)) is preserved, so in-game behavior is unchanged.
|
||||
|
||||
## Future Work
|
||||
|
||||
- If `tests/` grows a multi-VM scenario, drive peer creation from the test script itself (each `tests/*.lua` already owns its setup) rather than re-adding peers to `startup/servers.lua`.
|
||||
- Consider a small `programs/spawn-peer.lua` wrapper to make ad-hoc shell incantations friendlier (`spawn-peer 10` instead of `periphemu create 10 computer`). Not done now — premature for current usage.
|
||||
79
docs/adrs/adr-0007-test-framework.md
Normal file
79
docs/adrs/adr-0007-test-framework.md
Normal file
@ -0,0 +1,79 @@
|
||||
# ADR 0007: Test Framework — libtest, runtest, and Layered Timeouts
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0005](adr-0005-craftos-pc-harness-and-probes.md) made CraftOS-PC the local harness. The first behavior test, `tests/eventloop.lua`, proved the harness can exercise ComputerCraft APIs headlessly, but it also duplicated test-runner concerns directly in the script: collecting named cases, per-case progress in verbose mode, fail-fast messaging, success-marker emission, and process shutdown. Those details are easy to copy incorrectly.
|
||||
|
||||
A blocked eventloop test then showed two more harness needs:
|
||||
|
||||
- The shell harness needs a timeout and captured output so agentic debugging can proceed without manual interruption.
|
||||
- A single shell watchdog is coarse. `kill -TERM` on the whole CraftOS-PC process cannot say *which* case hung, produces one generic message, and cannot tell a cooperatively-blocked event loop (the common failure — waiting on an event or `sleep` that never resolves) apart from a genuinely wedged process. A per-case timeout inside Lua is both finer and faster, but the shell watchdog is still needed for the cases Lua cannot interrupt.
|
||||
|
||||
At the same time, tests in this repository are still ComputerCraft programs. They should be useful in CraftOS-PC and in-game, not only inside a host-side shell loop.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. `libtest` is the per-case helper
|
||||
|
||||
[`apis/libtest.lua`](../../apis/libtest.lua) is the repository's lightweight ComputerCraft test helper. Tests under `tests/` require it with an absolute ComputerCraft path:
|
||||
|
||||
```lua
|
||||
local createLibTest = require('/apis/libtest');
|
||||
local testlib = createLibTest({ ... });
|
||||
|
||||
testlib.test('example', function()
|
||||
testlib.assertEquals(1 + 1, 2);
|
||||
end);
|
||||
|
||||
testlib.run();
|
||||
```
|
||||
|
||||
`libtest` intentionally stays small. It provides named cases, `assertEquals`, `assertTrue`, `assertErrors`, optional case-status report output consumed by the suite runner, and failure reporting. It must stay usable from normal ComputerCraft programs in CraftOS-PC or in-game.
|
||||
|
||||
### 2. `runtest` owns suite orchestration; the Justfile stays minimal
|
||||
|
||||
[`/programs/runtest.lua`](../../programs/runtest.lua) owns suite-level concerns: test discovery under `/tests`, invoking each script with `shell.run`, grouped `--pretty` output, `--verbose` runner diagnostics, the `__TRAPOS_TEST_OK__` success marker (printed only after the full suite passes), and optional shutdown when the host harness asks with `--shutdown`. `runtest` can run inside CraftOS-PC or in-game when `/tests` and dependencies are present.
|
||||
|
||||
The `Justfile` launches CraftOS-PC, mounts repository directories, enforces the process timeout, checks for the success marker, and prints runner output files. It does not know about individual test files or cases. Verbose mode is reserved for debugging and agent work loops; `--pretty` is the normal human-readable mode.
|
||||
|
||||
### 3. Layered test timeouts
|
||||
|
||||
Two independent timeout layers, ordered so the finer one fires first.
|
||||
|
||||
**Layer 1 — `libtest` per-case timeout (primary).** [`apis/libtest.lua`](../../apis/libtest.lua) races each test case against a timer with `parallel.waitForAny(runner, timer)`. The default is `DEFAULT_TIMEOUT_SECONDS = 3`. When the timer wins, the case fails with a distinct message containing the token `libtest timeout` and, in `--verbose`, an extra `TIMEOUT … (libtest)` diagnostic. `--timeout <seconds>` overrides the default; `--no-timeout` disables the layer. `runtest` forwards both flags to each case script. This only interrupts cases that yield (the usual hang); a non-yielding CPU loop cannot be preempted in ComputerCraft.
|
||||
|
||||
**Layer 2 — shell watchdog (backstop).** The `Justfile` `test:` recipe keeps its `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS` watchdog as an independent double-check. Its default matches the libtest default (`.env.test` ships `3`; the recipe falls back to `3`) so libtest fires first for yielding cases in normal runs and the watchdog only catches what Lua cannot — a non-yielding loop, a wedged libtest, or a deliberately bypassed case. Its SIGTERM message is worded differently from the `libtest timeout` message, so the two layers are never confused.
|
||||
|
||||
### How to write tests properly
|
||||
|
||||
- Normal tests live in `tests/*.lua`, use `/apis/libtest.lua`, and must finish under the libtest timeout. `runtest` auto-discovers them; `just test` runs the suite.
|
||||
- Never commit a hanging or intentionally-slow test to `tests/`: it would fail every run.
|
||||
- Intentionally-slow fixtures that exercise the harness itself live in `tests/harness/`. `runtest` discovery skips subdirectories, so they never run with the normal suite; they are driven only by dedicated recipes (`just test-timeout-lua`, `just test-timeout-shell`, aggregated by `just test-timeout`).
|
||||
- Use `--no-timeout` only for harness fixtures that must outlive the libtest layer to prove the shell watchdog, never for ordinary tests.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New deterministic behavior should get as many useful CraftOS-PC tests as practical. Tests that require human validation, such as complex turtle motion, in-game UX feel, or visual approval, may be skipped, but deterministic pieces should still get unit-style non-regression coverage.
|
||||
- Test scripts remain normal ComputerCraft programs, not standalone Lua tests. They can run through `just test`, `/programs/runtest.lua`, or direct in-game execution when copied with their dependencies.
|
||||
- `libtest` lives under `/apis` and ships in `trapos-test`, so it can be required consistently in the mounted CraftOS-PC environment.
|
||||
- The `__TRAPOS_TEST_OK__` marker remains the single shell-level success contract and is owned by `runtest`.
|
||||
- A hung case fails in ~3s with a per-case message instead of taking down the whole process anonymously.
|
||||
- `just test-timeout` is a self-asserting harness regression guard wired into `just ci`. It chains `test-timeout-lua` (Layer 1: libtest cancels the slow case immediately with `--timeout 0`, before the shell backstop) and `test-timeout-shell` (Layer 2: the `TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS` watchdog, default `1`, kills the slow case with libtest bypassed). Both drive a single `tests/harness/slow-case.lua` fixture; the tight timeouts — not the fixture's sleep length — decide which layer fires, so the harness itself is covered against regressions on every `ci`.
|
||||
- `libtest` stays a normal ComputerCraft program: `parallel` and `sleep` are sandbox globals, so the timeout works in CraftOS-PC and in-game alike.
|
||||
- Host-specific concerns remain outside production Lua code.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Add more assertions only when tests need them; avoid growing a large framework.
|
||||
- Add test selection filters when the suite grows.
|
||||
- Add runner-level or per-case timing in `--verbose` output if slow-but-passing cases become hard to spot.
|
||||
- A `libtest`-level marker for "expected timeout" if more harness fixtures appear.
|
||||
- Explore GitHub Actions with the Linux CraftOS-PC AppImage after local coverage is broader.
|
||||
@ -1,56 +0,0 @@
|
||||
# ADR 0007: Use libtest For CraftOS Tests
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0005 made CraftOS-PC the local harness. The first real behavior test, `tests/eventloop.lua`, proved the harness can exercise ComputerCraft APIs headlessly, but it also duplicated test-runner concerns directly in the script:
|
||||
|
||||
- Collect named cases.
|
||||
- Print per-case progress only in verbose mode.
|
||||
- Fail fast with a useful message.
|
||||
- Report success only after every assertion passes, allowing the suite runner to print `__TRAPOS_TEST_OK__` once.
|
||||
- Shut the ComputerCraft process down cleanly.
|
||||
|
||||
Those details are easy to copy incorrectly. A blocked eventloop test also showed that the shell harness needs a timeout and captured output so agentic debugging can proceed without manual interruption.
|
||||
|
||||
## Decision
|
||||
|
||||
Add `/apis/libtest.lua` as the repository's lightweight ComputerCraft test helper.
|
||||
|
||||
Tests under `tests/` should require it with an absolute ComputerCraft path:
|
||||
|
||||
```lua
|
||||
local createLibTest = require('/apis/libtest');
|
||||
local testlib = createLibTest({ ... });
|
||||
|
||||
testlib.test('example', function()
|
||||
testlib.assertEquals(1 + 1, 2);
|
||||
end);
|
||||
|
||||
testlib.run();
|
||||
```
|
||||
|
||||
`libtest` intentionally stays small. It provides named cases, `assertEquals`, `assertTrue`, `assertErrors`, optional case-status report output consumed by the suite runner, and failure reporting.
|
||||
|
||||
`/programs/runtest.lua` owns suite-level concerns: test discovery, invoking test scripts, grouped pretty output, verbose runner diagnostics, the `__TRAPOS_TEST_OK__` success marker, and optional shutdown. The shell harness keeps only host-level concerns: CraftOS-PC launch flags, read-only mounts, stdout capture, and timeout enforcement through `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New deterministic behavior should get as many useful CraftOS-PC tests as practical.
|
||||
- Tests that require human validation, such as complex turtle motion, in-game UX feel, or visual approval, may be skipped, but deterministic pieces should still get unit-style non-regression coverage.
|
||||
- Test scripts remain normal ComputerCraft programs, not standalone Lua tests. They can run through `just test`, `/programs/runtest.lua`, or direct in-game execution when copied with their dependencies.
|
||||
- `libtest` lives under `/apis` and is listed in `manifest.json`, so it can be required consistently in the mounted CraftOS-PC environment.
|
||||
- The `__TRAPOS_TEST_OK__` marker remains the single shell-level success contract and is owned by `/programs/runtest.lua`.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Add more assertions only when tests need them; avoid growing a large framework.
|
||||
- Add more runner filters only when useful; keep the common no-argument path as automatic `tests/*.lua` discovery.
|
||||
- Explore GitHub Actions with the Linux CraftOS-PC AppImage after local coverage is broader.
|
||||
@ -1,37 +0,0 @@
|
||||
# ADR 0008: Keep Tests Runnable In CraftOS And In-Game
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
The initial CraftOS-PC harness proved that repository code can be tested locally, but the first shell recipe also owned too much suite behavior: explicit test lists, per-case rendering, and success formatting. That made the host shell script more complex than necessary and tied test orchestration to CraftOS-PC launch details.
|
||||
|
||||
At the same time, tests in this repository are still ComputerCraft programs. They should be useful in CraftOS-PC and in-game, not only inside a host-side shell loop.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep `/apis/libtest.lua` focused on test cases and assertions. It must stay usable from normal ComputerCraft programs in CraftOS-PC or in-game.
|
||||
|
||||
Move suite orchestration into `/programs/runtest.lua`. The runner discovers tests under `/tests`, invokes each script with `shell.run`, renders grouped `--pretty` output, emits additional `--verbose` diagnostics, prints `__TRAPOS_TEST_OK__` only after the full suite passes, and can shut down when the host harness asks with `--shutdown`.
|
||||
|
||||
Keep the `Justfile` minimal. It launches CraftOS-PC, mounts repository directories, enforces the process timeout, checks for the success marker, and prints runner output files. It should not know about individual test files or cases.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Tests using `libtest` remain plain ComputerCraft programs.
|
||||
- `/programs/runtest.lua` can be run inside CraftOS-PC or in-game when `/tests` and dependencies are present.
|
||||
- Pretty colors and grouped suite output are runner concerns, not `libtest` concerns.
|
||||
- Verbose mode is reserved for debugging and agent work loops, while `--pretty` is the normal human-readable mode.
|
||||
- Host-specific concerns remain outside production Lua code.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Add test selection filters when the suite grows.
|
||||
- Add runner-level timing if slow tests become hard to diagnose.
|
||||
- Add more `libtest` assertions only when real tests need them.
|
||||
@ -1,73 +0,0 @@
|
||||
# ADR 0009: Layered Test Timeouts
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0005 made CraftOS-PC the local harness and ADR 0007 split `libtest` (cases and
|
||||
assertions) from `runtest` (suite orchestration). The only timeout in that harness was the
|
||||
shell watchdog in the `Justfile` `test:` recipe: it `kill -TERM`s the whole CraftOS-PC
|
||||
process after `TRAP_CCLIBS_TEST_TIMEOUT_SECONDS`.
|
||||
|
||||
That single layer is coarse. It kills the entire process, so it cannot say *which* case
|
||||
hung, it produces one generic message, and it cannot tell a cooperatively-blocked event
|
||||
loop (the common failure: waiting on an event or `sleep` that never resolves) apart from a
|
||||
genuinely wedged process. A per-case timeout inside Lua is both finer and faster, but the
|
||||
shell watchdog is still needed for the cases Lua cannot interrupt.
|
||||
|
||||
## Decision
|
||||
|
||||
Run two independent timeout layers, ordered so the finer one fires first.
|
||||
|
||||
**Layer 1 — `libtest` per-case timeout (primary).** `/apis/libtest.lua` races each test
|
||||
case against a timer with `parallel.waitForAny(runner, timer)`. The default is
|
||||
`DEFAULT_TIMEOUT_SECONDS = 3`. When the timer wins, the case fails with a distinct message
|
||||
containing the token `libtest timeout` and, in `--verbose`, an extra `TIMEOUT … (libtest)`
|
||||
diagnostic. `--timeout <seconds>` overrides the default; `--no-timeout` disables the layer.
|
||||
`/programs/runtest.lua` forwards both flags to each case script. This only interrupts cases
|
||||
that yield (the usual hang); a non-yielding CPU loop cannot be preempted in ComputerCraft.
|
||||
|
||||
**Layer 2 — shell watchdog (backstop).** The `Justfile` `test:` recipe keeps its existing
|
||||
`TRAP_CCLIBS_TEST_TIMEOUT_SECONDS` watchdog unchanged, as an independent double-check. Its
|
||||
default matches the libtest default (`.env.test` ships `3`; the recipe falls back to `3`)
|
||||
so libtest should fire first for yielding cases in normal runs and the watchdog only catches
|
||||
what Lua cannot — a non-yielding loop, a wedged libtest, or a deliberately bypassed case. Its
|
||||
SIGTERM message is worded differently from the `libtest timeout` message, so the two layers
|
||||
are never confused.
|
||||
|
||||
## How To Write Tests Properly
|
||||
|
||||
- Normal tests live in `tests/*.lua`, use `/apis/libtest.lua`, and must finish under the
|
||||
libtest timeout. `runtest` auto-discovers them; `just test` runs the suite.
|
||||
- Never commit a hanging or intentionally-slow test to `tests/`: it would fail every run.
|
||||
- Intentionally-slow fixtures that exercise the harness itself live in `tests/harness/`.
|
||||
`runtest` discovery skips subdirectories, so they never run with the normal suite; they
|
||||
are driven only by dedicated recipes (`just test-timeout-lua`, `just test-timeout-shell`,
|
||||
aggregated by `just test-timeout`).
|
||||
- Use `--no-timeout` only for harness fixtures that must outlive the libtest layer to prove
|
||||
the shell watchdog, never for ordinary tests.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A hung case now fails in ~3s with a per-case message instead of taking down the whole
|
||||
process anonymously.
|
||||
- `just test-timeout` is a self-asserting harness regression guard wired into `just ci`. It
|
||||
chains `test-timeout-lua` (Layer 1: libtest cancels the slow case immediately with
|
||||
`--timeout 0`, before the shell backstop) and `test-timeout-shell` (Layer 2: the
|
||||
`TRAP_CCLIBS_TEST_TIMEOUT_WATCHDOG_SECONDS` watchdog, default `1`, kills the slow case with
|
||||
libtest bypassed). Both drive a single `tests/harness/slow-case.lua` fixture; the tight
|
||||
timeouts — not the fixture's sleep length — decide which layer fires, so the harness itself
|
||||
is covered against regressions on every `ci`.
|
||||
- `libtest` stays a normal ComputerCraft program: `parallel` and `sleep` are sandbox
|
||||
globals, so the timeout works in CraftOS-PC and in-game alike.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Per-case timing in `--verbose` output if slow-but-passing cases become hard to spot.
|
||||
- A `libtest`-level marker for "expected timeout" if more harness fixtures appear.
|
||||
@ -10,10 +10,13 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0004 made installs manifest-driven: `install.lua` reads a flat `manifest.json`
|
||||
file list from a branch and `wget`s every file. That is all-or-nothing. There is no
|
||||
way to install just networking or just the UI, and no way to add or remove pieces of
|
||||
the OS after the initial install.
|
||||
The previous install flow (a `LIST_FILES` table inside `install.lua` and a single flat
|
||||
`manifest.json` read by `wget`) was all-or-nothing. There was no way to install just
|
||||
networking or just the UI, and no way to add or remove pieces of the OS after the
|
||||
initial install. The project also outgrew the "Trap's ComputerCraft APIs" framing into
|
||||
a small in-game OS — TrapOS — with a name, a version, a boot banner, and a persisted
|
||||
beta channel; that motion is preserved here even though the original install
|
||||
mechanism is fully replaced.
|
||||
|
||||
We want a package manager, `ccpm` ("ComputerCraft Package Manager"), installed first
|
||||
as a standalone user-facing step. After that, a machine can `ccpm update`,
|
||||
@ -68,7 +71,12 @@ dependencies), downloads its files, and writes:
|
||||
|
||||
- `/trapos/manifest.json` — the aggregated `{ name, version, branch, files, autostart }`
|
||||
still consumed by `startup/motd.lua` and `startup/servers.lua` after boot packages
|
||||
are installed;
|
||||
are installed. This is the surviving piece of the previous manifest-driven install:
|
||||
it is no longer the install source of truth (each package's `ccpm.json` is), but it
|
||||
is still the local system state used at boot for the colored `TrapOS v<version>`
|
||||
banner and to read the `autostart` list. `branch` is the persisted beta opt-in
|
||||
(a single confirmed `--beta` install switches subsequent `ccpm upgrade` runs to
|
||||
`next` with no flag needed; `--stable` is the symmetric opt-out);
|
||||
- `/trapos/ccpm.lock.json` — so right after a fresh install `ccpm install trapos-core`
|
||||
correctly reports "already installed";
|
||||
- `/trapos/ccpm.json` — seeding/refreshing the default `guillaumearm/cc-libs` registry
|
||||
@ -89,7 +97,7 @@ first to refresh available versions.
|
||||
## Consequences
|
||||
|
||||
- The repo gains a `packages/` descriptor tree; the flat source layout is untouched.
|
||||
- `just trapos` (formerly `just craftos`; see [ADR-0012](adr-0012-headless-craftos-pc-as-hypothesis-probe.md)) no longer derives mounts
|
||||
- `just trapos` (formerly `just craftos`; see [ADR-0005](adr-0005-craftos-pc-harness-and-probes.md)) no longer derives mounts
|
||||
from `manifest.json .files` (it is now `.packages`); it mounts a fixed list of
|
||||
top-level dirs instead. `just test` was already on fixed mounts and is unaffected.
|
||||
- ccpm logic is covered by `tests/ccpm.lua` (URL resolution, dependency ordering,
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
# ADR 0011: Git Hooks Own Commit/Push Verification
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
The repository already has a CraftOS-PC harness (`just test`) and a fuller local CI path
|
||||
(`just ci`) that adds tool checks, `luacheck`, and harness regression guards. Agents and
|
||||
humans can also be asked to commit and push changes, which means verification can happen in
|
||||
two places: manually before Git operations and automatically inside Git hooks.
|
||||
|
||||
Running the same tests manually and then again in hooks makes commit/push workflows slower
|
||||
without improving the success contract. It also risks divergent habits between human and
|
||||
agent workflows if agents run one verification path and hooks run another.
|
||||
|
||||
## Decision
|
||||
|
||||
Install two local Git hooks through `just install` / `just install-git-hooks`:
|
||||
|
||||
- `.git/hooks/pre-commit` runs `just check test`.
|
||||
- `.git/hooks/pre-push` runs `just ci`.
|
||||
|
||||
When an agent is explicitly asked to commit and/or push, it should not run `just test`
|
||||
manually before the Git operation. The hook is the source of truth for that workflow:
|
||||
commit triggers `just check test`, and push triggers `just ci`.
|
||||
|
||||
Manual verification is still appropriate outside commit/push workflows. For example, run
|
||||
`just test` while developing a behavior change, and run `just ci` when checking the full
|
||||
local state without pushing.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Commit and push verification is consistent for humans and agents.
|
||||
- Commit workflows avoid duplicating the same CraftOS-PC test run before and during
|
||||
`git commit`.
|
||||
- Push workflows still get the full local CI gate before remote updates.
|
||||
- The hooks are local files under `.git/hooks`, so developers should run `just install`
|
||||
after cloning or after hook behavior changes.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Revisit the split if `just test` becomes too slow for pre-commit or `just ci` gains
|
||||
checks that should also block commits.
|
||||
60
docs/adrs/adr-0011-repo-conventions.md
Normal file
60
docs/adrs/adr-0011-repo-conventions.md
Normal file
@ -0,0 +1,60 @@
|
||||
# ADR 0011: Repository Conventions — Git Hooks and Markdown Link Syntax
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-08
|
||||
|
||||
## Context
|
||||
|
||||
Two small operational conventions emerged together as the repo grew and want to be remembered:
|
||||
|
||||
- **Where verification runs.** The repository has a CraftOS-PC harness (`just test`) and a fuller local CI path (`just ci`) that adds tool checks, `luacheck`, and harness regression guards. Agents and humans can be asked to commit and push changes, so verification can happen in two places: manually before Git operations and automatically inside Git hooks. Running the same tests manually and again in hooks makes workflows slower without improving the success contract, and risks divergent habits between human and agent paths.
|
||||
- **How docs reference each other.** The `docs/` tree increasingly cross-references itself, and ADRs reference one another by number. `lychee` is wired into `just check` (`lint-markdown` recipe) so broken local links fail the build. Lychee only validates *links it can see*: a bare prose mention of `docs/foo.md` or `ADR-0005`, or a backticked path like `` `docs/foo.md` ``, is invisible to it. When a doc is renamed or moved, those mentions silently rot until a human happens to read the surrounding paragraph.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Git hooks own commit/push verification
|
||||
|
||||
Install two local Git hooks through `just install` / `just install-git-hooks`:
|
||||
|
||||
- `.git/hooks/pre-commit` runs `just check test`.
|
||||
- `.git/hooks/pre-push` runs `just ci`.
|
||||
|
||||
When an agent is explicitly asked to commit and/or push, it should not run `just test` manually before the Git operation. The hook is the source of truth for that workflow: commit triggers `just check test`, and push triggers `just ci`.
|
||||
|
||||
Manual verification is still appropriate outside commit/push workflows. For example, run `just test` while developing a behavior change, and run `just ci` when checking the full local state without pushing.
|
||||
|
||||
### 2. Markdown link syntax for cross-references
|
||||
|
||||
In every markdown file in the repository, references to other `.md` files must use markdown link syntax `[text](relative/path.md)`. This includes:
|
||||
|
||||
- Direct file references (`` `docs/install-craftos-pc.md` `` → `` [`docs/install-craftos-pc.md`](docs/install-craftos-pc.md) ``).
|
||||
- ADR-number references (`ADR-0005` → `[ADR-0005](docs/adrs/adr-0005-craftos-pc-harness-and-probes.md)`, with the path adjusted to be relative to the referencing file).
|
||||
- Plain-prose mentions (`called out in CLAUDE.md` → `called out in [CLAUDE.md](../../CLAUDE.md)`).
|
||||
|
||||
Excluded by design:
|
||||
|
||||
- Mentions inside fenced code blocks. They are example/code content; `lychee.toml` skips them via `include_verbatim = false`.
|
||||
- Mentions inside inline code spans used purely to *illustrate* the wrong form. The reader can see they are placeholders, and lychee does not extract them as links either.
|
||||
- A file's own title (e.g. `# CLAUDE.md` as a heading is not a reference).
|
||||
|
||||
Link paths are written relative to the file containing the link, so `lychee --offline` resolves them on the local filesystem with no `--base` indirection.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Commit and push verification is consistent for humans and agents.
|
||||
- Commit workflows avoid duplicating the same CraftOS-PC test run before and during `git commit`.
|
||||
- Push workflows still get the full local CI gate before remote updates.
|
||||
- The hooks are local files under `.git/hooks`, so developers should run `just install` after cloning or after hook behavior changes.
|
||||
- `just lint-markdown` (and therefore `just check`, pre-commit, and pre-push) catches dead cross-references the moment a doc is moved or renamed.
|
||||
- The markdown-link convention is *social* — humans and agents must remember to apply it when writing prose. Lychee enforces correctness only once a link exists; it cannot flag a mention that should have been a link but wasn't.
|
||||
- The `[`path`](path)` style (backticked path as link text) is preferred for direct file references, matching the existing house style in [`docs/README.md`](../README.md) and [`docs/adrs/README.md`](README.md). For ADR mentions in flowing prose, `[ADR-####](path)` reads better than the backticked form.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Revisit the verification split if `just test` becomes too slow for pre-commit or `just ci` gains checks that should also block commits.
|
||||
- If rot reappears in prose despite the link convention, add a small grep-based lint that flags unbracketed `.md` and `ADR-####` mentions and wire it into `just check`. Deferred until there is evidence the social convention is insufficient.
|
||||
@ -1,90 +0,0 @@
|
||||
# ADR 0012: Headless CraftOS-PC As The Canonical Hypothesis Probe
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-09
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0005](adr-0005-craftos-pc-harness.md) made CraftOS-PC the local harness and
|
||||
[ADR-0007](adr-0007-use-libtest-for-craftos-tests.md) /
|
||||
[ADR-0008](adr-0008-keep-tests-runnable-in-craftos-and-in-game.md) /
|
||||
[ADR-0009](adr-0009-layered-test-timeouts.md) wired it into the test suite via `just test`.
|
||||
That work focused on the *test* path. Headless CraftOS-PC is also a cheap, deterministic
|
||||
*interactive* tool: `just craftos-exec '<lua>'` boots the emulator, runs an arbitrary
|
||||
Lua snippet against the real CC:Tweaked ROM, prints output to stdout, and exits in well
|
||||
under a second. Humans and LLM agents can use it to verify hypotheses
|
||||
about CC:Tweaked behavior *before* writing code or tests — "does `os.epoch('utc')` return
|
||||
ms?", "does my new API factory `require` cleanly?", "does `fs.exists` follow symlinks
|
||||
inside `--mount-ro`?".
|
||||
|
||||
Today this usage was implicit: the harness existed, but no document framed
|
||||
safe headless exec recipes as the recommended first move when an agent is unsure about
|
||||
CC:Tweaked behavior. The original recipe was also named `just craftos` even though it
|
||||
mounted the entire TrapOS dev environment — so probes against it were never against
|
||||
vanilla CC:Tweaked, even when the agent thought they were.
|
||||
|
||||
Two concrete changes triggered this ADR:
|
||||
|
||||
1. **Recipe split.** The old `just craftos` (TrapOS dev mounts + persistent `.craftos/`) is
|
||||
renamed `just trapos`. A new `just craftos` launches a fresh, mount-less CraftOS-PC
|
||||
under `.craftos-vanilla/`. A `just trapos-install` recipe exercises the real ccpm
|
||||
bootstrap on an ephemeral state to validate the install path end-to-end.
|
||||
2. **Explicit guidance.** Agents working in this repo should reach for a headless probe
|
||||
the moment they catch themselves guessing about CC:Tweaked behavior, instead of
|
||||
speculating or committing changes that only run `luacheck`.
|
||||
|
||||
## Decision
|
||||
|
||||
Frame headless CraftOS-PC as the canonical hypothesis-probe pattern, with two safe
|
||||
exec flavors:
|
||||
|
||||
- `just trapos-exec '<lua>'` — probe against the **TrapOS dev
|
||||
environment**. Mounts of `/apis`, `/programs`, `/servers`, `/startup`, `/tests`, and the
|
||||
repo root at `/trapos` are live, so `require('/apis/eventloop')` and friends work
|
||||
against the current branch. Use this when the question involves repo code.
|
||||
|
||||
- `just craftos-exec '<lua>'` — probe against **vanilla
|
||||
CraftOS-PC**. No mounts, no startup scripts. Use this when the question is purely about
|
||||
CC:Tweaked behavior and TrapOS files would be a distraction, or to confirm a behavior
|
||||
is upstream rather than something the dev env layered on.
|
||||
|
||||
- `just trapos-install` — drive the full real install (`install-ccpm.lua` →
|
||||
`ccpm update` → `ccpm install trapos`) on a fresh ephemeral state. This is the probe
|
||||
to run when changing anything in the install path itself.
|
||||
|
||||
Conventions:
|
||||
|
||||
- Prefer the safe exec recipes over raw `--headless --exec`. They wrap snippets with
|
||||
`xpcall`, call `os.shutdown()` on success or Lua error, and use
|
||||
`TRAP_CCLIBS_HEADLESS_TIMEOUT_SECONDS` (default `10`) as a host watchdog for true hangs.
|
||||
- Keep snippets minimal and side-effect-free. If the probe reveals a fact worth defending,
|
||||
add a `libtest` case under `tests/` — probes are not a substitute for committed tests.
|
||||
- LLM agents SHOULD prefer a quick headless probe over speculation when answering
|
||||
"does X work in CC:Tweaked?" or "does my refactor still load?". The cost is one extra
|
||||
emulator boot (~1s); the benefit is grounded answers instead of plausible-sounding ones.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Higher CraftOS-PC invocation traffic during agent sessions; cheap enough that this is a
|
||||
good trade.
|
||||
- Faster convergence on correct fixes: agents stop committing speculative changes that pass
|
||||
`luacheck` but fail in-game.
|
||||
- A named pattern (`just trapos-exec '<lua>'` / `just craftos-exec '<lua>'`) shows up in [`CLAUDE.md`](../../CLAUDE.md) and
|
||||
[`docs/install-craftos-pc.md`](../install-craftos-pc.md), so contributors and agents reach for it without rediscovery.
|
||||
- `.craftos-vanilla/` is added to `.gitignore` alongside `.craftos/`.
|
||||
- `just trapos-install` is *not* part of `just ci`: it is network-dependent and slower
|
||||
than `just test`. Run it manually when touching `install-ccpm.lua` or ccpm package
|
||||
descriptors.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [ADR-0005](adr-0005-craftos-pc-harness.md) — CraftOS-PC as the local harness.
|
||||
- [ADR-0007](adr-0007-use-libtest-for-craftos-tests.md) — libtest for CraftOS tests.
|
||||
- [ADR-0009](adr-0009-layered-test-timeouts.md) — layered test timeouts.
|
||||
- [ADR-0010](adr-0010-ccpm-package-manager.md) — ccpm package manager (drives the
|
||||
`just trapos-install` flow).
|
||||
@ -1,41 +0,0 @@
|
||||
# ADR 0013: Cross-reference Markdown Files With `[]()` Syntax
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-09
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0012](adr-0012-headless-craftos-pc-as-hypothesis-probe.md) and the broader docs/ tree increasingly cross-reference each other, and ADRs reference one another by number. We recently wired `lychee` into `just check` (see the `lint-markdown` recipe in `Justfile`) so broken local links fail the build. Lychee, however, only validates *links it can see*: a bare prose mention of `docs/foo.md` or `ADR-0005`, or a backticked path like `` `docs/foo.md` ``, is invisible to it. When a doc is renamed or moved, those mentions silently rot until a human happens to read the surrounding paragraph.
|
||||
|
||||
We want renames to break the build, not the docs.
|
||||
|
||||
## Decision
|
||||
|
||||
In every markdown file in the repository, references to other `.md` files must use markdown link syntax `[text](relative/path.md)`. This includes:
|
||||
|
||||
- Direct file references (`` `docs/install-craftos-pc.md` `` → `` [`docs/install-craftos-pc.md`](docs/install-craftos-pc.md) ``).
|
||||
- ADR-number references (`ADR-0005` → `[ADR-0005](docs/adrs/adr-0005-craftos-pc-harness.md)`, with the path adjusted to be relative to the referencing file).
|
||||
- Plain-prose mentions (`called out in CLAUDE.md` → `called out in [CLAUDE.md](../../CLAUDE.md)`).
|
||||
|
||||
Excluded by design:
|
||||
|
||||
- Mentions inside fenced code blocks. They are example/code content, not cross-references, and `lychee.toml` already skips them via `include_verbatim = false`.
|
||||
- Mentions inside inline code spans used purely to *illustrate* the wrong form (as in this ADR's examples above). The reader can see they are placeholders, and lychee does not extract them as links either.
|
||||
- A file's own title (e.g. `# CLAUDE.md` as a heading is not a reference).
|
||||
|
||||
Link paths are written relative to the file containing the link, so that `lychee --offline` resolves them on the local filesystem with no `--base` indirection.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `just lint-markdown` (and therefore `just check`, pre-commit, and pre-push via [ADR-0011](adr-0011-git-hooks-own-commit-push-verification.md)) catches dead cross-references the moment a doc is moved or renamed.
|
||||
- The convention is *social* — humans and agents must remember to apply it when writing prose. Lychee enforces correctness only once a link exists; it cannot flag a mention that should have been a link but wasn't.
|
||||
- The `[`path`](path)` style (backticked path as link text) is preferred for direct file references, matching the existing house style in [`docs/README.md`](../README.md) and [`docs/adrs/README.md`](README.md). For ADR mentions in flowing prose, `[ADR-####](path)` reads better than the backticked form.
|
||||
|
||||
## Future Work
|
||||
|
||||
- If rot reappears anyway, add a small grep-based lint that flags unbracketed `.md` and `ADR-####` mentions and wire it into `just check`. Deferred until there is evidence the social convention is insufficient.
|
||||
@ -1,87 +0,0 @@
|
||||
# ADR 0014: Prefer `eventloop.setTimeout` Over `os.sleep` In Application Code
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-09
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0002](adr-0002-use-eventloop-for-async-code.md) made [`apis/eventloop.lua`](../../apis/eventloop.lua) the
|
||||
default substrate for async behavior. The eventloop drives a single
|
||||
`os.pullEventRaw` loop and dispatches every event to registered handlers and
|
||||
timer callbacks.
|
||||
|
||||
`os.sleep` looks innocent but breaks that contract. Its CC:Tweaked
|
||||
implementation yields the coroutine via `os.pullEvent("timer")`. While the
|
||||
sleep is in flight:
|
||||
|
||||
- The enclosing `os.pullEventRaw` of the eventloop is paused; nothing else
|
||||
runs in that coroutine.
|
||||
- Non-`timer` events that arrive are silently discarded by `os.pullEvent` —
|
||||
so handlers registered through the eventloop miss them entirely.
|
||||
- Even `eventloop.setTimeout` callbacks scheduled before the sleep cannot
|
||||
fire until the sleep returns, because their timer events are consumed by
|
||||
the sleep filter.
|
||||
|
||||
This bit `apis/libai.lua` `pollMessage`, which used a sleep-based throttle
|
||||
between HTTP polls. The function looked synchronous and stand-alone, but
|
||||
the moment a caller invoked `libai.ask` from inside a handler — exactly the
|
||||
composition [ADR-0002](adr-0002-use-eventloop-for-async-code.md)
|
||||
encourages — the whole event loop froze for the duration of every poll
|
||||
interval.
|
||||
|
||||
[`apis/net.lua`](../../apis/net.lua) `sendRequest` already shows the right
|
||||
pattern: create a private eventloop, schedule the wait through
|
||||
`setTimeout`, then `runLoop` until the work resolves. From the caller's
|
||||
perspective the function is still synchronous; internally, the dispatcher
|
||||
of timer events stays alive and arbitrary other handlers can be composed
|
||||
around it via `parallel.waitForAll`.
|
||||
|
||||
## Decision
|
||||
|
||||
In library, server, and program code that may run inside an eventloop
|
||||
(directly or transitively), use `eventloop.setTimeout` for any waiting,
|
||||
throttling, polling, or retry-with-delay. Libraries that need to temporize
|
||||
must take an eventloop factory through their constructor (the way
|
||||
`apis/net` does) rather than baking a hardcoded sleep call.
|
||||
|
||||
`os.sleep` remains acceptable only in narrow cases:
|
||||
|
||||
1. One-shot programs that are purely sequential and register no event
|
||||
handlers — a `programs/foo.lua` that prints, sleeps, prints again, and
|
||||
exits.
|
||||
2. `parallel.waitForAny(task, function() sleep(t); end)` used as an
|
||||
isolated guard to bound an inner task (e.g. the AI Lua-exec sandbox in
|
||||
`apis/libai.lua` and the `parallel.waitForAny`-driven per-case timer in
|
||||
`apis/libtest.lua`). The guard sleep is private to its own coroutine
|
||||
group; it does not block anything external.
|
||||
3. Tests that are themselves driven by `libtest`'s per-case timeout (see
|
||||
[ADR-0009](adr-0009-layered-test-timeouts.md)).
|
||||
|
||||
New code must not expose a `sleep` injection point on its constructor. If
|
||||
a wait is needed, accept an `eventloop` factory and schedule through
|
||||
`setTimeout`. Tests substitute a synchronous deterministic eventloop fake
|
||||
the same way they substitute `http` or `settings`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Slightly more ceremony in "synchronous-looking" functions that wait: a
|
||||
private eventloop plus a small `attempt`/`finish` pair. The benefit is
|
||||
that the function composes cleanly with any caller's eventloop.
|
||||
- Test fakes shift from a `sleep` stub to a synchronous eventloop double.
|
||||
Ergonomics are comparable; the eventloop fake additionally lets tests
|
||||
observe `pending` and `stopped` state, catching leaks the sleep stub
|
||||
would have missed.
|
||||
- Existing call sites are migrated opportunistically when they cause
|
||||
observable bugs. The first migration is `apis/libai.lua`.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-0002](adr-0002-use-eventloop-for-async-code.md) — use eventloop for async code.
|
||||
- [ADR-0009](adr-0009-layered-test-timeouts.md) — layered test timeouts (the `parallel.waitForAny` guard exception).
|
||||
- [`apis/net.lua`](../../apis/net.lua) `sendRequest` — canonical private-eventloop pattern.
|
||||
- [`apis/libai.lua`](../../apis/libai.lua) `pollMessage` — first migration.
|
||||
@ -1,49 +0,0 @@
|
||||
# ADR 0015: Unified Boot Eventloop and Service-Name Bus
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-09
|
||||
|
||||
## Context
|
||||
|
||||
Before this change, the TrapOS networking and boot model had accumulated several issues that were hard to fix incrementally:
|
||||
|
||||
- **Label collision was a silent footgun.** Two machines sharing the same label both accepted and rebroadcast packets addressed to that label, since `programs/router.lua:40` and `apis/net.lua:35` both matched `os.getComputerLabel()` independently. The sender received duplicate responses and the wire carried duplicate retransmits.
|
||||
- **`_G.isRouterEnabled` mutated send behavior across the codebase.** [`apis/net.lua`](../../apis/net.lua) `sendRaw` switched its transmit path based on a global flag set by the router program. This made the same function call mean different things depending on which machine ran it.
|
||||
- **Every autostart server ran its own private eventloop.** Each server file called `net.start()` which delegated to `el.startLoop()`. With N autostart entries, `parallel.waitForAll` ran N coroutines, each pumping an independent `os.pullEventRaw`. Wasteful and conceptually awkward: events were broadcast to every coroutine but only one would have a relevant handler.
|
||||
- **The router lived outside the eventloop entirely.** [`programs/router.lua`](../../programs/router.lua) was a hand-rolled `while true / os.pullEvent` loop, structurally different from every other long-lived process in the repo.
|
||||
- **Channel numbers leaked into every client.** `servers/ping-server.lua` and `programs/ping.lua` both duplicated a `PING_CHANNEL = 9` constant; there was no service registry. Adding a new service meant picking a free integer and replicating it on both ends.
|
||||
|
||||
Net's blast radius was small at this point — only `programs/ping.lua` and `servers/ping-server.lua` consumed it — so a clean break was cheaper than incremental patching.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt three coordinated changes:
|
||||
|
||||
1. **One boot eventloop per machine.** `startup/servers.lua` creates a single `createEventLoop()` instance, stores it at `_G.bootEventLoop`, runs autostart server files (which register handlers and return without blocking), then runs `parallel.waitForAny(shellFn, eventLoopFn)`. The shell and the eventloop are the only two coroutines.
|
||||
|
||||
2. **Service-name addressing on a single bus channel.** [`apis/net.lua`](../../apis/net.lua) exposes `net.serve(name, handler)`, `net.call(name, payload, opts)`, `net.send(name, payload, opts)`, and `net.listen(name, handler)`. All traffic flows on channel `10` and is demultiplexed inside the packet body via a `service` field. Channel numbers stop being a public concept. `require('/apis/net')()` returns a singleton bound to `_G.bootEventLoop` when present, otherwise an ephemeral instance.
|
||||
|
||||
3. **Router as a service on the boot eventloop.** [`programs/router.lua`](../../programs/router.lua) registers handlers on the same boot eventloop everything else uses. It owns a TTL-based label map (extracted into [`apis/librouter.lua`](../../apis/librouter.lua) for testability). Machines with a label autostart [`servers/net-registrar.lua`](../../servers/net-registrar.lua), which periodically broadcasts `(id, label)` so the router can resolve label-addressed packets. Duplicate label registrations are rejected with a printed warning. `_G.isRouterEnabled` is gone; the router service flips a local flag via `net.setRouter(true)` instead.
|
||||
|
||||
CLI programs stay standalone: `net.call` internally uses `os.pullEvent` with a timer, so programs do not need the boot eventloop to receive a response.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Adding a new networked service is now: write a `servers/foo.lua` that calls `net.serve('foo', handler)` and returns, then add it to a package's `autostart`. No channel allocation, no `.start()` blocking call.
|
||||
- The router program returns immediately instead of blocking the shell. Users type `router` once on the chosen machine and continue using the shell.
|
||||
- Label collisions are detected and rejected at registration time, with a clear warning, instead of causing silent duplicate delivery.
|
||||
- The ping API surface changed (`net.sendRequest` → `net.call`, `net.listenRequest` → `net.serve`). Out-of-tree consumers — if any existed — would need to migrate. Inside the repo only ping needed migration.
|
||||
- Programs that need to wait for events still work by direct `os.pullEvent`, but if a program registers a long-lived handler on `_G.bootEventLoop` and exits, the handler keeps firing with a stale closure. Programs should prefer `call`/`send` over `serve`/`listen`. This is documented in [`apis/net.lua`](../../apis/net.lua) but not enforced.
|
||||
- Tests for the router state machine live in [`tests/router.lua`](../../tests/router.lua) and exercise [`apis/librouter.lua`](../../apis/librouter.lua) with an injected clock. Tests for the net packet shape and dispatch live in [`tests/net.lua`](../../tests/net.lua) with a fake modem.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Multi-router topologies. The single-router assumption stays; a network is expected to run `router` on exactly one machine.
|
||||
- Retry and acknowledgement primitives beyond the existing per-call `timeout`.
|
||||
- Unifying `libtui`, `libai`, and `tuidemo` eventloops. They remain private; they are presentation/AI concerns, not network plumbing.
|
||||
- The `ccpm` package manager. It is recent, tested, and not in pain.
|
||||
@ -1,6 +1,6 @@
|
||||
# Install CraftOS-PC
|
||||
|
||||
CraftOS-PC is the local harness used to run this repo's Lua outside of Minecraft. See [ADR-0005](adrs/adr-0005-craftos-pc-harness.md) for why.
|
||||
CraftOS-PC is the local harness used to run this repo's Lua outside of Minecraft. See [ADR-0005](adrs/adr-0005-craftos-pc-harness-and-probes.md) for why.
|
||||
|
||||
CraftOS-PC is the emulator; `craftos` is the command-line executable it installs. For the broader upstream documentation index, see [`craftos_pc_glossary.md`](craftos_pc_glossary.md).
|
||||
|
||||
@ -105,7 +105,7 @@ Override the watchdog with `TRAP_CCLIBS_HEADLESS_TIMEOUT_SECONDS` (default `10`)
|
||||
Pass CraftOS-PC flags directly after `just trapos` or `just craftos` only for
|
||||
manual launches where you want raw emulator control.
|
||||
|
||||
See [`docs/adrs/adr-0012-headless-craftos-pc-as-hypothesis-probe.md`](adrs/adr-0012-headless-craftos-pc-as-hypothesis-probe.md) for the canonical headless probe pattern used to verify hypotheses about CC:Tweaked behavior.
|
||||
See [ADR-0005](adrs/adr-0005-craftos-pc-harness-and-probes.md) for the canonical headless probe pattern used to verify hypotheses about CC:Tweaked behavior.
|
||||
|
||||
`just repl` delegates to `just trapos --cli` for human interactive use only. LLM agents must not run `just repl`.
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
In-repo reference for the `periphemu` global exposed by [CraftOS-PC](https://www.craftos-pc.cc/). It does **not** exist in CC:Tweaked in-game — always guard usage with `if periphemu then`.
|
||||
|
||||
Upstream: <https://www.craftos-pc.cc/docs/periphemu>. See also the [CraftOS-PC glossary](craftos_pc_glossary.md) and [ADR-0005](adrs/adr-0005-craftos-pc-harness.md) (why CraftOS-PC is our local harness) and [ADR-0006](adrs/adr-0006-simplify-periphemu-bootstrap.md) (why our startup attaches only a modem).
|
||||
Upstream: <https://www.craftos-pc.cc/docs/periphemu>. See also the [CraftOS-PC glossary](craftos_pc_glossary.md) and [ADR-0005](adrs/adr-0005-craftos-pc-harness-and-probes.md) (why CraftOS-PC is our local harness, and why our startup attaches only a modem).
|
||||
|
||||
## Usage in this repo
|
||||
|
||||
@ -63,7 +63,7 @@ Call `periphemu.names()` from a CraftOS-PC shell to confirm what the current bui
|
||||
|
||||
## Caveats
|
||||
|
||||
- **CC:T has no `periphemu`.** Always guard with `if periphemu then`. CLAUDE.md and [ADR-0005](adrs/adr-0005-craftos-pc-harness.md) treat that guard as mandatory.
|
||||
- **CC:T has no `periphemu`.** Always guard with `if periphemu then`. CLAUDE.md and [ADR-0005](adrs/adr-0005-craftos-pc-harness-and-probes.md) treat that guard as mandatory.
|
||||
- **Speaker `playAudio` differs from CC:T** unless [standards mode](https://www.craftos-pc.cc/docs/standards) is on — CraftOS-PC queues audio without ever returning `false`.
|
||||
- **Modem network id** segregates emulated networks. Two modems with different ids will not see each other; useful for testing the [router](../programs/router.lua) against a sealed network.
|
||||
- **Persisted per-computer state** lives at `~/Library/Application Support/CraftOS-PC/computer/<id>/` and `config/<id>.json` (labels stored base64-encoded). Spawning a new id leaves that state behind across sessions.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user