// prep.mjs concern module — resolve the design-system chunk library (Phase 1b's // emit-chunks.mjs output) onto each scene, and extract the :root brand-tokens // block. Split out of prep.mjs (Step 4b). Mutates scenes[].design_chunks in place // and appends to the shared anomalies array; returns chunksIndex for the summary. import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { die } from "./prep-log.mjs"; // Phase 1b's emit-chunks.mjs writes design-system/chunks/{tokens.css, easings.js, // components/.html, index.json}. Downstream Phase 4b workers read only the // chunks listed in their dispatch — never design.html — cutting per-worker // must-read load by ~4× (12 KB design.html → 1-3 KB per chunk file). // // Resolution policy: // - chunks/index.json missing → degrade gracefully: design_chunks = null // for every scene, log an anomaly, and let // the worker fall back to reading design.html. // - index.json present → every scene gets tokens_file + easings_file // + the FULL component library (worker picks). export function resolveDesignChunks({ designSystemDir, scenes, anomalies }) { const chunksDir = join(designSystemDir, "chunks"); const chunksIndexPath = join(chunksDir, "index.json"); let chunksIndex = null; if (existsSync(chunksIndexPath)) { try { chunksIndex = JSON.parse(readFileSync(chunksIndexPath, "utf8")); } catch (e) { anomalies.push( `design-system/chunks/index.json present but unreadable (${e.message}) — workers will fall back to design.html`, ); } } else { anomalies.push( `design-system/chunks/ missing — Phase 1b's emit-chunks.mjs was not run. Workers will fall back to reading design.html (slower).`, ); } let availableComponents = null; if (chunksIndex) { availableComponents = new Map( (chunksIndex.components || []).map((c) => [c.id, join(designSystemDir, c.file)]), ); } for (const s of scenes) { if (!chunksIndex) { s.design_chunks = null; continue; } const tokensAbs = join(designSystemDir, chunksIndex.tokens_file || "chunks/tokens.css"); const easingsAbs = join(designSystemDir, chunksIndex.easings_file || "chunks/easings.js"); const voiceAbs = join(designSystemDir, chunksIndex.voice_file || "chunks/voice.md"); if (!existsSync(tokensAbs)) die(`design_chunks: tokens_file "${tokensAbs}" referenced by index.json but missing on disk`); if (!existsSync(easingsAbs)) die( `design_chunks: easings_file "${easingsAbs}" referenced by index.json but missing on disk`, ); if (!existsSync(voiceAbs)) die(`design_chunks: voice_file "${voiceAbs}" referenced by index.json but missing on disk`); // Optional chunks (null when preset declared no §H / §T). Worker reads // these on demand — paths are passed through dispatch verbatim. We only check // file existence when index.json references one (consistency guard); the // worker then opens it lazily without re-checking. const hintsAbs = chunksIndex.hints_file ? join(designSystemDir, chunksIndex.hints_file) : null; if (hintsAbs && !existsSync(hintsAbs)) die(`design_chunks: hints_file "${hintsAbs}" referenced by index.json but missing on disk`); const typeRolesAbs = chunksIndex.type_roles_file ? join(designSystemDir, chunksIndex.type_roles_file) : null; if (typeRolesAbs && !existsSync(typeRolesAbs)) die( `design_chunks: type_roles_file "${typeRolesAbs}" referenced by index.json but missing on disk`, ); // Components are a style REFERENCE library, not a plan-time citation. Forward // EVERY available component to every worker; the worker picks which to use by // visual judgment (see agents/hyperframes-scene.md). Existence is guaranteed by // emit-chunks; filter defensively so a stale index entry never ships a missing path. const componentPaths = availableComponents ? [...availableComponents.values()].filter((abs) => existsSync(abs)) : []; s.design_chunks = { tokens_file: tokensAbs, easings_file: easingsAbs, voice_file: voiceAbs, hints_file: hintsAbs, type_roles_file: typeRolesAbs, components: componentPaths, }; } return { chunksIndex }; } // Extract the :root token block from tokens.css. tokens.css is a single global // :root {…} block (brand colors, font roles, spacing/radius). Emit it into // group_spec.brand_tokens_css so assemble-index.mjs can declare it ONCE in // index.html's ; CSS custom properties inherit through the light DOM into // every mounted sub-composition, so scenes reference var(--*) without re-declaring. export function extractBrandTokensCss(chunksIndex, designSystemDir) { let brandTokensCss = ""; if (chunksIndex) { const tokensAbs = join(designSystemDir, chunksIndex.tokens_file || "chunks/tokens.css"); if (existsSync(tokensAbs)) { const tokensRaw = readFileSync(tokensAbs, "utf8"); const m = tokensRaw.match(/:root\s*\{[\s\S]*\}/); brandTokensCss = (m ? m[0] : tokensRaw).trim(); } } return brandTokensCss; }