Align project paths with timeline model

This commit is contained in:
Olive Vaughn 2026-09-28 02:48:57 -04:00
parent 9778b9023b
commit 7738c4e1c8
6 changed files with 56 additions and 55 deletions

View file

@ -19,13 +19,9 @@
clip/<cid>/timeline/<tid>/channel/<nid>/<prop>
clip/<cid>/timeline/<tid>/measured/<nid> the channels a re-freeze owns
WHY NODES SIT UNDER A TIMELINE. They did not until the clip and the timeline came
apart, and the flat `clip/<cid>/node/<nid>` was the persistence half of the same
conflation: it could only ever address the nodes of the one bag a clip had. A
library symbol is a timeline, an instance references one, and both need their
nodes addressed — so the timeline id is a segment, the root is `main`, and a
symbol's nodes are reachable by the same path shape as the clip's own. Adding it
later would have meant rewriting every stored path.
WHY NODES SIT UNDER A TIMELINE. A clip holds a library of timelines. Its root
and each symbol have their own nodes, so the timeline id is a path segment.
The root is `main`, and a symbol's nodes use the same path shape.
`:frames` MOVED OFF `timing` onto the timeline. A timeline is a frame space and a
clip is a rate, so `timing` holds `:fps` alone. Both used to be in one leaf, which

View file

@ -53,7 +53,8 @@
round-trip a clip through `JSON.parse(JSON.stringify(...))` and be running the
same conversion the network runs, rather than a CLJS-shaped rehearsal of it. The
one thing a keywordising `js->clj` would quietly break is the leaf paths —
`:clip/c1/node/mouth` is a keyword whose `name` is \"c1/node/mouth\", so the
`:clip/c1/timeline/main/node/mouth` is a keyword whose `name` is
\"c1/timeline/main/node/mouth\", so the
\"clip/\" would be lost on the way back in.
Refuses a document `domain/leaf` calls unaddressable, which is where a hand-made

View file

@ -4,7 +4,8 @@
Everything here returns a promise of a PARSED JS VALUE, not of CLJS data, and
that is deliberate: a leaf is transit, and `domain/project` reads it straight out
of the response object. A keywordising `js->clj` on the way past would turn the
leaf path \"clip/c1/node/mouth\" into a keyword whose name is \"c1/node/mouth\",
leaf path \"clip/c1/timeline/main/node/mouth\" into a keyword whose name is
\"c1/timeline/main/node/mouth\",
losing the prefix — a corruption that only shows up on the way back in.
CSRF IS NOT EXEMPTED. The page renders `{% csrf_token %}`, so Django sets its

View file

@ -1,5 +1,6 @@
(ns arthur.domain.feature-test
(:require [cljs.test :refer [deftest is]]
[arthur.domain.clip :as clip]
[arthur.domain.feature :as feature]
[arthur.domain.params :as params]))
@ -17,11 +18,14 @@
[id {:id id :kind :eye-pair
:subject (nth people i)
:members ids :params {}}])) members))
:nodes {}}))
:timelines {:main {:id :main :frames 1 :nodes {}}}}))
(defn- problems [c]
(feature/problems c (clip/nodes c)))
(deftest five-people-can-have-nine-identified-eyes
(let [scene (nine-eyes)]
(is (empty? (feature/problems scene)))
(is (empty? (problems scene)))
(is (= [:eye-8] (get-in scene [:groups :pair-4 :members])))
(is (= :person-4 (get-in scene [:features :eye-8 :subject])))))
@ -41,11 +45,11 @@
unpaired (feature/remove-from-pair scene :eye-0)]
(is (= before (feature/effective-params unpaired :eye-0)))
(is (= [:eye-1] (get-in unpaired [:groups :pair-0 :members])))
(is (empty? (feature/problems unpaired))))))
(is (empty? (problems unpaired))))))
(deftest a-pair-cannot-cross-subjects-or-own-an-eye-twice
(let [scene (nine-eyes)]
(is (seq (feature/problems
(is (seq (problems
(assoc-in scene [:groups :pair-0 :members] [:eye-0 :eye-2]))))
(is (seq (feature/problems
(is (seq (problems
(assoc-in scene [:groups :pair-1 :members] [:eye-0 :eye-3]))))))