# frontend The ClojureScript half. See `docs/port-plan.md` for what is being built and in what order; this file is only how to run it. ## Once ```sh mise install # from the REPO ROOT: java 21+, node 20, clojure, python cd frontend && npm install ``` `java` must be 21+. On an older JDK shadow-cljs fails with "CompilerOptions has been compiled by a more recent version of the Java Runtime", which reads like a shadow-cljs bug and is not one. `mise install` is what prevents it. ## The tests ```sh cd frontend && mise exec -- npm test ``` Two things: compile the `:test` build, run it under node. ``` shadow-cljs compile test node out/node-tests.js ``` Run them separately if a compile error is in the way. **Run them through `mise`**, or make sure `mise`'s node is first on PATH. `java` must be 21+ and node 20.19+. On an nvm node 20.11 shadowing the pinned one, things fail in ways that read like the code being broken and are not. ### And the browser one Step 5's done-criterion is a PICTURE, and no assertion in `cljs.test` can check one: a take that resolves to the right numbers and draws nothing would pass every test in `arthur.flow.freeze-test`. A blank canvas under a perfectly correct transport is the bug class unit tests miss, and it has happened here once. So there is a second suite that drives a real Chrome over CDP. It needs the dev server up: ```sh cd frontend && mise exec -- npx shadow-cljs watch app # in one shell cd frontend && mise exec -- npm run browser # in another ``` No dependencies. Playwright is not installed and CDP needs none — `node --experimental-websocket` has a global `WebSocket` and `--headless=new --remote-debugging-port=N` is the whole of the other side. It reads the canvas's own pixels rather than a screenshot, because the CSS scales the stage up by 2 and a screenshot is four pixels per raster pixel; it writes PNGs into `test/browser/out/` anyway, so "it drew something" can be checked by eye as well as by count. ## The app ```sh cd frontend && mise exec -- npx shadow-cljs watch app ``` Then open **** — with the `/index.html`, not bare `/`. This shadow-cljs does no directory-index resolution, so `/` is a 404 whatever the roots are. Four clips, on buttons in the transport: | | | | --- | --- | | `take` | the synthetic take, head **as filmed**. Step 5's deliverable: a moving mouth, frozen into dense channels, with no video file anywhere. | | `locked` | the same freeze, head **locked**. The same blocks — `:head`'s channels are written as framed identity instead of as a dense track, and nothing in tier 2 differs. | | `demo` | the hand-written scene from step 2. Not a face: the smallest scene that exercises every mechanism the model claims to have, so that each one is visible when it breaks. | | `swarm` | a hundred and twenty dense nodes. Not useful; it is the load test. | `take` and `locked` are the pair worth looking at together, because switching between them is the whole of what "stabilisation is a channel, not a mode" means. The demo scene itself is `src/arthur/demo/scene.edn`; the take is built in `src/arthur/demo/take.cljs`, which is also the only place the seven stages are composed in order. Port 8778 is deliberately not 8777. `python3 serve.py` from the repo root still runs the old JS tool on 8777, and the two are meant to run side by side. From step 9 Django serves the page and `:dev-http` goes away. ## The oracle, which is finished `js/` was the numeric oracle through step 4: `test/parity/` ran both implementations on the same synthetic track and diffed `fit-similarity`, `procrustes-mean`, the raster and `stabilize` to 1e-9. **It was deleted at step 5, on purpose.** Parity proves the port is FAITHFUL, not that the answer is RIGHT. The JS is a prototype and several of its conclusions contradict each other; a parity test pins behaviour while code moves, and keeping it afterwards would bake the prototype's mistakes into the rewrite and make them permanent. `docs/port-plan.md` says to delete it in one commit once the CLJS player renders the synthetic take, and that is what happened. `js/` itself stays. It is not an oracle any more, it is the SOURCE for steps 6 and 7 — the MediaPipe setup, the eye and brow signals, the interior extraction — and its comments encode bugs that actually happened. ## Layout ``` src/arthur/domain/ pure. No re-frame, no DOM, no flow/. src/arthur/flow/ the stages. `(f params inputs) -> output`, no state. src/arthur/synth.cljs the synthetic track. In src/ because the take PLAYS it — it stands in for flow/detect, and a tool that needs a video file before it shows you anything is one you cannot debug. src/arthur/demo.cljs the hand-written scene, read from demo/scene.edn src/arthur/demo/take.cljs the seven stages composed in order, and the only place they are src/arthur/ui/canvas.cljs the one imperative sink — the only DOM canvas call test/browser/ drives a real Chrome over CDP. Not run by `npm test`. public/index.html dev host page. Django replaces it at step 9. ``` ## Two evaluators, on purpose `domain/scene` has both `eval-frame` and `resolver`, and they are not alternatives: - **`(eval-frame scene f store)`** is the specification. Allocating, order-free, obviously correct. Tests and one-off renders use it. - **`(resolver scene store)` -> `(fn [f] ops)`** is what playback uses. It caches the topological order and the z paths, holds a cursor per channel and reuses one point buffer per node, so a frame allocates the op maps and nothing else. Both run the same walk, parameterised by how a channel is read and where its points are written — two independent implementations of frame evaluation would drift, and the drift would look like a rendering bug rather than like two functions disagreeing. What differs between them is exactly the part that can be wrong, and `scene-test` asserts they agree frame for frame in forward, backward and random order. Because the resolver reuses its buffers, **ops must be rasterised before the next frame is asked for.** That is the contract the rAF loop wants anyway: it reads, blits, and dispatches nothing.