#!/usr/bin/env node // transitions.mjs — merges three former Phase-4c scripts behind one subcommand dispatcher. // // inject ← inject-transitions.mjs (Tier-B/Tier-A wrapper overlap + GSAP stamp) // verify ← verify-transitions.mjs (deterministic gate over injector output) // check-bridge ← check-bridge-continuity.mjs (Step-6 preflight gate for Tier-A bridges) // // Each subcommand reads its args from process.argv AFTER the subcommand token // (i.e. process.argv.slice(3)). Original per-subcommand usage: // node transitions.mjs inject --group-spec ./group_spec.json --hyperframes . // node transitions.mjs verify --group-spec ./group_spec.json --index ./index.html // node transitions.mjs check-bridge --hyperframes . --group-spec ./group_spec.json import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { transitionsByName } from "./lib/transition-registry.mjs"; import { readDims } from "./lib/dimensions.mjs"; // =========================================================================== // inject ← inject-transitions.mjs async function runInject(argv) { const flag = (name, def) => { const i = argv.indexOf(`--${name}`); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def; }; function die(msg) { console.error(`✗ inject-transitions.mjs: ${msg}`); process.exit(1); } const groupSpecPath = resolve(flag("group-spec", "./group_spec.json")); const hyperframesDir = resolve(flag("hyperframes", ".")); const indexPath = join(hyperframesDir, "index.html"); if (!existsSync(groupSpecPath)) die(`group_spec.json not found at ${groupSpecPath}`); if (!existsSync(indexPath)) die(`index.html not found at ${indexPath} — run assemble-index.mjs first`); let spec; try { spec = JSON.parse(readFileSync(groupSpecPath, "utf8")); } catch (e) { die(`group_spec.json parse: ${e.message}`); } // Slide transitions push a wrapper fully off-canvas, so the travel distance is // the canvas dimension along the slide axis (read from group_spec; landscape // default for pre-dims specs). const { width: CANVAS_W, height: CANVAS_H } = readDims(spec); const transitions = Array.isArray(spec.transitions) ? spec.transitions : []; const rawInjectable = transitions.filter((t) => t.tier === "b" || t.tier === "a"); let html = readFileSync(indexPath, "utf8"); if (rawInjectable.length === 0) { console.log(`✓ inject-transitions: 0 transitions to inject — index.html unchanged`); process.exit(0); } let txByName; try { txByName = transitionsByName(); } catch (e) { die(`transition registry: ${e.message}`); } // The authoritative VISUAL clip id set. Captions / voice / bgm / sfx also use // id="el-…" or data-composition-src, but they are not visual scene clips and // must not join the ping-pong transition track assignment. const visualIds = new Set(); if (Array.isArray(spec.visual_clips) && spec.visual_clips.length > 0) { for (const v of spec.visual_clips) if (v?.id) visualIds.add(v.id); } else { for (const g of spec.groups || []) for (const sid of g.scene_ids || []) visualIds.add(sid); } if (visualIds.size === 0) die(`group_spec has no visual clips — cannot identify wrappers`); const sceneToVisual = spec.scene_to_visual || {}; const normalizeClipId = (id) => (visualIds.has(id) ? id : sceneToVisual[id] || id); // ---------- parse VISUAL clip wrappers out of index.html ---------- // assemble-index emits each scene clip as a stable multi-line block: //
// We only keep clips whose id is a known visual clip — captions (el-captions) // and audio (el-