arthur/docs/lane-handoff.md
Your Name 7a54bfca56 Write down what the next person needs
`docs/lane-handoff.md`, after `timing-handoff.md`'s shape, because the next
thread starts cold and the expensive part of that is not the code — it is the
decisions that were argued out and would otherwise be argued again.

So the section that matters most is the one listing what NOT to re-litigate:
the shot length is authored, placing ripples and overwrite is blank-then-place,
a position inside a cel refuses and names split, a correction has no time space
of its own, a layer's values are a channel, a conflict is not a problem, and a
command refuses rather than guesses. Each of those is a paragraph here and a
commit message in full.

Then the vocabulary, since it was settled one commit ago and the old words are
still in this repository's history: instance, cel, lane, drawing, and placement
for where a node sits only. With the warning that `exposure` still means the
`:expose` grid and always did.

Then the mechanisms that keep paying out — `:span` in the node's own frames
above all, which is why split, trim and blank cost almost nothing — the known
gaps, and how to run the suites, including that a release build clobbers the dev
bundle the browser tests need.

Recommended next piece is the cel sheet: it needs no new model, and it is the
first real evidence the document is not shaped by the timeline that grew up
with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:45:28 -04:00

11 KiB

Lane and cel handoff

Status (2026-09-30): the lane model is implemented through its commands. Cels are ordinary nodes with their own playback clock, a lane draws them as one row, the cel commands all exist, and correction layers evaluate and survive regeneration. What is missing is a second view and the commands that make a correction.

Seven commits on branch lanes, off 624242b. master is untouched and can be fast-forwarded. Each commit message is the argument for its change and is worth reading before touching what it did — they are the design record, more than this file is.

3d3c1bb  An occurrence is a node, with a clock of its own
9446829  Reuse, duplicate and make unique: deciding what is shared
26517af  A position is an argument, not another command
94c0a21  A correction is a layer, and a layer's values are a channel
72b57e3  Regenerate the base, keep the hand work, and say when you cannot
76106d3  The shot is as long as somebody said it was
598c186  One word for one thing: it is a cel

Read first, in this order

  1. The Lane Model — the design, and the status note under Proof obligations says what is built. It supersedes animation-model.md, timing-model.md and architecture.md wherever they overlap.
  2. frontend/src/arthur/domain/lane.cljs — every command, and the reasoning in its docstrings.
  3. frontend/test/arthur/domain/lane_test.cljs — what the model is asserted to do. It is the fastest way to see the shapes.
  4. frontend/src/arthur/domain/channel.cljs, the correction-layer section.

Vocabulary — one word for one thing

Renamed in 598c186, after four words had accumulated for one object. Use these and do not reintroduce the others.

word means
instance the :kind. The general thing, anywhere in a document
cel an instance in a lane. One drawing, held for some duration
lane a group with :layout :sequence
drawing the content a cel names — an ordinary symbol
placement ONLY where a node sits: nest/placement, and the transform that puts a face on the stage. Never the node itself

occurrence and exposure are not words for a cel. exposure means something else and still does: :time :expose is how many frames each step of a subtree lasts, which is what shooting on twos is — node/expose, clock/exposed-frame, subs/render ::exposure. Keeping these apart is why the block is called a cel.

:layout :sequence stays as the field, and is the one place two words are kept on purpose: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. node/lane? is where they meet.

The second view is the CEL SHEET, not the exposure sheet.

Decisions already made — do not re-litigate

These were each argued out and are load-bearing. Changing one is a design decision, not a cleanup.

  • The shot length is authored. :frames is the symbol's window; the occupied extent of its lanes is a different fact derived from the cels. A command grows the window only when the caller passes :extent :grow-symbol, and never shrinks it. Blanking the end of a shot leaves empty frames at the end, because deriving the window from the extent would make deleting the last drawing silently shorten the film. lane/finish.
  • Placing ripples; overwrite is blank then place. Overwrite is not a policy argument and should not become one until there is a reason blank cannot serve. An argument whose second value is unimplemented is worse than no argument.
  • A position inside a cel refuses and names split. One command must not quietly perform two. The UI offers the retry.
  • A correction has no time space of its own. Its :support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is in lane frames and reaches across the drawings under it; one on a cel travels with that cel. Ownership already answered it.
  • A layer's values are a channel. Constant, ramp and return motion are one mechanism. Do not add a second way to say what a value is over time.
  • A conflict is not a problem. A document whose topology outgrew a correction loads, evaluates and saves; clip/conflicts lists the decisions waiting for a person. problems means the document will not load.
  • Refuse rather than guess. Every command returns {:clip :selection} or {:refused why}, never a half-applied edit. Where the model needs a choice nobody has made, refusing and saying why is the behaviour, not a placeholder.
  • A cel is not a row. Rows, expansion and selection are editor state. The document has never known about rows and must not learn.

Next steps, in order

  1. The cel sheet. The doc's own test that the document is separate from its presentation: frames down, a column per lane, one cell per frame naming the drawing. It needs NO new model — symbol/lane-cels and the existing commands are the whole API. The obligation is that the same command issued from the sheet and from the timeline produces identical document changes and the same selection. This is the recommended next piece: it is self-contained, it is specified, and it is the first real evidence the model is not shaped by the timeline that grew up with it.
  2. The commands that make a correction — Constant adjustment, Ramp, Return motion over a selected range, per lane-model.md. The evaluator is done and has no opinion about how a range or a motion shape is chosen, which is now a view question. A panel also needs to offer clip/conflicts for resolution. Note the one open question: a correction needs a stable :id from somewhere, and cel ids come from the caller because this namespace is pure.
  3. Slip source and retime. Both have real design questions open and the doc says to refuse rather than approximate: retime needs a defined warp and interpolation behaviour, and is not moving keys whose numbers happen to fall inside a selection.
  4. Deleting reused content. Reference discovery exists (node/sources, clip/places, clip/contains-symbol?); the policy does not.
  5. Collaboration. lane-model.md is explicit that one leaf per channel does NOT solve two people editing different keys of the same channel. No conflict policy exists for that.

Mechanisms to reuse — these keep paying out

  • :span is in the node's OWN frames and :time says where they land in the lane. Moving an edge of a cel is therefore one write to :span, with :time and :playback untouched. This is why split costs nothing, why the two halves of a split go on meaning what the one cel meant, why trimming the front of a playing insert starts it later into its animation instead of restarting it, and why extending a hold leaves lane keys alone. lane/local and lane/edged are the whole geometry; trim, split and blank are all it.
  • lane/finish is the one commit path: it validates, applies the shot-length policy, and returns the refusal. New commands go through it.
  • :required-frames plus the retry event is the pattern for "this needs a decision you have not made": the domain reports what it would need, the UI offers one button. events/ui/lane-retry.
  • lane/lane-frame converts a symbol frame to a lane frame, or returns nil through a stepped or looping lane where there is no single answer. Nil refuses; it never snaps.
  • channel/conflict-with is the single rule for "can this layer apply to this base", used by the validator, by conflicts, and by the regeneration that marks them.
  • Two test patterns worth copying. the-cursor-agrees-with-the-specification-in-any-frame-order holds the optimized cursor to value-at in forward, backward and random order — add a case to it for any new channel shape. And drawn in lane_test samples every frame before and after an edit, which is how split and trim are proved to change nothing: state a claim as "the same picture" rather than as numbers computed by hand.

Known gaps and traps

  • Audio lanes do not work. symbol/lane-problems requires :instance children, so an audio node in a lane is rejected outright. lane-model.md says a lane may hold visual OR audio cels and should reject only a mixture.
  • :z is required on cels and means nothing there. A lane never has two cels on one frame, so draw order between them cannot matter. node/problems requires :z on every node uniformly, which is its own kind of simplicity — but the field is noise on a cel.
  • channel/offset-onto throws on a shape mismatch that no regeneration has recorded as a conflict. That is deliberate — a correction that silently does not take is the failure the design exists to prevent, and channel/problems catches the authored case — but it is a throw in the read path, so any new producer of layers must not create a mismatched one.
  • docs/timing-handoff.md is a separate, unreconciled thread. Performance- pose selection and plate drawings/tracing, instance-specific picture-rate requests, pose/put-cut addressing only :main. It predates the lane model and nobody has squared the two.
  • The button row in the timeline pane is a test harness, not a design. It is how the commands were made reachable and provable. lane-model.md describes the real cel action strip, the breadcrumb and the location bar; none exist.
  • shadow-cljs release app clobbers the dev bundle. Both builds write ../static/arthur/js, which Django serves, and the optimized build does not export the arthur global — so after a release the browser tests fail with ReferenceError: arthur is not defined. Run npx shadow-cljs compile app to restore it. A running watch app does not notice; it rebuilds on the next source change.

Running it

From frontend/:

npx shadow-cljs compile test && node out/node-tests.js     # 424 tests, 5,749 assertions
npx shadow-cljs compile app                                # the bundle Django serves
npx shadow-cljs release app                                # then `compile app` again — see above

The browser tests need the Django dev server up (mise exec -- python manage.py runserver 8778 from the repo root) and a compiled dev bundle:

node --experimental-websocket test/browser/lane.mjs                  # the lane/cel flow
CHROME=/usr/bin/chromium node --experimental-websocket test/browser/take.mjs

take.mjs defaults to a macOS Chrome path, hence CHROME=. It writes a real project to the local server by design; lane.mjs never writes to the server.

From the repo root: mise exec -- python manage.py test clips — 56 tests.

Documents are schema 3. A version 2 document is not read and nothing converts one; there is no backward compatibility to preserve anywhere in this work.