// Which scenes have migrated to the identity artifacts, and what each of the // rest needs. See MIGRATION.md for the recipe this reports progress against. // // Sixty-one scenes is too many to migrate from memory or from taste. The // classifier below reads each shader and proposes a TIER from evidence in the // source, so the work is a queue rather than a judgement call each time, and so // two passes over the library reach the same answer. // // It is deliberately conservative: it proposes, the gate disposes. A scene is // only migrated when `consumes` is declared AND checks.html?scene= shows // the matching `consumes:` line passing, which no amount of pattern matching // can fake. // // node tools/migration-status.js progress plus the next ten // node tools/migration-status.js --all every scene // node tools/migration-status.js --tier drawn import { scenes } from '../src/scenes/registry.js'; const has = (n) => process.argv.includes(`--${n}`); const argOf = (n) => { const i = process.argv.indexOf(`--${n}`); return i >= 0 ? process.argv[i + 1] : null; }; /** * Tiers, in migration order. The order is by payoff per unit of risk: scenes * that already loop over discrete elements are a near-mechanical change and are * where the cast actually shows, while a full-frame field has no elements to * replace and can only take the ink. */ const TIERS = { drawn: { consumes: ['cast', 'ink', 'staging'], why: 'loops over discrete elements — replace the primitive with the cast, ' + 'the placement with stageNode, the edge with inkMask', }, figure: { consumes: ['cast', 'ink'], why: 'draws one or a few forms without a lattice — take the cast and the ink, ' + 'keep its own composition', }, field: { consumes: ['ink'], why: 'a continuous surface with no elements — inkValue on the way out, ' + 'inkPattern where it already dithers or hatches', }, treatment: { consumes: ['ink'], why: 'an effect over an image rather than an image — takes the value ' + 'structure only, and should probably become an EFFECT rather than a scene', }, }; const RX = { migrated: /\b(castMain|castChorus|castSDF|inkMask|inkValue|stageNode)\s*\(/, // A loop whose body places something at a computed position: the signature // of an element-placing scene. elementLoop: /for\s*\(\s*int\s+\w+\s*=\s*0[\s\S]{0,900}?(length\s*\(\s*p\s*-|p\s*-\s*(pos|centre|center|c)\b|sigForm\s*\(|smoothstep\s*\([^)]*\bd\b)/, anyLoop: /for\s*\(\s*int\s+\w+\s*=\s*0/, // A single distance-field subject, no loop needed. figure: /\bsig(Shape|Form)\s*\(|\bsdf\w*\s*\(|length\s*\(\s*p\s*\)\s*-/, // Screen-space corruption: it operates on the frame, not on a world. treatment: /\bprev\s*\(|\bvUv\b[\s\S]{0,200}(tear|glitch|shift)|u_prev/, field: /\b(fbm|vnoise|curl|voronoi|noise)\s*\(/, }; function classify(module) { const src = module.shader || ''; if (RX.migrated.test(src) || (module.consumes || []).length) return 'migrated'; if (module.role === 'accent') return 'accent'; if (RX.elementLoop.test(src)) return 'drawn'; if (RX.treatment.test(src)) return 'treatment'; if (RX.figure.test(src) && !RX.anyLoop.test(src)) return 'figure'; if (RX.field.test(src)) return 'field'; return 'figure'; } const rows = scenes.map((m) => ({ name: m.name, family: m.family, role: m.role || 'stage', tier: classify(m), consumes: m.consumes || [], lines: (m.shader || '').split('\n').length, })); const migrated = rows.filter((r) => r.tier === 'migrated'); const pending = rows.filter((r) => r.tier !== 'migrated' && r.tier !== 'accent'); const accents = rows.filter((r) => r.tier === 'accent'); const bar = (v, w = 28) => '█'.repeat(Math.round(v * w)) + '·'.repeat(w - Math.round(v * w)); console.log(`\nMIGRATION STATUS — ${migrated.length}/${migrated.length + pending.length} scenes on the identity artifacts\n`); console.log(` ${bar(migrated.length / Math.max(1, migrated.length + pending.length))} ` + `${(migrated.length / Math.max(1, migrated.length + pending.length) * 100).toFixed(0)}%` + ` (${accents.length} accents excluded — they are depth passes, not subjects)\n`); console.log(' REMAINING BY TIER\n'); for (const [tier, spec] of Object.entries(TIERS)) { const group = pending.filter((r) => r.tier === tier); if (!group.length) continue; console.log(` ${tier} — ${group.length} scenes → consumes: [${spec.consumes.join(', ')}]`); console.log(` ${spec.why}`); console.log(` ${group.map((r) => r.name).join(', ')}`); console.log(''); } if (has('all') || argOf('tier')) { const want = argOf('tier'); console.log(' PER SCENE\n'); for (const r of rows.filter((x) => !want || x.tier === want)) { console.log(` ${r.name.padEnd(24)} ${r.family.padEnd(11)} ${r.tier.padEnd(10)}` + `${r.consumes.length ? `[${r.consumes.join(',')}]` : ''}`); } console.log(''); } else if (pending.length) { // The queue: drawn first, biggest payoff and least invention required. const order = ['drawn', 'figure', 'field', 'treatment']; const next = pending.slice().sort( (a, b) => order.indexOf(a.tier) - order.indexOf(b.tier) || a.lines - b.lines); console.log(' NEXT TEN\n'); for (const r of next.slice(0, 10)) { console.log(` ${r.name.padEnd(24)} ${r.tier.padEnd(10)} ${r.family.padEnd(11)} ${r.lines} shader lines`); } console.log(''); console.log(' Recipe: MIGRATION.md. Verify one with:'); console.log(` checks.html?scene=${encodeURIComponent(next[0].name)}`); console.log(' and look for a passing "consumes:" line for every artifact declared.\n'); }