music-video-gen/flow-state/MIGRATION.md
Dejvino e5bb7d77e0 A reproducible migration, and the first eighteen scenes through it
The library is sixty-one scenes, which is too many to convert from memory or
from taste, so the migration is a queue with a gate rather than a judgement call
per file.

Three pieces. A classifier reads each shader and assigns a tier from evidence in
the source — drawn, figure, field, treatment — so two passes over the library
reach the same answer and the work has an order. MIGRATION.md is the recipe per
tier, written to be followed mechanically. And a gate makes the result
verifiable: `consumes` is now a schema field, the lint enforces it in both
directions, and the per-scene battery renders each scene under two deliberately
distant identities and requires the picture to change.

That gate is the part that matters. Without it `consumes` is a comment, and the
whole inversion becomes unverifiable at exactly the point where it stops being
checkable by eye. With it, a scene that declares the cast and ignores it fails.

Eighteen scenes migrated. Four by hand at the drawn tier — Firefly Drift,
Metaballs, Floating Geometry, Prism Bloom — and ten at the field tier by script,
which is one declaration and one wrapped return. All eighteen pass.

The field tier is honestly marginal and the gate says so: every one of the ten
moves by 37 to 39 of 255, against 173 to 255 for the drawn tier, and the
uniformity across ten unrelated scenes is the tell. That is one global
posterisation applying, not ten scenes expressing anything. Cheap, real, shallow.

The decomposition moved from identity being worth 54% of the container to 158%,
but the stage set changed underneath the measurement and part of that is
Metaballs expressing a cast better than Constellation did. What survives the
caveat is the useful finding: a migrated library scene carries the identity
better than a stage written from scratch to carry it. The four bespoke stages
were the wrong shape of effort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:59:24 +02:00

7.6 KiB

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.

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:

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:

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.

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.

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:

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.

consumes: ['ink'],
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=<Scene%20Name>

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 consume the cast:

identity only    0.1287      container only   0.0815      identity = 158% of container

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.