arthur/frontend
Olive Vaughn eb06be005c Port steps 0-1: scaffold, the oracle, and the pure bottom
Scaffolds frontend/ (shadow-cljs, reagent 1.2.0, re-frame 1.4.3) and ports
everything below the data model, with the JS kept as a numeric oracle.

  domain/landmarks  index tables, verbatim
  domain/ring       subsample, offset, simplicity
  domain/geom       similarity fit, procrustes, moving average
  domain/raster     indexed scanline fill, stencil, disc, rect
  domain/palette    the ramp, and the no-sampled-RGB rule

58 tests, 166 assertions. Parity with js/ on the identical 72-frame synthetic
track: fit-similarity, procrustes-mean, fit-residual, moving-average,
smooth-transforms, offset-ring and subsample-slots to 1e-9; the raster
pixel-for-pixel over the whole buffer.

Three deviations from the JS, each for a reason:

- synth.cljs jitters from a SEEDED generator, not Math.random. Parity is only
  checkable if both sides can be handed the same track, and a failing assertion
  has to be reproducible. `:rand-fn` takes the generator over, so oracle.mjs
  stubs js/Math.random and js/ itself stays untouched.

- raster/->rgba replaces toImageData. ImageData is a DOM type and domain/ may
  not touch the DOM; returning plain bytes also lets the
  no-intermediate-colours assertion run in node. ui/canvas wraps it later.

- offset-ring lives in domain/ring, not domain/geom, per architecture.md: it is
  an operation on an ordered traversal, not on a transform.

Step 1's "done" also names the swapped-iris vote, but pairIrises is in
pipeline.js and belongs to step 7. The precondition is asserted instead --
`:swap-iris` really does move both blocks -- so the vote will have a track that
disagrees with it when it arrives.

One finding, recorded in full in the test that measures it: smooth-transforms
buys nothing on the synthetic track. Against jitter-free ground truth, radius 1
helps by 17% on one noise realisation and hurts by 0.5% on another, so its
benefit is within noise; from radius 2 up the cost is unambiguous, and by radius
5 the filter is below the true motion's own high-frequency energy, i.e.
smoothing away performance. The test pins the shape of the knob rather than a
preferred value. This may say more about the synth's jitter being unrealistically
small (+/-0.001 normalised) than about the knob; step 6 settles it on real
footage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 14:43:34 -04:00
..
public Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
src/arthur Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
test Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
package-lock.json Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
package.json Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
README.md Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00
shadow-cljs.edn Port steps 0-1: scaffold, the oracle, and the pure bottom 2026-09-27 14:43:34 -04:00

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

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

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.

The app

cd frontend && npx shadow-cljs watch app

Then open http://localhost:8778/index.html — with the /index.html, not bare /. This shadow-cljs does no directory-index resolution, so / is a 404 whatever the roots are.

Through step 1 the page is a placeholder on purpose: there is nothing to render until the data model exists, and a shell built before the model is a shell built around a guess. Something moves on screen at step 2.

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.

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/.
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.