wiki: rewrite the project tracker to the task-table standard
Tracker: 10.8 is deployed — T-01 closed
The live site now serves 10.8 with the city split in place, and the API was driven
end to end over a kept-alive connection through Caddy. T-03 is the last row: a
human with a WebGPU browser, after dark.
Tracker: T-01 is down to SSH access
main carries the work and CI is green on it, so the only thing between the repo
and production is that root@91.99.176.243 refuses the only key here. Records the
exact public key to authorize and the command to run instead.
Tracker: T-02, T-04 and T-05 are done; one deploy and one browser check left
T-02, T-04 and T-05 closed, so their rows are gone: a number is never reused, so
the gaps are the record that they are done.
What is left is the two things only a human can do — the 10.8 deploy (which now
also has to sync the new roomcad_api/ package, or the entry point arrives with
nothing to import) and an actual look at the 3D in a WebGPU browser after dark.
The gate figure is now 41 files and 2405 checks.
T-01 also records what was verified without touching the host, so whoever runs
the deploy knows the artifacts are already known good rather than untested.
Tracker: open work only, one numbered row per item
Reduce the tracker to what is outstanding: open work and open decisions, in one
table with a sequential task number.
Removed the rows that are not open work — the Won't-fix decisions (D1-D7, D9) and
the Reference notes (N1-N6) — along with the prose that only described state. They
remain in this wiki's git history, which is the same place completed work already
lives; code-level traps belong to AGENTS.md, as the header says.
Kept and refreshed the three items that are still outstanding (the pending 10.8
deploy, the two unsplit large modules, and looking at the 3D in a real WebGPU
browser), updated the gate figure to the current suite (41 files, 2401 checks),
and added the two open decisions surfaced by the pre-production audit: how
autoLayoutRooms should treat a piece with no door frontage, and whether the
city's vehicle lamps should follow the time of day.
Tracker: five of seven splits done
styles.css, store.js, app.js, editor2d.js and walk3d.js are split; city.js and
server.py remain. Each split was verified against the same check count as before
it, which is the evidence that it was a move rather than a rewrite.
The row also records a caveat rather than hiding it: the walk3d.js commit was
verified by --fast plus a hand check of every constant the skipped fuzz files grep
out of the walk3d source, but the four slow fuzz files had not re-run against it
when work stopped. ./tests/run.sh should be run before the next release.
Tracker: app.js is split; four files remain in T17
styles.css, store.js and app.js are done, each verified by the full suite at the
same check count as before the split — which is the evidence that a split was a
move rather than a rewrite.
app.js was the awkward one and the row now says why: a script wired by ordered
side effects, ten tests grepping its source, three lifting fragments with
new Function, and five module-level lets written from two or three sections at
once. The last of those is the interesting one — an imported binding cannot be
assigned to, so shared mutable state had to become a property of one appState
object rather than five scattered lets.
Remaining: editor2d.js, walk3d.js, city.js (one class, 123 methods, shared
instance state) and server.py, which is last because it is the only one coupled
to the deploy.
Tracker: store.js is split; app.js is next and is the awkward one
store.js is a facade over six files composed with Object.assign. All 78 methods
are present exactly once — checked against the original with brace-aware
extraction, because a duplicate name would silently win with no error and no test
that names it.
The interesting failure was live-multi: it stood five separate clients up by
importing store.js under five query strings, which stopped being five separate
people the moment store.js became a facade. The query string gave a new facade
while the state stayed the one instance, so all five shared a room. One check of
forty-four failed, and the other forty-three went on passing. createStore() now
says what it means.
app.js is next and needs care for three separate reasons, all of which the row
records: it is a script wired by ordered side effects, ten tests grep its source,
and app-internals lifts fragments with new Function. The package-source helper
landed before the split so the tests could keep asking about the real code.
Tracker: T17 has started — styles.css is split and the other six are ordered
styles.css is six sheets under roomcad/web/styles/, cut at the section markers
rather than grouped into tidier files, because the link order is now part of the
cascade: a rule in a later sheet wins a specificity tie exactly as it used to win
by coming later in the one file. Reordering the links, or moving a rule between
sheets out of order, changes which rule wins and nothing reports it.
The split was verified byte-for-byte against the pre-split file — all 25931 bytes
— and is held permanently by tests/styles-split.test.mjs as 'no rule was lost'
rather than 'nothing changed', because the second fails on the next legitimate
edit. A gate that has to be edited away is one nobody trusts.
The row now lists the remaining six in the order they will be taken, with the
reason server.py is last.
Tracker: T16 closed — a repaired load reports what it cost
T16's decision was 'report it', and it is built: sanitize() returns what it
dropped and repaired, the three load paths say so, and a drop raises a toast.
The row is gone, the gate figure is 34 files and 2026 checks, and the version line
now distinguishes the repository (10.8) from the live site (10.7 until someone
deploys) — which they have never been before, and will not be again after the next
deploy.
16 rows: 1 Open, 8 Won't fix, 1 Waiting, 6 Reference.
T16: record the owner's decision — a repaired load must say so
The question was whether a load that had to repair the document should tell the
user. Answer: yes.
The row keeps what it will take, so it is a decision with a plan rather than a
question that has been asked twice. A plan that quietly lost a 12 cm wall is a
plan the user believes is intact, and that is the failure a repair pass must not
have. It is user-facing, so it carries a version bump.
Tracker: T12-T15 closed, and the decisions they pointed at corrected
Four test gaps are closed, so their rows are gone and the decisions behind them
now name the tests that hold them.
D2 was wrong about its own subject. It said walk3d.js's non-axis collider branch
was "left in place deliberately ... the collider's own guard against a wall it
cannot represent". It was not a guard: it was an assumption that anything not
horizontal is vertical, and for any other angle it put the box in the wrong place.
The collider is now oriented to the wall, so D2 records what is actually left
deliberately — the two axis-aligned cases, unrotated, because rotating them would
change the geometry of every wall in every room for no gain.
D8 rested on a false premise and is gone. "The vendored Rapier build carries no
version string" is not true: the export identifier is minified, the value it
returns is not, and RAPIER.version() answers 0.20.0. N6 records that, and the
SHA-256 table that now pins every file under lib/.
D1 and D3 keep their decisions and gain the test that defends them.
17 rows: 2 Open, 8 Won't fix, 1 Waiting, 6 Reference.
Tracker: the model split, and what is left of the 500-line guideline
Task 9 of the cleanup.
T17 records the seven modules still over the guideline and why each is left:
city.js is one class with 123 methods sharing instance state, and walk3d,
editor2d, store and app are the same shape, so splitting them is a refactor of
shared state rather than the move plan.js allowed. server.py is also tied to the
deploy — deploy.sh copies that one file and the unit runs it — so a package means
changing the deploy, the unit and the docs together.
D9 records autoLayoutRooms as a deliberate exception: one 585-line algorithm whose
phases and inner helpers share closure state, which is why plan/layout.js is the
one module still over. N5 notes that plan.js is a facade now, so a source contract
has to read the whole package.
21 rows: 6 Open, 9 Won't fix, 1 Waiting, 5 Reference.
Wiki: a user guide with a worked example, not a history of the repository
Task 4 and 6 of the cleanup.
The tracker had 73 rows and 55 of them were finished work. That made it a history
of the development, which is what git is for. The closed rows are gone; what is
left is one table of 18 — the 5 open items, the 8 deliberate decisions, the one
thing waiting on a human, and the 4 notes that are easy to get wrong twice. A line
at the top says where the completed work went, so the emptiness reads as a
decision rather than as an oversight.
Home is a landing page now: the live URL first, then a six-step "try it in sixty
seconds" example with what each step gives you, then navigation, then how to run
it locally. The sidebar carries the live link on every page.
Architecture and Development was the one page that was about the repository
rather than about using RoomCAD — module line counts, design-decision narratives,
and a second copy of rules that already live in AGENTS.md. It is now how to run
it, how to run the tests, a one-line map of the modules, and a pointer to
AGENTS.md and RELEASE.md as the single source. Repeated rules drift; the copy in
the wiki is the one nobody updates.
Verified: every internal wiki link resolves, no page is orphaned, every page is
reachable from the sidebar, and no page claims the tracker shows finished work.
Tracker: one table, one row per item
The tracker had grown six different shapes for the same thing — a status table, four
"Done" tables grouped by area, an "Open — bugs" table that repeated B1-B4 already
listed above it, an "Open — tasks" table, a "Known and deliberate" list, a "Waiting
on a human" section and a deploy log. Reading it meant knowing which section a
thing would be in.
It is now one table of 73 rows with four columns: ID, item, status, outcome. The ID
prefix says what kind it is (A audit, B bug, T task, D deliberate, H needs a human,
N note), so kind and status are separate facts rather than the same one twice.
Nothing was dropped, and two things were added that only existed elsewhere:
- the guard-check caveat now has a row (N1). It was in the d0c640e commit message —
"the 10 acceptance guards in server-live.test.py ... exist to catch a regression
in the fix, not to detect the absent feature" — which is not where anyone reading
the tracker would find it. This is the row I told the user was in the tracker and
was not.
- the deploy rehearsal became its own row (T18) rather than being folded into the
deploy. Rehearsing is what de-risked the first run of the hardened unit, and the
evidence (the 401 → login → rooms → save round trip, the WAL permission failure
it reproduced, the same-origin header shapes) was worth keeping.
Also fixed: the T3 and T8 rows had run together on one line with no newline, so T8
did not render as a row at all.
The five Open rows are the actionable remainder found while auditing the tracker
itself: tests for the strict furniture helpers, the non-axis collider branch and
Walk3D.dispose(); a hash to pin the unversioned vendored Rapier build; and a
decision on whether sanitize() should report what it repaired.
Tracker: 10.7 is deployed to production
The hardened unit runs the API as roomcadapp inside the sandbox (5.3 MEDIUM, from
9.4 UNSAFE as root). Backed up the live database first with sqlite3 .backup — most
of its data was still only in the -wal file, so a cp would have been wrong — and
verified afterwards: 10.7 served, the directory and all three database files owned
by the service user, data intact at 35 rows, GET / 200, /api/rooms 401
unauthenticated, the legacy :8443 links still answering, a real login through the
public URL returning the room list, and an authenticated live stream opening with
200 text/event-stream.
What is left is not code: the 3D fixes still want a look in a WebGPU browser,
because nothing in the repository renders the scene.
Tracker: the same-origin check verified against production's own Origin/Host shapes
The 8443 entry point is kept for legacy links, and it only keeps working because
the master Caddy forwards Host verbatim (header_up Host {host}) — so the check was
run with the exact shapes: 443 without ports, 8443 with the port on both sides,
8443 with the port stripped from Host (refused, correctly), a cross-site origin
(refused), and no Origin at all (allowed, which is what curl and the suite send).
Tracker: the deploy path is rehearsed — the hardened unit runs, reads and writes
Production could not be touched, so the hardened unit was stood up in a scratch
directory against a copy of the live database, under the same sandbox directives,
as an unprivileged user. It started, refused an unauthenticated call, accepted a
login, read the live rooms and saved one. It also demonstrated the failure it
prevents: a root-owned /var/roomcad kills the app on PRAGMA journal_mode=WAL,
which is why deploy.sh chowns the directory itself.
Tracker + architecture: T2 closed — the app's buttons and the inspector actions are driven
Tracker: name the exact blocker left in T2 (no HTML parsing in the stub, not just no selector engine)
Tracker: app.js can be imported now, and T2 is mostly closed
The blocker T2 recorded — "app.js cannot be imported at all" — is gone. A module
resolution hook applies the page's own import map to the whole graph, which
rewriting import lines cannot do, because the vendored addons behind walk3d import
the bare `three` themselves. So app-wiring.test.mjs boots the real app, lets it
adopt the room the server names, fires its own document keydown handler to check
command-Z reaches store.undo, and checks the live EventSource is opened.
What is left of T2 is the click-driven half: the toolbar and inspector buttons.
The stub DOM has no selector engine, so querySelectorAll returns nothing and a
click cannot be aimed at a real button. A known, small gap with a named cause.
Architecture also gained the resolver, the new test, and a correction — it still
said app.js could not be imported at all.
Tracker: the production-readiness pass — T1, T3-T11 closed, and B5 fixed
Ten of the eleven open tasks are done and each row says what actually changed.
T2 narrows to what is genuinely left: app.js's DOM event wiring and inspector
rendering. The pure logic and the escaping boundary are covered now, and
boot.test.mjs holds the ids the page has to provide.
The pass also found a bug. Making the gate trustworthy meant seeding city-fuzz's
traffic — it drove twenty cities with unseeded Math.random, which is why the full
suite failed where the file alone passed — and hunting that drive turned up a bus
leaving the city grid: a recorded junction decision held only a road INDEX, and
an index is not a junction, so a decision to carry on through index 0 one way was
still counted as a decision when the vehicle came back the other way, where there
is no road. The record names the direction now, the stop-for-a-space path clears
it, and _decideTurn refuses to act on a carry-on record where carrying on is not
possible.
Also recorded: the two traps this cost time to learn — a fresh query string does
not re-evaluate an already-imported ES module (the first login test was silently
inert because of it), and a test that configures its own preconditions can hide
the bug it exists to catch.
Three pages updated with the new facts: the stream caps and the same-origin check
(Server API), the checksum verification and the narrow install default (Hosting),
and the new tests plus the seeding rule (Architecture).
Tracker: all four open bugs fixed; note the light limit and the new invariants
B1-B4 are done, so the tracker's bug table is now a record of what each one
actually did rather than a list of what is wrong, and the status line reads 0
open bugs. Every entry names the test that holds it.
3D Walkthrough said 'only the first 16 get one', which was true and was the bug:
the pool was dealt out in plan order, so a particular lamp was dead for the life
of the room. It now describes what happens instead — the nearest sixteen, and
they follow you — and the 2D Editor Guide's note about lights points at it.
Architecture and Development gains the invariants the fixes created: no shadow
caster may hold a negative depth bias, the two wall-length floors are different
rules, the light budget goes to what the viewer can see, and a plan too large to
decompose reports it. Module line counts refreshed, including the two new test
files.
Resync the wiki with the code, and add a Project Tracker
The wiki had not been touched since 2026-08-23, and 38 commits landed on the
code after that. Some of what it said had been wrong for weeks.
Hosting and Deployment was the worst of it: it documented an nginx :443 ->
Caddy :8077 topology with certbot, a certificate hook and /etc/caddy paths.
None of that has existed since RoomCAD moved behind the host's master Caddy —
the real path is master Caddy -> RoomCAD's own Caddy on loopback :8081 ->
server.py, with no certbot anywhere and the master answering ACME itself. It
now describes the architecture that is actually deployed, the two service
accounts, the systemd sandbox, and why deploy.sh chowns the database.
Architecture and Development claimed city.js was ~630 lines (it is ~5200) and
that app.js is the only module that touches the network (login.js does too,
and plan.js/store.js do no I/O at all). It now has accurate sizes and module
responsibilities, the test runner and CI, and the newer invariants — the sky
dome must contain the walkable city, and frames must stop when the 3D view is
not on screen.
Server API and Storage gained the endpoint it was missing (/api/live-check),
the baseSeq staleness rule, the JSON-object body requirement, and the note
that rooms.db.sql is structure only.
Troubleshooting and Live Collaboration lost their nginx instructions and
gained the service-account failure mode and the two-file Caddy ownership.
Project Tracker is new. There was no tracker anywhere — not in the wiki, not
in issues (the repository has never had one), not in milestones. It records
what this audit fixed, the 4 bugs and 11 tasks still open, and the handful of
things that are deliberate and must not be 'fixed' by accident.