Repository navigation
Architecture and Development
How to run RoomCAD locally, run its tests, and find the rules. The rules
themselves are not repeated here — they live in the repository, in
AGENTS.md (the one
instruction file) and
RELEASE.md (the
release standard). One copy, in the repository, is the point: a rule written down
twice drifts, and the copy in the wiki is the one nobody updates.
ROOMCAD_DB_PATH=/tmp/roomcad.db ROOMCAD_PASSWORD=letmein \
python3 roomcad/server/server.py & # the API on 127.0.0.1:8078
cd roomcad/web && ./serve.sh # the app on http://localhost:8080There is no build step and nothing to install. web/Caddyfile proxies /api/*
to the local API, so saving, versions and live collaboration all work locally;
skip the API and you still get the app, with a "server not reachable" banner.
The first serve.sh run downloads Caddy into web/bin/ (git-ignored).
The 3D view needs WebGPU — a current Chrome or Edge, or Safari Technology
Preview. There is no CDN fallback: Three.js and Rapier are vendored under
roomcad/web/lib/.
./tests/run.sh # everything — the real gate
./tests/run.sh --fast # skip the four long fuzz files, ~45 s
./tests/run.sh plan-editing # just the files whose name matches
ROOMCAD_TEST_TIMEOUT=600 ./tests/run.sh # seconds per file (default 1800)41 files and 2405 checks. city-fuzz alone takes about twelve minutes, so the
full sweep is 13–15 minutes and --fast is what you iterate with. The same suite
runs in CI on every push and pull request.
There is no test framework: each file is a plain Node script that prints
N passed, M failed and exits non-zero, and the runner totals them. Five are
worth knowing by name:
| File | What it pins that nothing else can |
|---|---|
boot.test.mjs |
the app's real import graph — every specifier resolved through the import map, every element id the page must have. No browser, no dependencies. It proves the graph and the DOM contract, not that anything renders |
app-wiring.test.mjs |
boots the real app.js, adopts the room the server names, and drives its own keyboard handler |
app-clicks.test.mjs |
clicks the real buttons and the inspector's delegated actions |
city-turns.test.mjs |
the traffic's junction decisions — the bug where a bus left the grid |
login.test.mjs |
the password gate, including that it fails closed when the server is unreachable |
Anything that drives the traffic must seed it. city.js uses real
Math.random() in production on purpose, so an unseeded fuzz is a coin toss that
fails on a busy machine. city-fuzz, city-physics and city-turns each call
setTransportRandom() first; seed any new drive the same way.
The app is plain ES modules with no bundler, loaded through the import map in
roomcad/web/index.html.
| Module | What lives there |
|---|---|
plan.js |
the room model: grid and snapping, wall and opening geometry, wall joins, room detection, auto-layout, the .rcad format. Pure — no DOM, no network |
store.js |
editing state, tools, selection, undo/redo, applying a remotely-changed room. It does no I/O |
editor2d.js |
the 2D canvas: drawing, hit-testing, zoom, pan, rotate |
walk3d.js |
the Three.js walkthrough, Rapier physics, sun, lights, bloom |
city.js |
the stylised city around the room: streets, blocks, traffic, signals, weather |
app.js |
UI glue and every /api/* call — with login.js, the only module that touches the network |
server/server.py |
the whole API: stdlib Python, SQLite, SSE for live collaboration |
plan.js and store.js do no I/O, which is why so much of the app can be tested
in Node with no server and no DOM. app.js and walk3d.js used to be impossible
to import at all; the test harness resolves the page's import map for the whole
graph now, so they can be driven for real. AGENTS.md explains how, and the traps
that go with it.
Deployment is a separate, documented operation:
Hosting and Deployment is the procedure, and
roomcad/server/deploy.sh is the script. It validates the candidate Caddyfile
with the server's own Caddy before installing it, and refuses to report success
until the live URL answers. The version is not bumped by deploying: bump
roomcad/web/version.js in the same change as the user-facing fix you are
shipping.
A deploy is not a release, and the two use different scripts. ./release.sh cuts
a release: it is a dry run until you pass --publish, and then it tags, packages
and publishes a GitHub Release. RELEASE.md is the standard it implements, and it
is edited in this repository and nowhere else.
Releases are a source archive — nothing here compiles, so there is no binary
and no architecture to assert. release.sh reads the version out of
roomcad/web/version.js, so the tag cannot disagree with what the footer shows; it
refuses a dirty tree or a branch that is not main; it checks the packaged archive
for the files the standard requires by reading the archive rather than the working
tree; and after publishing it re-downloads the asset and compares the digest.
The notes live in docs/release-notes-vX.Y.md, one section per user-visible change
with the check that backs it, ending in a checksum block the script substitutes at
publish time — the committed file keeps the placeholders, so a rebuild can never
leave a stale digest in the tree.
The first release is v10.8, cut from main. Its assets are
roomcad-10.8-src.tar.gz and its .sha256; the same digest is quoted in the
release notes, and the archive carries the project's LICENSE, its
THIRD_PARTY_NOTICES.md and a README-binaries.txt stating the platform floor.