#!/usr/bin/env node /** * Build design-grammar/index.html from design-grammar/grammar.yml. * * Strategy: the existing index.html is the template. Data sections are * marked with sentinel HTML comments (`` etc.). On * each run we replace the contents between the sentinels with freshly * emitted markup; everything else (CSS, render JS, prose) is preserved. * * On the FIRST run (before sentinels exist) we use a one-time bootstrap * that finds the current data definitions by their familiar `const ... =` * patterns and inserts the sentinel comments around them. * * Usage: node design-grammar/scripts/build-html.mjs */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import yaml from "js-yaml"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(__dirname, "..", ".."); const HTML_PATH = path.join(REPO, "design-grammar", "index.html"); const YAML_PATH = path.join(REPO, "design-grammar", "grammar.yml"); // ── 1. Load and validate ──────────────────────────────────────────────── const doc = yaml.load(fs.readFileSync(YAML_PATH, "utf8")); validate(doc); const KNOWN_SYMS = new Set(doc.primitives.map((e) => e.sym)); // ── 2. Render the data sections from YAML ─────────────────────────────── const renderedRoles = renderRolesJs(doc.roles, doc.layers); const renderedPrimitives = renderPrimitivesJs(doc.primitives); const renderedAssemblies = renderAssembliesHtml(doc.assemblies, KNOWN_SYMS); // ── 3. Read the current HTML, bootstrap sentinels if needed, patch ────── let html = fs.readFileSync(HTML_PATH, "utf8"); html = bootstrapSentinels(html); html = html.replace(/
.*?<\/p>/, `
${escapeHtml(doc.subtitle)}
`); html = replaceBetween(html, "@gen:roles", renderedRoles); html = replaceBetween(html, "@gen:primitives", renderedPrimitives); html = replaceBetween(html, "@gen:assemblies", renderedAssemblies); fs.writeFileSync(HTML_PATH, html); console.log(`Wrote ${HTML_PATH}`); console.log(` ${doc.primitives.length} primitives`); console.log(` ${doc.assemblies.reduce((n, s) => n + s.items.length, 0)} assemblies across ${doc.assemblies.length} sections`); console.log(` ${(doc.known_collisions || []).length} documented symbol collisions`); // ════════════════════════════════════════════════════════════════════════ // Validation // ════════════════════════════════════════════════════════════════════════ function validate(doc) { if (!doc || typeof doc !== "object") throw new Error("grammar.yml is empty or malformed"); if (!Array.isArray(doc.primitives) || doc.primitives.length === 0) throw new Error("No primitives defined"); const issues = []; const knownSyms = new Set(doc.primitives.map((e) => e.sym)); const cellSeen = new Map(); for (const e of doc.primitives) { const k = `${e.layer},${e.col}`; if (cellSeen.has(k)) { issues.push(`Cell collision at (${k}): #${cellSeen.get(k)} and #${e.id}`); } else { cellSeen.set(k, e.id); } } for (const e of doc.primitives) { for (const b of e.composition_links || []) { if (!knownSyms.has(b)) issues.push(`Primitive #${e.id} ${e.sym}: unresolved composition link '${b}'`); } } const symCount = {}; for (const e of doc.primitives) symCount[e.sym] = (symCount[e.sym] || 0) + 1; const declared = new Set((doc.known_collisions || []).map((c) => c.sym)); for (const [sym, count] of Object.entries(symCount)) { if (count > 1 && !declared.has(sym)) { issues.push(`Undocumented symbol collision: ${sym} appears ${count} times — add to known_collisions`); } } for (const section of doc.assemblies || []) { for (const item of section.items || []) { const refs = extractExpressionSymbols(item.expression); for (const ref of refs) { if (!knownSyms.has(ref)) { issues.push(`Assembly "${item.name}" references unknown symbol '${ref}': ${item.expression}`); } } } } if (issues.length > 0) { console.error("VALIDATION FAILED:"); issues.forEach((i) => console.error(" " + i)); process.exit(1); } } function extractExpressionSymbols(expression) { const cleaned = expression.replace(/_[A-Za-z]+/g, ""); const re = /(? jsStr(r.name)).join(",")}];`; return out; } function renderPrimitivesJs(primitives) { let out = "// [num, sym, name, role, layer, col, year, links[], rationale]\n"; out += "const primitives = [\n"; const byLayer = {}; for (const e of primitives) (byLayer[e.layer] ||= []).push(e); const layerComments = { 1: "// Layer 0: Data (The Raw Material)", 2: "// Layer 1: Math (The Theoretical Bedrock)", 3: "// Layer 2: Algorithms (The Operations)", 4: "// Layer 3: Architecture (The Topologies)", 5: "// Layer 4: Optimization (The Physics of Efficiency)", 6: "// Layer 5: Runtime (Software Execution Primitives)", 7: "// Layer 6: Hardware (Silicon Primitives)", 8: "// Layer 7: Production (Fleet Primitives)", }; const layerKeys = Object.keys(byLayer).map(Number).sort((a, b) => a - b); for (const layer of layerKeys) { out += ` ${layerComments[layer] || `// Layer ${layer}`}\n`; for (const e of byLayer[layer]) { const yearLit = e.year === null || e.year === undefined ? `'—'` : jsStr(e.year); const linksLit = `[${(e.composition_links || []).map(jsStr).join(",")}]`; out += ` [${e.id},${jsStr(e.sym)},${jsStr(e.name)},${jsStr(e.role)},${e.layer},${e.col},${yearLit},${jsStr(e.description)},${linksLit},${jsStr(e.rationale)}],\n`; } out += "\n"; } // Drop the trailing blank line and trailing comma to keep diffs minimal. out = out.replace(/,\n\n$/, "\n"); out += "];"; return out; } function renderAssembliesHtml(assemblies, knownSyms) { let out = ""; for (const section of assemblies) { out += `\n