web/app.js, one 6,400-line closure, is now fourteen ES modules under web/graph/, cut by concern: state, utilities, data and feed, layout, canvas drawing, labels and handles, the frame loop, the side panel, the cards' wiring, input, the emoji tally's card, the toolbar and controls, the address bar, and main.js. index.html loads it as <script type="module" src="graph/main.js">, which the CSP's 'self' allows, with no bundler and no build step. A new web/README.md says which module owns what. lanes.js is 560 lines and one concern, so it stays as it is.
The split is mechanical. A scope analysis moved each top-level statement verbatim, with its comments, and wrote each module's imports and exports. A statement-level comparison against app.js finds 441 of 443 statements byte-identical after the dedent. The other two now call one of four small setters (setFeed, setLiving, shiftClock, wantGroup), because an ES module's imports are read-only. state.js imports nothing, so it always runs first, and a check of the modules' evaluation order finds no binding read before it's initialised. The web tests pass unchanged. The canvas harness now runs the modules as the one script they were cut from, so each case still gets a fresh graph, and one new test keeps that list complete. The Caddy route test passes with the new directory.
Rendered deterministically (a saved /view, a paused fake clock, seeded Math.random), the page is pixel-identical before and after at 1440 and 390. The live page, the mock page, a ?warm load and /lanes show no console or CSP errors. Raw script weight goes from 258.7 KB to 256.8 KB. Gzipped it's 78 KB as one file and 89 KB as fourteen, which carry their own import and export lines.
| before | after (pixel-identical) | |
|---|---|---|
| 1440 | ||
| 390 |
🤖 Generated with Claude Code