# 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 && npm test ``` That is three things in order: regenerate the JS oracle's answers, compile the `:test` build, run it under node. ``` node test/parity/oracle.mjs # drives js/ and writes test/parity/oracle.json shadow-cljs compile test node out/node-tests.js ``` Run them separately if a compile error is in the way. The oracle JSON is generated, not committed. **Run them through `mise`**, or make sure `mise`'s node is first on PATH. The oracle imports `js/*.js` directly, and those are ES modules in a directory with no `package.json`, so node needs the module detection that became default in 20.19. On an older node 20 — an nvm install shadowing the pinned one is the easy way to get there — every import fails with "Named export not found ... is a CommonJS module", which reads like the oracle being broken and is not. ## The app ```sh cd frontend && 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. The page is the hand-written scene from step 2, scrubbed by hand. There is deliberately no clock: the audio clock, the rAF loop, the `::resolver` subscription and the transport are step 3, and the question this step answers is whether the data model evaluates correctly, not whether it evaluates at 30fps. A scrubber answers the first and nothing else, which is what makes a failure here unambiguous. The scene itself is `src/arthur/demo/scene.edn`, and it is not a face. It is the smallest scene that exercises every mechanism the model claims to have — inherited exposure, a sparse held `[:xform :pos]`, composition through a group, rotation about an anchor, a stencil chain, a keyed `[:vis]`, a `:span`, and fractional z among siblings — chosen so that each one is visible when it breaks. 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 — that is the whole reason `js/` is still in the tree. From step 9 Django serves the page and `:dev-http` goes away. ## The oracle `js/` is the numeric oracle, not dead weight. `test/parity/` runs both implementations on the same synthetic track and diffs them: `fit-similarity` and `procrustes-mean` agree to 1e-9, the raster pixel-for-pixel, and `stabilize`'s whole output agrees across the three namespaces it was split into. The oracle drives `stabilize` at aspect 0.5625 as well as at 1. Aspect 1 makes the anisotropy correction the identity, so a port that dropped it entirely would pass — which is the one thing a parity suite on normalised landmarks can be blind to. Both sides get the identical track because `js/synth.js` reads `Math.random` at call time, so `oracle.mjs` stubs it to a constant and the CLJS side passes `:rand-fn (constantly 0.5)`. `js/` itself is never modified. **`test/parity/` and `arthur.parity-test` get deleted in one commit at step 5.** A parity test pins behaviour while code moves; keeping it afterwards would bake the prototype's mistakes into the rewrite and make them permanent. ## 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/demo.cljs the hand-written scene, read from demo/scene.edn src/arthur/ui/canvas.cljs the one imperative sink — the only DOM canvas call test/arthur/synth.cljs the synthetic track — test infrastructure, not src test/parity/ the JS oracle harness. Deletable at step 5. 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.