# Migrating a scene onto the identity artifacts The recipe `tools/migration-status.js` reports progress against. Sixty-one scenes is too many to convert from taste, so this is written to be followed mechanically and to produce the same result twice. Background is EPIC-3.md. The one-line version: a scene draws its own content, a migrated scene draws the *song's* content — its cast, in its ink, on its lattice — and the measured payoff is additive with everything the scene already did. ```bash node tools/migration-status.js # the queue, and what each scene needs node tools/migration-status.js --all # every scene and its tier ``` --- ## The rule that decides everything > **An artifact is content a scene could not have invented for itself.** If the scene still looks right with the identity switched off, it has used the artifact as a modifier and the migration has not happened. That is not a style preference — it is the exact failure mode of `u_sigSides`, which thirty scenes declare and most quietly ignore, and it is why the gate below exists. --- ## The four tiers The classifier assigns these from the shader source. Check its answer, don't trust it: it reads patterns, and a scene that places elements in an unusual way will be misfiled. | tier | what it looks like | consumes | |---|---|---| | **drawn** | loops over discrete elements at computed positions | `cast, ink, staging` | | **figure** | one or a few forms, its own composition | `cast, ink` | | **field** | a continuous surface — noise, flow, terrain | `ink` | | **treatment** | an effect over an image rather than an image | `ink` | Accents are excluded. They are mostly-empty depth passes, not subjects. --- ## Recipe: `drawn` The highest payoff and the least invention. Four edits. **1. Declare it.** Add to the module, above `params`: ```js consumes: ['cast', 'ink', 'staging'], ``` **2. Replace the primitive with the cast.** Whatever the loop was drawing — a circle, a box, `sigShape`, a bespoke SDF — becomes one of: ```glsl float d = castMain(q) * size; // the protagonist: large, few float d = castChorus(q) * size; // the chorus: small, many ``` `q` is the element-local coordinate, `(p - pos) / size`. Multiplying the result back by `size` restores it to scene units, which is what the ink expects. Use `castMain` when the scene draws a handful of things and `castChorus` when it draws a crowd. A scene that draws both should use both — that is what the two members are for. **3. Replace the placement with the lattice.** ```glsl vec3 node = stageNode(fi, float(u_count)); // xy position, z scale multiplier vec2 pos = node.xy; float size = u_size * node.z; ``` Keep whatever the scene did that was *motion* — a drift, an orbit, a wander, a march. Give up what was *composition*. The split is the point: identity owns where things are, the scene owns what they do. A scene whose composition IS its identity — a spiral that must be a spiral — can keep it and take `stageScale()` alone, which is the song's element size. Declare `staging` either way. **4. Replace the edge with the ink.** ```glsl col = mix(col, pal(i + 1), inkMask(d, uv)); ``` `inkMask` does fill, fill treatment (hatch, stipple, halftone), outline and edge hardness in one call. Delete the scene's own `smoothstep(soft, -soft, d)` and its `sigEdge` — the ink supersedes both. Then wrap the return: ```glsl return vec4(inkValue(col), 1.0); ``` **Do not** delete `sigCamera`, `sigGrain`, `sigHorizonY` or `sigAir`. Traits and artifacts are different layers and both still apply. --- ## Recipe: `figure` Steps 1, 2 and 4, skipping the lattice. Declare `consumes: ['cast', 'ink']`. Take `stageScale()` if the figure has a size worth scaling and add `staging` if you do. --- ## Recipe: `field` and `treatment` There are no elements to replace, so this is one edit plus a judgement. ```js consumes: ['ink'], ``` ```glsl return vec4(inkValue(col), 1.0); ``` If the field already dithers, hatches or posterises internally, replace that with `inkPattern(uv)` so the treatment is the song's rather than the scene's. If it does not, `inkValue` alone is the whole migration — a value structure shared across every scene in a video is worth having and costs one line. Be honest about `treatment` scenes. Most of them are effects wearing a scene's clothes, and EPIC-3 §8 argues they should move into the identity's EFFECTS register rather than compete for screen time as subjects. Migrating one is a holding action, not the answer. --- ## Verifying — the part that makes this reproducible A migration is not done when the code looks right. It is done when the gate passes: ``` checks.html?scene= ``` Every artifact in `consumes` must show a passing line: ``` PASS consumes: cast delta 255/255 (floor 24) PASS consumes: ink delta 255/255 (floor 24) PASS consumes: staging delta 255/255 (floor 24) ``` That check renders the scene twice under two deliberately distant identities and requires the picture to change. A scene that declares `cast` and ignores it fails here, which is the only reason `consumes` can be trusted at library scale. `npm run lint:scenes` enforces the other half in both directions: declaring an artifact without calling it, and calling one without declaring it. The second matters more than it looks — an undeclared artifact hides the scene from this report and from anything that later selects scenes on capability. Then the usual battery still applies. `renders something`, `animates`, `deterministic`, `distinct`, `param sweep`, `flash rate` and every declared trait must all still pass. A migration that breaks `distinct` has made the scene into one of its neighbours, which is a real risk here: the more scenes share a cast, the more two weakly-composed ones converge. --- ## Measuring the payoff Per scene, the gate. Across the library, two numbers: ``` checks.html?decompose=1 identity against container, measured apart checks.html?experiment=1 stages against legacy, with error bars ``` `decompose` is the one to watch. Before the migration, over the four purpose- built stages: ``` identity only 0.0299 container only 0.0557 identity = 54% of container ``` After the first eighteen scenes, over the eight that consumed the cast: ``` identity only 0.1287 container only 0.0815 identity = 158% of container ``` Complete, over all thirty-two: ``` identity only 0.1054 container only 0.0901 identity = 117% of container ``` The number that matters in that row is `container only`, which went 0.0557 → 0.0815 → 0.0901 as the migration progressed. That is the stop condition below holding: the library did not homogenise. `identity only` is noisier than it looks because the scene it probes is picked from the migrated set and changes between runs — Constellation, then Metaballs, then Floating Geometry — so read it as "comparable to the container" rather than as a precise ratio. End to end, song variety across twelve songs before and after the epic: ``` floor observed separation before 0.1449 0.1508 not computable (the reference sat below the floor) after 0.1185 0.1376 0.38 ``` The floor fell and the gap widened, which is both halves of what the inversion was supposed to buy. Every structural block moved with it — scale 40→59%, orient 65→89%, layout 60→111%, texture 89→81%, motion 53→65% of reference. Coupling did not move: -0.09, still indistinguishable from zero. Whether a song LOOKS different in proportion to how it SOUNDS different remains unsolved, and nothing in the migration addressed it. Read that with its caveat: the stage set changed underneath the measurement, so part of the jump is that Metaballs expresses a cast more strongly than Constellation did rather than that the migration itself moved anything. What it does establish is the thing worth knowing — a migrated LIBRARY scene carries the identity better than a stage written from scratch to carry it. The bespoke stages were the wrong shape of effort. As scenes migrate, the `identity only` number should climb while `container only` holds — because the whole point of the correction in EPIC-3 §9b is that these add rather than trade. If `container only` falls as scenes migrate, the migration is homogenising the library and should stop. --- ## A caution learned the expensive way This harness's noise floor is large enough to invent findings. Three claims in this epic were made from single runs and withdrawn after repeats: that roster size was the dominant lever, that stages beat legacy by 14%, and that the identity's range was the bottleneck. Anything under about 0.01 of spread needs `&repeats=3` before it is believed, and a difference that changes sign with the sample size is not a difference. Migrate in batches, measure after each batch, and expect the per-batch effect to be inside the noise. The trend across batches is the signal.