Two devices that were built, wired up, and doing nothing. OVERLAYS. buildStack gated layering on `surfaceOf(m) === 'composable'`, and no scene in the library declared `surface` — so the only thing that could ever sit on top was the one scene declaring `role: 'accent'`, and the overlay roster excluded accents by construction. Empty intersection, every time: 0/144 stacks carried an overlay. Every layered frame in every song was the same particle field. Labelled the library from the phase 12 coverage gate rather than by eye: the 37 scenes painting under 30% of the frame are composable. Particle Field loses its privileged slot and becomes one of them, keeping only `background: false`, which is the honest part — points in empty space cannot carry a section alone. The reserved accent slot is gone; one roster, two passes at it. 1 distinct overlay scene becomes 27, and the rate lands at 34% of stacks after trimming a base chance that had been tuned while the branch was dead. THE CAMERA. framing.shift moved the frame by a median of 0.029 of a half-frame at a fresh random angle every shot, so successive offsets cancelled and the median jump at a cut was 0.014. Present in every frame, visible in none. look/Camera.js is the director's camera department: the story says tension, order and which act a section is in, and this turns that into where the frame looks and how it travels there. Each director names a camera. Jump distance follows tension and act, speed follows energy, and the curve is one of four. Cuts choose between reframing and matching, so two scenes can still read as one place. Median offset 0.190, median reframe 0.135, and 94% of shots now move during the shot rather than only at the cut. Four new gates, each with a FLOOR — the device this replaces passed every existing check while doing nothing, because the only bound was a ceiling. Also lands the in-progress Epic 4 story layer it builds on: Story.js, phase 13, and the direction statistic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
430 lines
22 KiB
JavaScript
430 lines
22 KiB
JavaScript
// The variety report as text.
|
|
//
|
|
// Kept apart from the measurement so the gate and the diagnostic read the same
|
|
// numbers. The layout is chosen so the first three lines answer "is this bad",
|
|
// and everything below answers "where did the variety go" — which is the only
|
|
// question worth printing a table for.
|
|
|
|
import { song } from '../../audio/songbank.js';
|
|
import {
|
|
measureVariety, measureSpecDiversity, librarySweep,
|
|
measureSongVariety, measureFingerprint, measureDecomposition,
|
|
} from './report.js';
|
|
import { generateLook, describeLook } from '../../look/LookGenerator.js';
|
|
|
|
const bar = (v, width = 24) => {
|
|
const n = Math.max(0, Math.min(width, Math.round(v * width)));
|
|
return '█'.repeat(n) + '·'.repeat(width - n);
|
|
};
|
|
|
|
const pct = (v) => `${(v * 100).toFixed(0)}%`.padStart(4);
|
|
|
|
export async function varietyReportLines({ seeds = 8, probes = 5, library = true } = {}) {
|
|
// A real bank entry, not the two-section synthetic this used to run on.
|
|
// That one segmented into intro and outro only, and quiet kinds are
|
|
// restricted to the restful families for every director — a third of the
|
|
// library was unreachable and the score was measuring that, not the seed.
|
|
const track = song('centre').track;
|
|
|
|
const lines = [];
|
|
const spec = measureSpecDiversity(track, { seeds: Math.max(seeds, 24) });
|
|
|
|
// Yield so the "measuring…" line paints before the GPU work blocks.
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const r = measureVariety(track, { seeds, refScenes: 5, probes });
|
|
|
|
const ok = r.ceilingValid && r.separation >= 0.35;
|
|
const headline = r.ceilingValid
|
|
? `seed separation ${r.separation.toFixed(2)} (${ok ? 'acceptable' : 'TOO LOW'}) · ` +
|
|
`${pct(r.identity)} of what the library can express`
|
|
: `observed ${r.observed.toFixed(3)} vs floor ${r.floor.toFixed(3)} · ` +
|
|
'reference below the floor, separation not computable';
|
|
|
|
lines.push('SEED VARIETY — one track, one analysis, only the seed changes');
|
|
lines.push('');
|
|
lines.push(` floor ${r.floor.toFixed(4)} one video against itself, across its own sections`);
|
|
lines.push(` observed ${r.observed.toFixed(4)} two seeds against each other`);
|
|
lines.push(` ceiling ${r.ceiling.toFixed(4)} same pipeline, casts that share no scenes at all`);
|
|
lines.push('');
|
|
lines.push(r.ceilingValid
|
|
? ` separation ${bar(r.separation)} ${r.separation.toFixed(2)}`
|
|
: ' separation — not computable: the reference landed below the floor,\n' +
|
|
' which means real seeds already differ by more than videos\n' +
|
|
' built from casts that share no scenes.');
|
|
lines.push(' 0 = the seed changes nothing a viewer could name');
|
|
lines.push(' 1 = two seeds as unalike as two randomly assembled videos');
|
|
lines.push('');
|
|
lines.push('DIRECTION — of the floor above, how much is the video GOING somewhere');
|
|
lines.push('');
|
|
lines.push(` direction ${bar((r.direction + 1) / 2)} ${r.direction.toFixed(2)}` +
|
|
` (arcless reference ${r.directionFloor.toFixed(2)})`);
|
|
lines.push(' rank correlation between how far apart two probes are in');
|
|
lines.push(' time and how far apart they look. A video with an arc is');
|
|
lines.push(' most unlike itself at its two ends; one that rotates a');
|
|
lines.push(' roster is as unlike itself after ten seconds as after four');
|
|
lines.push(' minutes. Both score the same floor. Read them together:');
|
|
lines.push(' a floor that rises WITH direction is a story, and a floor');
|
|
lines.push(' that rises without one is a shuffle.');
|
|
lines.push('');
|
|
lines.push('WHERE THE VARIETY IS — per structural block, as a fraction of achievable');
|
|
lines.push('');
|
|
for (const [name, b] of Object.entries(r.byBlock)) {
|
|
const note = name === 'colour' ? ' (not counted — this is the axis that lies)' : '';
|
|
lines.push(` ${name.padEnd(8)} ${bar(b.ratio)} ${pct(b.ratio)}` +
|
|
` ${b.between.toFixed(3)} of ${b.ceiling.toFixed(3)}${note}`);
|
|
}
|
|
lines.push('');
|
|
lines.push(' scale feature size — fine texture vs large soft forms');
|
|
lines.push(' orient grid vs radial vs stripes, measured rotation-blind');
|
|
lines.push(' layout where in the frame the structure sits');
|
|
lines.push(' texture element count, sparsity, mirror and radial symmetry');
|
|
lines.push(' motion what moves and where, not how much');
|
|
lines.push('');
|
|
|
|
lines.push('CLOSEST SIBLING PER SEED — a seed below the floor is a duplicate video');
|
|
lines.push('');
|
|
r.nearest.forEach((d, i) => {
|
|
const flag = d < r.floor * 0.75 ? ' ← shadowed' : d < r.floor ? ' ← thin' : '';
|
|
lines.push(` seed ${r.seeds[i].toString(16).padStart(8, '0')} ${bar(d / Math.max(r.ceiling, 1e-6))} ${d.toFixed(3)}${flag}`);
|
|
});
|
|
if (r.worstPair) {
|
|
const w = r.worstPair;
|
|
const blocks = Object.entries(w.byBlock)
|
|
.map(([n, v]) => `${n} ${v.toFixed(3)}`).join(' · ');
|
|
lines.push('');
|
|
lines.push(` closest pair: seeds ${w.a} and ${w.b} at ${w.distance.toFixed(3)} — ${blocks}`);
|
|
}
|
|
lines.push('');
|
|
|
|
lines.push('THE DECISIONS BEHIND IT — spec-level, no rendering');
|
|
lines.push(' (high here + low above = the generator decides freely and the render flattens it;');
|
|
lines.push(' low here = casting is the bottleneck, and shader work will not fix it)');
|
|
lines.push('');
|
|
lines.push(` scene-set distance ${bar(spec.sceneSetDistance)} ${spec.sceneSetDistance.toFixed(2)} how differently ${spec.seeds} seeds cast`);
|
|
lines.push(` library coverage ${bar(spec.libraryCoverage)} ${pct(spec.libraryCoverage)} of castable scenes ever chosen`);
|
|
lines.push(` identical casts ${spec.identicalCasts} seed pairs`);
|
|
for (const key of ['director', 'paletteScheme', 'signature', 'grain', 'framing', 'paletteArc', 'anchorScenes']) {
|
|
const e = spec[key];
|
|
lines.push(` ${key.padEnd(20)} ${bar(e.normalized)} ${String(e.unique).padStart(3)} distinct (entropy ${e.entropy.toFixed(2)} bits)`);
|
|
}
|
|
lines.push('');
|
|
|
|
if (library) {
|
|
// The map the whole test is drawn on: a seed cannot reach more variety
|
|
// than the library holds, so a low separation is only the generator's
|
|
// fault once the library is known to hold distinct looks.
|
|
const sweep = librarySweep(track, { probes: 3 });
|
|
lines.push('THE LIBRARY — all ' + sweep.scenes.length + ' visualizations, every pair, colour not counted');
|
|
lines.push('');
|
|
lines.push(` median pair distance ${sweep.median.toFixed(3)} ` +
|
|
`(seed test ceiling for reference: ${r.ceiling.toFixed(3)})`);
|
|
lines.push(` closest 5% under ${sweep.p05.toFixed(3)}`);
|
|
lines.push('');
|
|
lines.push(` structural twins — every pair inside a group is under ${sweep.twinAt.toFixed(3)}:`);
|
|
if (sweep.twins.length === 0) {
|
|
lines.push(' no group of three or more; see the closest pairs below');
|
|
} else {
|
|
for (const group of sweep.twins) lines.push(` ${group.join(' ≈ ')}`);
|
|
}
|
|
lines.push('');
|
|
lines.push(' closest pairs in the library:');
|
|
for (const p of sweep.closestPairs) {
|
|
lines.push(` ${p.distance.toFixed(3)} ${p.name.padEnd(22)} ≈ ${p.nearest} (${p.family})`);
|
|
}
|
|
lines.push('');
|
|
if (spec.uncast.length) {
|
|
lines.push(` never cast in ${spec.seeds} seeds: ${spec.uncast.join(', ')}`);
|
|
lines.push('');
|
|
}
|
|
}
|
|
|
|
lines.push('SAMPLE LOOKS');
|
|
lines.push('');
|
|
for (const seed of r.seeds.slice(0, 6)) {
|
|
lines.push(` ${describeLook(generateLook(track, { seed }))}`);
|
|
}
|
|
|
|
return { lines, ok, headline };
|
|
}
|
|
|
|
|
|
/**
|
|
* The SONG variety report.
|
|
*
|
|
* Deliberately laid out in the order the questions have to be answered. Are two
|
|
* songs different at all; is that difference DERIVED from the music or merely
|
|
* random; and what does every output have in common regardless — which is the
|
|
* one that speaks to "you can tell what made it".
|
|
*/
|
|
export async function songVarietyReportLines({ songs = 6, probes = 5 } = {}) {
|
|
const lines = [];
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const r = measureSongVariety({ songs, probes });
|
|
const f = measureFingerprint({ songs: Math.min(songs, 5), probes: 3 });
|
|
|
|
const ok = r.ceilingValid && r.separation >= 0.45 && r.coupling >= 0.3;
|
|
const headline = (r.ceilingValid ? `song separation ${r.separation.toFixed(2)}` :
|
|
`observed ${r.observed.toFixed(3)} vs floor ${r.floor.toFixed(3)} (no valid ceiling)`) +
|
|
` · coupling ${r.coupling.toFixed(2)} (${ok ? 'acceptable' : 'TOO LOW'})`;
|
|
|
|
lines.push('SONG VARIETY — different songs, each with its own audio-derived seed');
|
|
lines.push('');
|
|
for (const b of r.bank) lines.push(` ${b.name.padEnd(9)} ${b.kinds.join(' ')}`);
|
|
lines.push('');
|
|
lines.push(` floor ${r.floor.toFixed(4)} one video against itself, across its own sections`);
|
|
lines.push(` observed ${r.observed.toFixed(4)} two songs against each other`);
|
|
lines.push(` ceiling ${r.ceiling.toFixed(4)} same pipeline, casts that share no scenes at all`);
|
|
lines.push('');
|
|
lines.push(r.ceilingValid
|
|
? ` separation ${bar(r.separation)} ${r.separation.toFixed(2)}`
|
|
: ' separation — not computable: reference below the floor');
|
|
lines.push(` coupling ${bar(Math.max(0, r.coupling))} ${r.coupling.toFixed(2)}`);
|
|
lines.push(' coupling is how strongly musical distance predicts visual distance.');
|
|
lines.push(' near 0 means the look is unrelated to the song — separation without');
|
|
lines.push(' it is not variety, it is noise with a different seed per file.');
|
|
lines.push('');
|
|
lines.push('WHERE THE VARIETY IS');
|
|
lines.push('');
|
|
for (const [name, b] of Object.entries(r.byBlock)) {
|
|
const note = name === 'colour' ? ' (not counted)' : '';
|
|
lines.push(` ${name.padEnd(8)} ${bar(b.ratio)} ${pct(b.ratio)}` +
|
|
` ${b.between.toFixed(3)} of ${b.ceiling.toFixed(3)}${note}`);
|
|
}
|
|
lines.push('');
|
|
lines.push('CLOSEST SONG PAIRS — two songs that came out as one video');
|
|
lines.push('');
|
|
for (const p of r.pairs.slice(0, 6)) {
|
|
lines.push(` ${p.total.toFixed(3)} ${p.a.padEnd(9)} ≈ ${p.b.padEnd(9)}` +
|
|
` (they sound ${p.musical < 0.25 ? 'alike' : p.musical > 0.5 ? 'nothing alike' : 'somewhat alike'}` +
|
|
`, musical distance ${p.musical.toFixed(2)})`);
|
|
}
|
|
lines.push('');
|
|
lines.push('THE HOUSE FINGERPRINT — what every output has in common');
|
|
lines.push(' (variance across our videos as a fraction of variance across random ones;');
|
|
lines.push(' a low number is a constant the generator imposes on everything it makes)');
|
|
lines.push('');
|
|
for (const t of f.tells) {
|
|
lines.push(` ${t.block.padEnd(8)} ${bar(Math.min(1, t.ratio))} ${pct(Math.min(1, t.ratio))}` +
|
|
` ${t.frozen}/${t.dims} dimensions effectively frozen`);
|
|
}
|
|
lines.push('');
|
|
|
|
return { lines, ok, headline };
|
|
}
|
|
|
|
/**
|
|
* The Epic 3 experiment: does a video built from STAGES beat one built from
|
|
* comparable legacy scenes?
|
|
*
|
|
* Same songs, same instrument, same number of scenes available — the only
|
|
* difference is whether those scenes draw the song's cast in the song's ink, or
|
|
* their own content. Three arms, because two would not distinguish "stages are
|
|
* better" from "a small pool is better".
|
|
*/
|
|
export async function experimentReportLines({ songs = 6, probes = 4, repeats = 3 } = {}) {
|
|
const { scenes } = await import('../../scenes/registry.js');
|
|
const byName = (n) => scenes.find((m) => m.name === n);
|
|
|
|
const arms = [
|
|
{
|
|
label: 'STAGES — draw the song\'s cast and ink',
|
|
pool: ['Procession', 'Constellation', 'Soloist', 'Swarm'].map(byName),
|
|
},
|
|
{
|
|
label: 'LEGACY — four comparable element-placing scenes',
|
|
pool: ['Floating Geometry', 'Firefly Drift', 'Scale Mosaic', 'Metaballs'].map(byName),
|
|
},
|
|
{
|
|
label: 'FULL — the unrestricted generator, for reference',
|
|
pool: null,
|
|
},
|
|
];
|
|
|
|
const lines = [];
|
|
lines.push('EPIC 3 EXPERIMENT — container vs content');
|
|
lines.push('');
|
|
lines.push(' Same songs, same instrument, same pool size. The stages own arrangement');
|
|
lines.push(' and nothing else: what is on screen comes from the song. The legacy arm');
|
|
lines.push(' is four scenes that each invent their own content.');
|
|
lines.push('');
|
|
lines.push(' Lower floor = a video that looks like itself over its own length.');
|
|
lines.push(' Higher observed = two songs that look like different work.');
|
|
lines.push('');
|
|
|
|
// Repeats, with error bars. The first run of this comparison used seven
|
|
// songs and put stages 14% ahead; at twelve songs the ordering flipped. A
|
|
// difference that changes sign with the sample is a difference that has to
|
|
// be reported with its spread or not at all.
|
|
const results = [];
|
|
for (const arm of arms) {
|
|
const runs = [];
|
|
for (let k = 0; k < repeats; k++) {
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
runs.push(measureSongVariety({
|
|
songs, probes, pool: arm.pool, seedSalt: k * 7919,
|
|
}));
|
|
}
|
|
results.push({ arm, runs, r: runs[0] });
|
|
}
|
|
|
|
const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length;
|
|
const half = (a) => (Math.max(...a) - Math.min(...a)) / 2;
|
|
|
|
lines.push(` arm floor observed spread (${repeats} runs)`);
|
|
lines.push(' ' + '-'.repeat(80));
|
|
for (const { arm, runs } of results) {
|
|
const spreads = runs.map((x) => x.observed - x.floor);
|
|
lines.push(` ${arm.label.padEnd(44)}${mean(runs.map((x) => x.floor)).toFixed(4)}` +
|
|
` ${mean(runs.map((x) => x.observed)).toFixed(4)}` +
|
|
` ${mean(spreads) >= 0 ? '+' : ''}${mean(spreads).toFixed(4)} ±${half(spreads).toFixed(4)}`);
|
|
}
|
|
lines.push('');
|
|
|
|
const gap = mean(results[0].runs.map((x) => x.observed - x.floor))
|
|
- mean(results[1].runs.map((x) => x.observed - x.floor));
|
|
const noise = Math.max(
|
|
half(results[0].runs.map((x) => x.observed - x.floor)),
|
|
half(results[1].runs.map((x) => x.observed - x.floor)));
|
|
lines.push(` stages minus legacy: ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} against a noise band of ±${noise.toFixed(4)}`);
|
|
lines.push(` → ${Math.abs(gap) > noise * 2 ? 'a real difference' : 'INDISTINGUISHABLE at this sample size'}`);
|
|
lines.push('');
|
|
|
|
for (const { arm, runs, r } of results) {
|
|
lines.push(` ${arm.label}`);
|
|
for (const [name] of Object.entries(r.byBlock)) {
|
|
const v = runs.map((x) => x.byBlock[name].between);
|
|
lines.push(` ${name.padEnd(8)} between ${mean(v).toFixed(3)} ±${half(v).toFixed(3)}`);
|
|
}
|
|
const c = runs.map((x) => x.coupling);
|
|
lines.push(` coupling ${mean(c).toFixed(2)} ±${half(c).toFixed(2)}`);
|
|
lines.push('');
|
|
}
|
|
|
|
const ok = gap > noise * 2;
|
|
const headline = `stages ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} vs legacy, noise ±${noise.toFixed(4)} — ` +
|
|
(Math.abs(gap) > noise * 2 ? (gap > 0 ? 'the inversion helps' : 'the inversion hurts')
|
|
: 'indistinguishable');
|
|
return { lines, ok, headline };
|
|
}
|
|
|
|
/**
|
|
* How big should a track's casting pool be?
|
|
*
|
|
* The Epic 3 experiment found the roster size, not the content sharing, was
|
|
* carrying most of the improvement — so the number deserves to be measured
|
|
* rather than picked. Reported as spread, because both ends of it matter: a
|
|
* one-scene pool would score perfectly on the floor and be unwatchable.
|
|
*/
|
|
export async function poolSweepLines({ songs = 6, probes = 4, sizes = null, repeats = 1 } = {}) {
|
|
const list = sizes || [3, 4, 6, 8, 12, 18, 24, 40];
|
|
const lines = [];
|
|
lines.push('CASTING POOL SWEEP — how many scenes one track may draw on');
|
|
lines.push('');
|
|
lines.push(' floor one video against itself, across its own sections');
|
|
lines.push(' observed two songs against each other');
|
|
lines.push(' spread the gap between them — the thing worth maximising');
|
|
lines.push('');
|
|
lines.push(' pool floor observed spread ratio distinct scenes/video');
|
|
lines.push(' ' + '-'.repeat(66));
|
|
|
|
const { generateLook } = await import('../../look/LookGenerator.js');
|
|
const { song } = await import('../../audio/songbank.js');
|
|
const track = song('centre').track;
|
|
|
|
// Repeats exist because the first run of this sweep was pure noise: spread
|
|
// was non-monotonic in pool size and peaked at the LARGEST pool, which is
|
|
// the opposite of what the Epic 3 arms suggested. Each size draws a
|
|
// different random pool per song, so a single run measures which scenes
|
|
// happened to come up as much as it measures the size. Repeating with a
|
|
// different draw and reporting the range is how to tell those apart.
|
|
let best = null;
|
|
for (const size of list) {
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const runs = [];
|
|
for (let k = 0; k < repeats; k++) {
|
|
const rk = measureSongVariety({
|
|
songs, probes, poolSize: size, seedSalt: k * 7919,
|
|
});
|
|
runs.push(rk);
|
|
}
|
|
const r = runs[0];
|
|
const spreads = runs.map((x) => x.observed - x.floor);
|
|
const spread = spreads.reduce((a, b) => a + b, 0) / spreads.length;
|
|
const range = repeats > 1
|
|
? ` ±${((Math.max(...spreads) - Math.min(...spreads)) / 2).toFixed(4)}` : '';
|
|
|
|
// How many distinct scenes a video actually ends up showing, which is
|
|
// the number a viewer experiences rather than the pool it was drawn from.
|
|
let distinct = 0;
|
|
for (let s = 0; s < 4; s++) {
|
|
const look = generateLook(track, { seed: 900 + s * 7919, poolSize: size });
|
|
distinct += new Set(look.sections.flatMap(
|
|
(sec) => sec.variants.flatMap((v) => v.map((l) => l.module.name)))).size / 4;
|
|
}
|
|
|
|
lines.push(` ${String(size).padStart(4)} ${r.floor.toFixed(4)} ${r.observed.toFixed(4)}` +
|
|
` ${spread >= 0 ? '+' : ''}${spread.toFixed(4)} ${(r.observed / r.floor).toFixed(3)}` +
|
|
` ${distinct.toFixed(1)}${range}`);
|
|
if (!best || spread > best.spread) best = { size, spread };
|
|
}
|
|
|
|
lines.push('');
|
|
lines.push(` widest spread at pool ${best.size} (+${best.spread.toFixed(4)})`);
|
|
lines.push('');
|
|
lines.push(' Read the whole column, not the winner. A very small pool wins this');
|
|
lines.push(' metric by making every video repetitive, which the metric cannot see');
|
|
lines.push(' and a viewer cannot miss — pick the knee, not the peak.');
|
|
|
|
return { lines, ok: true, headline: `widest spread at pool ${best.size}` };
|
|
}
|
|
|
|
|
|
/**
|
|
* Identity versus container, measured separately.
|
|
*
|
|
* The one experiment that says whether Epic 3 can work at all: hold the stage
|
|
* fixed and vary only the song's identity, then hold the identity fixed and
|
|
* vary only the stage.
|
|
*/
|
|
export async function decomposeReportLines({ songs = 6, probes = 3 } = {}) {
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
const d = measureDecomposition({ songs, probes });
|
|
|
|
const lines = [];
|
|
lines.push('IDENTITY vs CONTAINER — where visual difference actually comes from');
|
|
lines.push('');
|
|
lines.push(` ${d.songs} songs · one fixed track · ${d.stageCount} cast-consuming scenes`);
|
|
lines.push('');
|
|
lines.push(` identity only ${bar(Math.min(1, d.identityOnly * 5))} ${d.identityOnly.toFixed(4)}`);
|
|
lines.push(` one stage (${d.stage}), each song's cast, ink and lattice`);
|
|
lines.push('');
|
|
lines.push(` container only ${bar(Math.min(1, d.containerOnly * 5))} ${d.containerOnly.toFixed(4)}`);
|
|
lines.push(' one identity, four different stages');
|
|
lines.push('');
|
|
lines.push(` both ${bar(Math.min(1, d.both * 5))} ${d.both.toFixed(4)}`);
|
|
lines.push(' what the generator actually does');
|
|
lines.push('');
|
|
lines.push(` neither ${bar(Math.min(1, d.sameBoth * 5))} ${d.sameBoth.toFixed(4)}`);
|
|
lines.push(' the same thing rendered twice — the instrument\'s own noise floor');
|
|
lines.push('');
|
|
|
|
const ratio = d.containerOnly > 1e-6 ? d.identityOnly / d.containerOnly : 0;
|
|
lines.push(` identity is worth ${(ratio * 100).toFixed(0)}% of what the container is worth.`);
|
|
lines.push('');
|
|
if (d.identityOnly < d.sameBoth * 3) {
|
|
lines.push(' IDENTITY IS NOT REACHING THE FRAME. Swapping every design decision a song');
|
|
lines.push(' makes moves the picture barely more than rendering the same thing twice.');
|
|
lines.push(' More registers cannot help until this number moves.');
|
|
} else if (ratio < 0.5) {
|
|
lines.push(' Identity reaches the frame but the container still dominates. The stages');
|
|
lines.push(' express what they are given only weakly — worth fixing the stages before');
|
|
lines.push(' adding registers for them to ignore.');
|
|
} else {
|
|
lines.push(' Identity carries as much as the container does. The inversion is working');
|
|
lines.push(' at the frame level, and the place to look next is whether the harness');
|
|
lines.push(' aggregates it away.');
|
|
}
|
|
lines.push('');
|
|
return { lines, ok: ratio >= 0.5, headline: `identity ${d.identityOnly.toFixed(4)} vs container ${d.containerOnly.toFixed(4)} (${(ratio * 100).toFixed(0)}%)` };
|
|
}
|