feat(visual): implement the slice 4e procedural systems and behaviors

The normative coherent noise of 18.7 (ordered gradients, a 255-sample
Fisher-Yates permutation, octave normalization, and curl as the explicit
perpendicular of the potential's gradient, with published tolerances), the nine
distributions with their normative draw-order table, all seventeen behaviors
with their field contracts and the accumulating-versus-fresh split, component
expansion independent of group nesting depth, particles, emitters, repeaters,
trails, and links. Traces 1 through 19 of 18.10 are automated.

The renderer core this builds on is corrected here rather than separately,
because 4e is what exercises it: the production render path now passes a real
compositing surface factory, so a masked group or a non-opaque layer composites
instead of throwing and permanently deactivating the visual subsystem;
system-level `behaviors` arrays run `validateBehaviors` at import with field
context, so the six classes of invalid document that used to fail at activation
now fail where an author can see them; velocity-accumulating behaviors integrate
on object and repeater hosts; `morph` guards both sides of the z delta rather
than poisoning perspective projection with NaN; `face-motion` initializes its
follow state lazily and holds the previous rotation at zero velocity; a `ring`
with equal start and end angles draws nothing instead of a radial spoke; a
resolved non-integer creation count raises ERR_TYPE_MISMATCH at the boundary
instead of being rounded; boolean leaves are type-checked; per-type required
fields and the static half of the unbounded-emission rule are enforced at
import; and a repeater rejects the emitter-only fields 18.5 says it has none of.

A duration is now the authored literal or a non-negative finite number already
in milliseconds, resolving the one design question the review triage left open.
Runtime, validator, and specification agree on it.

A guard test asserts that no two bundled modules declare the same top-level
identifier: the bundler concatenates into one scope, so a private helper name
collision is a SyntaxError in the artifact while every unit test still passes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ShxxFqFmCUDQnQvFNm4TKy
This commit is contained in:
2026-09-06 21:53:13 +00:00
co-authored by Claude Opus 5
parent 0af58da89d
commit 8b00f5c4a0
20 changed files with 2928 additions and 74 deletions
+27 -1
View File
@@ -9,7 +9,7 @@ import { Diagnostics } from '../src/runtime/diagnostics.js';
import { LibraryManager } from '../src/runtime/library.js';
import { deriveStreamState, SeededRNG } from '../src/runtime/rng.js';
import { parseAndValidateExhibit } from '../src/runtime/validator.js';
import { buildStandalone } from '../tools/build-xzbt.mjs';
import { buildStandalone, bundleRuntime } from '../tools/build-xzbt.mjs';
const fixedSource = readFileSync(resolve('exhibits/minimal-fixed.xzbt'), 'utf8');
const randomSource = readFileSync(resolve('exhibits/minimal-random.xzbt'), 'utf8');
@@ -79,8 +79,34 @@ test('two clean standalone builds are byte-identical and self-contained', () =>
// A module import *statement*, not the word in a comment: the bundler strips
// the former, and prose about import-stage validation is not a leak.
assert.doesNotMatch(html, /^[ \t]*import[ \t][^;(\n]*from[ \t]*['"]/m);
// C14: Also catch side-effect imports (import './chunk.js';).
assert.doesNotMatch(html, /^[ \t]*import[ \t]*['"]/m);
assert.doesNotMatch(html, /^[ \t]*export[ \t]/m);
assert.equal(createHash('sha256').update(firstBytes).digest('hex'), createHash('sha256').update(secondBytes).digest('hex'));
const script = html.match(/<script>([\s\S]*)<\/script>/)[1];
assert.doesNotThrow(() => new Function(script));
});
test('no two bundled modules declare the same top-level identifier', () => {
// The bundler strips imports and exports and concatenates into one scope, so
// two modules that each declare a private helper of the same name produce a
// SyntaxError in the artifact while every unit test still passes.
const sources = bundleRuntime();
const declarations = /^(?:const|let|class|function)\s+([A-Za-z_$][\w$]*)/gm;
const perModule = sources.split(/^\/\* (src\/runtime\/[\w.-]+) \*\/$/m);
const seen = new Map();
const collisions = [];
for (let index = 1; index < perModule.length; index += 2) {
const module = perModule[index];
const body = perModule[index + 1] ?? '';
const names = new Set();
let match;
declarations.lastIndex = 0;
while ((match = declarations.exec(body)) !== null) names.add(match[1]);
for (const name of names) {
if (seen.has(name)) collisions.push(`${name}: ${seen.get(name)} and ${module}`);
else seen.set(name, module);
}
}
assert.deepEqual(collisions, []);
});