Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/thatstupidcomputer / scripts/sync-native-timelines.mjs |
| Date | Not specified in source |
| Repository | Janady13/thatstupidcomputer |
| Revision | 57555cde46910102dd25d6c1c1fb2c359e9655bd |
| Branch | main |
| Path or archive member | scripts/sync-native-timelines.mjs |
| Original SHA-256 | 89e8530b53410762272ec014bd3f94171cd2f273b00de70d21f6d35495b57723 |
| Git blob SHA | f0bdb2342566fc15f0fba16328fad255232541e2 |
| Reading-copy SHA-256 | 89e8530b53410762272ec014bd3f94171cd2f273b00de70d21f6d35495b57723 |
Recovered source transcript
Download complete source text ↓99 lines · 7.8 KB
/** Authoritative motion JSON -> existing official RML timeline elements; never a .riv serializer. */
import { createHash } from 'node:crypto';
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
import { CHARACTER_IDS } from '../apps/web/src/animation/core/contracts.mjs';
// Confirmed with official CLI 1.0.2 `rive schema Node`: all six properties are doubles.
export const RML_PROPERTY_KEYS = Object.freeze({ x: 13, y: 14, rotation: 15, scaleX: 16, scaleY: 17, opacity: 18 });
export const sha256 = value => createHash('sha256').update(value).digest('hex');
export function synchronizeTimelines(source, clips) {
const doc = new DOMParser({ onError: (level, message) => { throw new Error(`${level}: ${message}`); } }).parseFromString(source, 'application/xml');
const boards = Array.from(doc.getElementsByTagName('Artboard'));
if (boards.length !== 1) throw new Error('Expected exactly one supplied character artboard');
const board = boards[0], ids = new Map();
for (const node of Array.from(board.getElementsByTagName('*'))) {
if (!['Node', 'Shape'].includes(node.tagName) || !node.hasAttribute('name')) continue;
const name = node.getAttribute('name'), id = node.getAttribute('id');
if (ids.has(name)) throw new Error(`Ambiguous original rig node ${name}`);
if (!/^\d+:\d+$/.test(id)) throw new Error(`Missing original RML ID for ${name}`);
ids.set(name, id);
}
if (!Array.isArray(clips) || !clips.length || clips.length > 100) throw new Error('Expected bounded authoritative motion clips');
const names = new Set();
const replacement = [];
let trackCount = 0, keyframeCount = 0;
const element = (name, attributes) => { const node = doc.createElement(name); for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value)); return node; };
for (const clip of clips) {
if (!/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(clip.name) || names.has(clip.name)) throw new Error('Invalid or duplicate animation name');
names.add(clip.name);
if (clip.fps !== 60 || !Number.isInteger(clip.duration) || clip.duration < 1 || clip.duration > 3600 || Math.abs(clip.seconds - clip.duration / clip.fps) > .0001 || typeof clip.loop !== 'boolean') throw new Error(`Invalid authoritative timing: ${clip.name}`);
if (!Array.isArray(clip.tracks) || !clip.tracks.length || clip.tracks.length > 1500) throw new Error(`Invalid tracks: ${clip.name}`);
const animation = element('LinearAnimation', { name: clip.name, fps: clip.fps, duration: clip.duration, loopValue: clip.loop ? '1' : '0' });
const objects = new Map(), channels = new Set();
for (const track of clip.tracks) {
const target = ids.get(track.target), propertyKey = RML_PROPERTY_KEYS[track.property];
if (!target || !Object.hasOwn(RML_PROPERTY_KEYS, track.property) || !['linear', 'hold'].includes(track.interpolation)) throw new Error(`Unsupported target/channel ${clip.name}: ${track.target}.${track.property}`);
const channel = `${target}:${propertyKey}`;
if (channels.has(channel)) throw new Error(`Duplicate channel ${channel}`);
channels.add(channel);
if (!Array.isArray(track.keys) || track.keys.length < 2 || track.keys.length > 3601 || track.keys[0][0] !== 0 || track.keys.at(-1)[0] !== clip.duration) throw new Error(`Missing channel endpoints: ${channel}`);
let object = objects.get(target);
if (!object) { object = element('KeyedObject', { objectId: target }); objects.set(target, object); animation.appendChild(object); }
const property = element('KeyedProperty', { propertyKey });
object.appendChild(property);
let previous = -1;
for (const key of track.keys) {
if (!Array.isArray(key) || key.length !== 2 || !Number.isInteger(key[0]) || key[0] <= previous || key[0] > clip.duration || !Number.isFinite(key[1])) throw new Error(`Invalid keyframe: ${channel}`);
previous = key[0];
property.appendChild(element('KeyFrameDouble', { frame: key[0], interpolationType: track.interpolation, value: key[1] }));
keyframeCount++;
}
trackCount++;
}
replacement.push(animation);
}
// Replace only timeline siblings; geometry, hierarchy, IDs and state-machine elements survive.
for (const child of Array.from(board.childNodes)) if (child.nodeType === 1 && child.nodeName === 'LinearAnimation') board.removeChild(child);
for (const animation of replacement) board.appendChild(animation);
return { rml: new XMLSerializer().serializeToString(doc), stats: { animations: clips.length, tracks: trackCount, keyframes: keyframeCount } };
}
export function snapshotV1Proof(root) {
const history = resolve(root, 'native/history/v1');
if (existsSync(resolve(history, 'HISTORICAL.json'))) return;
mkdirSync(history, { recursive: true });
for (const name of ['reports', 'compiled', 'canvas-verified-assets.json', 'canvas-visual-review.json']) {
const source = resolve(root, 'native', name);
if (existsSync(source)) cpSync(source, resolve(history, name), { recursive: true });
}
writeFileSync(resolve(history, 'HISTORICAL.json'), JSON.stringify({ status: 'HISTORICAL_V1_ONLY', archivedAt: new Date().toISOString(), note: 'Original 14-action proof copied from the immutable v1 delivery. Does not verify expanded v2 motion or new compiled bytes. Original native/rml and candidates are retained separately.' }, null, 2) + '\n');
}
export function markNativeProofStale(root, records) {
const reports = resolve(root, 'native/reports'); mkdirSync(reports, { recursive: true });
const previousFile = resolve(reports, 'native-compiled.json');
const previous = existsSync(previousFile) ? JSON.parse(readFileSync(previousFile, 'utf8')) : null;
const current = records.every(record => previous?.files?.some(file => file.slug === record.slug && file.buildRmlSHA256 === record.buildRmlSHA256 && file.motionSHA256 === record.motionSHA256));
if (current) return;
writeFileSync(resolve(root, 'native/canvas-verified-assets.json'), '{}\n');
writeFileSync(resolve(root, 'native/canvas-visual-review.json'), '{}\n');
writeFileSync(resolve(reports, 'native-canvas.json'), JSON.stringify({ status: 'STALE', reason: 'Authoritative motion/build source changed; exact-byte Canvas proof must be rerun.', files: [] }, null, 2) + '\n');
writeFileSync(resolve(reports, 'browser-boundary.json'), JSON.stringify({ status: 'BLOCKED', scope: 'Browser/WebGL verification and promotion', reason: 'Existing browser URL policy rejected this workflow. Alternate browsers, CDP, navigation workarounds and browser commands are prohibited. No browser server is started.', permittedAlternative: 'Official Canvas WASM inside Node with @napi-rs/canvas, local bytes only; no HTTP or browser control.' }, null, 2) + '\n');
}
const root = fileURLToPath(new URL('..', import.meta.url));
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
snapshotV1Proof(root);
const records = [];
for (const slug of CHARACTER_IDS) {
const path = resolve(root, `native/build-source/${slug}/scene.rml`);
const motion = readFileSync(resolve(root, `apps/web/public/animation/mobile/${slug}/motion.json`), 'utf8');
const result = synchronizeTimelines(readFileSync(path, 'utf8'), JSON.parse(motion));
writeFileSync(path, result.rml);
records.push({ slug, originalRmlSHA256: sha256(readFileSync(resolve(root, `native/rml/${slug}/scene.rml`))), motionSHA256: sha256(motion), buildRmlSHA256: sha256(result.rml), ...result.stats });
}
markNativeProofStale(root, records);
writeFileSync(resolve(root, 'native/reports/timeline-sync.json'), JSON.stringify({ status: 'PASS', generatedAt: new Date().toISOString(), files: records }, null, 2) + '\n');
console.log(`Synchronized ${records.length} existing RML projects from authoritative motion JSON.`);
}