Finish the migration: all 65 scenes on the identity artifacts

Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:

    sigForm(  -> castForm(     sigShape( -> castMain(     sigEdge( -> inkStroke(

Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.

Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.

Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.

Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.

Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-18 06:39:19 +02:00
co-authored by Claude Opus 5
parent e5bb7d77e0
commit 2d3ed8eb02
49 changed files with 234 additions and 123 deletions
+47 -21
View File
@@ -103,43 +103,69 @@ const CONTRACT_UNIFORMS = new Set([
* cast in the hexagon video and be the one shot that looks filmed elsewhere.
* So the claim is machine-checked against the source rather than trusted.
*/
// A stage that draws the song's CAST is expressing `shape` more completely than
// A scene that draws the song's CAST is expressing `shape` more completely than
// sigShape ever did — the form is the subject rather than a hint applied to one
// — so castMain/castChorus count as evidence. Likewise inkMask/inkValue are the
// style trait carried out in full. See scenes/stage/README.md.
// — so castMain/castChorus count as evidence.
//
// The INK does not work the same way and must not be listed under `style`. A
// trait is a property of the track that a scene may honour; an artifact is
// content it draws. Taking the ink says nothing about whether the scene
// responds to u_sigLine, and treating it as evidence let Eclipse Field claim a
// trait it had stopped honouring — which the runtime gate then caught.
// The same idea as TRAIT_EVIDENCE, for the identity artifacts: a scene that
// declares it consumes the cast has to actually draw it. Without this,
// `consumes` is a comment, and the migration becomes unverifiable the moment it
// is more than a handful of files.
const ARTIFACT_EVIDENCE = {
cast: /\bcast(Main|Chorus|SDF)\s*\(/,
ink: /\bink(Mask|Value|Pattern)\s*\(/,
cast: /\bcast(Main|Chorus|SDF|Form)\s*\(/,
ink: /\bink(Mask|Value|Pattern|Stroke)\s*\(/,
staging: /\b(stageNode|stageScale)\s*\(/,
};
const TRAIT_EVIDENCE = {
shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF))\s*\(/,
shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF|Form))\s*\(/,
camera: /\bsigCamera\s*\(/,
space: /\b(sigHorizonY|sigAir)\s*\(|\bu_sig(Horizon|Depth|Wash)\b/,
style: /\b(sigEdge|sigGrain|sigFolded|inkMask|inkValue|inkPattern)\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/,
style: /\b(sigEdge|sigGrain|sigFolded)\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/,
};
// The shader preamble is a JS template literal, so a backtick anywhere inside
// it silently closes the literal. Twice now that has produced a check page that
// hangs on "starting…" with an empty console, which is an expensive way to find
// a typo. One grep is cheaper.
console.log('\nshader contract');
// Every shader in this project lives inside a JS template literal, so a
// backtick anywhere in one silently closes it. Three times now that has cost a
// debugging round: twice in the preamble, where it produced a check page that
// hung on "starting…" with an empty console, and once in a scene, where at
// least the module failed to parse loudly. A GLSL comment is the natural place
// to reach for backticks when quoting a param name, which is exactly why this
// keeps happening.
//
// Checked across the contract AND every scene, since the scene case is the one
// a mechanical pass over sixty files will keep reintroducing.
console.log('\nshader literals');
{
const src = readFileSync(join(SRC, 'engine/shader-contract.js'), 'utf8');
const literal = src.slice(src.indexOf('export const PREAMBLE = `') + 25);
const body = literal.slice(0, literal.indexOf('\n`;'));
const stray = body.split('\n').filter((l) => l.includes('`') && !l.includes('${'));
if (stray.length) {
fail(`shader preamble contains a backtick, which closes the template literal:\n` +
stray.map((l) => ` ${l.trim()}`).join('\n'));
} else {
ok('preamble has no stray backticks');
const targets = [join(SRC, 'engine/shader-contract.js'), ...walk(join(SRC, 'scenes'))];
let clean = 0;
for (const file of targets) {
const src = readFileSync(file, 'utf8');
const rel = relative(SRC, file).replace(/\\/g, '/');
// Every template literal in the file, then the lines inside them that
// carry a backtick without being an interpolation.
const stray = [];
const rx = /`([\s\S]*?)`/g;
let m;
while ((m = rx.exec(src)) !== null) {
if (!/\bvec4 scene|precision highp|void main/.test(m[1])) continue;
for (const line of m[1].split('\n')) {
if (line.includes('`') && !line.includes('${')) stray.push(line);
}
}
// A shader body that swallowed a closing backtick shows up as an
// unbalanced count across the file.
const ticks = (src.match(/`/g) || []).length;
if (stray.length || ticks % 2 !== 0) {
fail(`${rel}: backtick inside a shader literal — it closes the string` +
(stray.length ? `:\n${stray.map((l) => ` ${l.trim()}`).join('\n')}` : ''));
} else clean++;
}
if (clean === targets.length) ok(`${clean} shader literals balanced, no stray backticks`);
}
console.log('\nscene schema lint');