Symbols, not timelines; no symbol is special

Everything that holds nodes is a symbol (domain/timeline -> domain/symbol,
:timelines -> :symbols) and a node that places one is :kind :instance. The
reserved :main root is gone: which symbol is on screen is editor state
([:ui :open]), every domain function that needs a symbol is told which, and
a document opens on the longest symbol nothing else places.

Saved projects move to schema 2 through migration 0007, which rewrites leaf
paths, instance kinds and the feature :symbol key; the client refuses a
schema it does not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-29 12:46:42 -04:00
parent 179770d7d4
commit 5dff490162
61 changed files with 1587 additions and 1431 deletions

View file

@ -382,13 +382,13 @@ class DocumentTests(TestCase):
# cache marker, keyword keys, and a frame-keyed inner map.
return {
"clip/c1/timing": ["^ ", "~:fps", 30],
"clip/c1/timeline/main": ["^ ", "~:frames", 48],
"clip/c1/timeline/main/node/mouth": ["^ ", "~:id", "~:mouth", "~:z", "a1"],
"clip/c1/timeline/main/channel/mouth/geom.pts": [
"clip/c1/symbol/main": ["^ ", "~:frames", 48],
"clip/c1/symbol/main/node/mouth": ["^ ", "~:id", "~:mouth", "~:z", "a1"],
"clip/c1/symbol/main/channel/mouth/geom.pts": [
"^ ", "~:animated?", True, "~:dense",
["^ ", "~:store", self.block, "~:offset", 0, "~:stride", 16],
],
"clip/c1/timeline/main/channel/mouth-in/vis": [
"clip/c1/symbol/main/channel/mouth-in/vis": [
"^ ", "~:animated?", True, "~:keys", ["^ ", "~i0", True, "~i12", False],
],
}
@ -407,7 +407,7 @@ class DocumentTests(TestCase):
self.assertEqual(5, len(response.json()["written"]))
loaded = self.client.get(f"/api/projects/{self.project.id}").json()
self.assertEqual(1, loaded["schema_version"])
self.assertEqual(2, loaded["schema_version"])
self.assertEqual(1, len(loaded["clips"]))
clip = loaded["clips"][0]
self.assertEqual("c1", clip["cid"])
@ -424,22 +424,22 @@ class DocumentTests(TestCase):
self.save()
first = {leaf.path: leaf.version for leaf in Leaf.objects.all()}
moved = self.leaves()
moved["clip/c1/timeline/main/channel/mouth-in/vis"] = [
moved["clip/c1/symbol/main/channel/mouth-in/vis"] = [
"^ ", "~:animated?", True, "~:keys", ["^ ", "~i0", False],
]
response = self.save(moved)
self.assertEqual(["clip/c1/timeline/main/channel/mouth-in/vis"], response.json()["written"])
self.assertEqual(["clip/c1/symbol/main/channel/mouth-in/vis"], response.json()["written"])
self.assertEqual(4, response.json()["unchanged"])
after = {leaf.path: leaf.version for leaf in Leaf.objects.all()}
self.assertEqual(2, after["clip/c1/timeline/main/channel/mouth-in/vis"])
self.assertEqual(2, after["clip/c1/symbol/main/channel/mouth-in/vis"])
self.assertEqual(first["clip/c1/timing"], after["clip/c1/timing"])
def test_a_removed_node_removes_its_leaf(self):
self.save()
fewer = {k: v for k, v in self.leaves().items()
if k != "clip/c1/timeline/main/node/mouth"}
if k != "clip/c1/symbol/main/node/mouth"}
response = self.save(fewer)
self.assertEqual(["clip/c1/timeline/main/node/mouth"], response.json()["removed"])
self.assertEqual(["clip/c1/symbol/main/node/mouth"], response.json()["removed"])
self.assertEqual(4, Leaf.objects.count())
def test_a_save_does_not_disturb_another_clip(self):
@ -472,7 +472,7 @@ class DocumentTests(TestCase):
def test_a_leaf_write_carries_an_etag(self):
self.save()
url = f"/api/projects/{self.project.id}/leaves/clip/c1/timeline/main/node/mouth"
url = f"/api/projects/{self.project.id}/leaves/clip/c1/symbol/main/node/mouth"
got = self.client.get(url)
self.assertEqual('"1"', got["ETag"])
@ -488,7 +488,7 @@ class DocumentTests(TestCase):
# take-theirs. A PUT that replaced unconditionally is the bug where the
# loser's work disappears silently.
self.save()
url = f"/api/projects/{self.project.id}/leaves/clip/c1/timeline/main/node/mouth"
url = f"/api/projects/{self.project.id}/leaves/clip/c1/symbol/main/node/mouth"
self.put(url, {"value": ["^ ", "~:z", "a2"]}, HTTP_IF_MATCH='"1"')
stale = self.put(url, {"value": ["^ ", "~:z", "a3"]}, HTTP_IF_MATCH='"1"')
self.assertEqual(409, stale.status_code)