Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. forked from cameron.stream/misaligned
Something went wrong. Try again.
14 kB · 433 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434#!/usr/bin/env node/** * Sync the wiki corpus (+ the root DESIGN.md legacy doorway) into Starlight, * emit a sidebar module from wiki/SUMMARY.md. * * Generated paths are gitignored; never hand-edit them. */import fs from 'node:fs';import path from 'node:path';import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));const SITE_ROOT = path.resolve(__dirname, '..');const REPO_ROOT = path.resolve(SITE_ROOT, '..');const WIKI_ROOT = path.join(REPO_ROOT, 'wiki');const REPO_ASSETS_ROOT = path.join(REPO_ROOT, 'assets');const DOCS_OUT = path.join(SITE_ROOT, 'src', 'content', 'docs');const ASSETS_OUT = path.join(SITE_ROOT, 'src', 'content', 'assets');const GENERATED_OUT = path.join(SITE_ROOT, 'src', 'generated');const SIDEBAR_OUT = path.join(GENERATED_OUT, 'sidebar.ts');
function rmrf(dir) { fs.rmSync(dir, { recursive: true, force: true });}
function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true });}
const ASSET_EXT = new Set([ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.avif',]);
function walkFiles(dir, predicate, out = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walkFiles(full, predicate, out); } else if ( (entry.isFile() || entry.isSymbolicLink()) && predicate(entry.name, full) ) { out.push(full); } } return out;}
function extractTitle(body, fallback) { for (const line of body.split(/\r?\n/)) { const m = line.match(/^#\s+(.+?)\s*$/); if (m) { return m[1] .replace(/[*_`]/g, '') .replace(/\s+/g, ' ') .trim(); } } return fallback;}
const CORPUS_ROLES = { law: { name: 'LAW', meaning: 'Binding invariant' }, spec: { name: 'SPEC', meaning: 'Binding system contract' }, knowledge: { name: 'KNOWLEDGE', meaning: 'Current-state fact' }, log: { name: 'LOG', meaning: 'Historical record — not current design' }, navigation: { name: 'NAVIGATION', meaning: 'Doorway only — no design authority' },};
function extractCorpusRole(body, wikiRel) { if (wikiRel === 'DESIGN.md') { return { role: 'navigation', body }; }
const matches = [...body.matchAll(/^Type: (law|spec|knowledge|log)\s*$/gm)]; if (matches.length !== 1) { throw new Error( `${wikiRel}: expected exactly one supported Type line, found ${matches.length}`, ); }
const match = matches[0]; body = `${body.slice(0, match.index)}${body.slice(match.index + match[0].length)}`; // Law, knowledge, and log pages commonly put Type in its own fenced block. // Once the line becomes a visible role banner, remove that now-empty fence. body = body.replace(/^```[ \t]*\r?\n[ \t]*\r?\n```[ \t]*\r?\n?/m, ''); return { role: match[1], body };}
function renderCorpusRole(role) { const copy = CORPUS_ROLES[role]; return [ `<div class="corpus-role corpus-role--${role}" data-page-role="${role}">`, ` <span class="corpus-role__name">${copy.name}</span>`, ` <span class="corpus-role__meaning">${copy.meaning}</span>`, '</div>', ].join('\n');}
/** Strip the first ATX H1 — Starlight already renders `title` as the page H1. */function stripLeadingH1(body) { const lines = body.split(/\r?\n/); let i = 0; while (i < lines.length && lines[i].trim() === '') i += 1; if (i < lines.length && /^#\s+/.test(lines[i])) { i += 1; while (i < lines.length && lines[i].trim() === '') i += 1; return lines.slice(i).join('\n'); } return body;}
function yamlEscape(s) { if (/[:#{}[\],&*!|>'"%@`]|^[-?]|\n/.test(s) || s !== s.trim()) { return JSON.stringify(s); } return s;}
/** * Site base prefix without trailing slash. Empty string when the site is * served at domain root (local `SITE_BASE=/`). Production Tangled sub-path * hosting defaults to `/misaligned` — same default as astro.config.mjs. */function siteBasePrefix() { const raw = process.env.SITE_BASE ?? '/misaligned'; if (!raw || raw === '/') return ''; return raw.endsWith('/') ? raw.slice(0, -1) : raw;}
/** wiki-relative path → Starlight slug (no leading/trailing slash). */function wikiRelToSlug(relPosix) { const normalized = path.posix.normalize(relPosix); let slug = normalized.replace(/\.md$/i, ''); // Keep the old constitution slug for root-doorway compatibility. if (slug === 'DESIGN' || slug === '../DESIGN') return 'constitution'; if (slug.endsWith('/README')) { slug = slug.slice(0, -'/README'.length); } else if (slug === 'README') { slug = ''; } // Starlight / Astro content collection IDs are lowercased. return slug.toLowerCase();}
/** Starlight slug → public URL under SITE_BASE (trailing slash). */function slugToPublicUrl(slug) { const prefix = siteBasePrefix(); if (!slug) return `${prefix}/`; return `${prefix}/${slug}/`;}
function destFileForWikiRel(relPosix) { if (relPosix === 'DESIGN.md') return path.join(DOCS_OUT, 'constitution.md'); if (relPosix === 'SUMMARY.md') return null; if (/(^|\/)README\.md$/i.test(relPosix)) { const dir = relPosix.replace(/README\.md$/i, ''); return path.join(DOCS_OUT, dir, 'index.md'); } // Keep original filename casing on disk. Starlight content IDs are // lowercased regardless; sidebar slugs go through wikiRelToSlug(). return path.join(DOCS_OUT, relPosix);}
function rewriteMarkdownLinks(body, fromWikiRel) { const fromDir = path.posix.dirname(fromWikiRel); return body.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (full, text, href) => { const trimmed = href.trim(); if ( !trimmed || trimmed.startsWith('#') || trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('mailto:') || trimmed.startsWith('tel:') ) { return full; }
const [pathPartRaw, hash = ''] = trimmed.split('#'); let pathPart = pathPartRaw; if (!pathPart) return full;
pathPart = pathPart.replace(/^<|>$/g, '').split(/\s+/)[0];
let targetWikiRel; if (pathPart.startsWith('/')) { const abs = pathPart.replace(/^\//, ''); if (abs === 'DESIGN.md' || abs === 'DESIGN') { targetWikiRel = 'DESIGN.md'; } else if (abs.startsWith('wiki/')) { targetWikiRel = abs.slice('wiki/'.length); } else { return full; } } else { const joined = path.posix.normalize(path.posix.join(fromDir, pathPart)); if (joined === '../DESIGN.md' || joined === 'DESIGN.md') { targetWikiRel = 'DESIGN.md'; } else if (fromWikiRel === 'DESIGN.md' && joined.startsWith('wiki/')) { targetWikiRel = joined.slice('wiki/'.length); } else if (fromWikiRel === 'DESIGN.md') { // Other root repository files are not part of the public docs tree. return text; } else if (joined.startsWith('../')) { return full; } else { targetWikiRel = joined; } }
if (!/\.md$/i.test(targetWikiRel)) return full; // SUMMARY drives the sidebar but is not itself a rendered page. if (targetWikiRel === 'SUMMARY.md') return text;
const slug = wikiRelToSlug(targetWikiRel); // Must include SITE_BASE. Root-absolute `/slug/` links 404 on Tangled // sub-path hosting (cameron.tngl.io/misaligned/...), where the public // route is `/misaligned/slug/`. Starlight prefixes its own chrome; body // markdown is emitted as written, so the sync owns the base. const url = slugToPublicUrl(slug); const withHash = hash ? `${url}#${hash}` : url; return `[${text}](${withHash})`; });}
function writeDoc(wikiRel, sourceAbs) { const dest = destFileForWikiRel(wikiRel); if (!dest) return;
let body = fs.readFileSync(sourceAbs, 'utf8'); if (body.startsWith('---\n')) { const end = body.indexOf('\n---\n', 4); if (end !== -1) body = body.slice(end + 5); }
const fallbackTitle = path.basename(wikiRel, '.md'); const title = extractTitle(body, fallbackTitle); const corpus = extractCorpusRole(body, wikiRel); body = corpus.body; body = stripLeadingH1(body); body = rewriteMarkdownLinks(body, wikiRel); body = `${renderCorpusRole(corpus.role)}\n\n${body.trimStart()}`;
const out = `---\ntitle: ${yamlEscape(title)}\n---\n\n${body}`; ensureDir(path.dirname(dest)); fs.writeFileSync(dest, out, 'utf8');}
function nestInto(items, item, depth) { if (depth <= 0 || items.length === 0) { items.push(item); return; } const prev = items[items.length - 1]; if (!prev.items) { items[items.length - 1] = { label: prev.label, items: [{ label: prev.label, slug: prev.slug }], }; } const parent = items[items.length - 1]; if (depth === 1) { parent.items.push(item); } else { nestInto(parent.items, item, depth - 1); }}
function mapItem(item) { if (item.items) { return { label: item.label, collapsed: true, items: item.items.map(mapItem), }; } const slug = item.slug === '' ? 'index' : item.slug; return { label: item.label, slug };}
/** Parse the repo's CommonMark SUMMARY.md into Starlight sidebar items. */function parseSummary(summaryText) { const lines = summaryText.split(/\r?\n/); const top = []; let currentSection = null;
const linkRe = /^(\s*)-\s+\[([^\]]+)\]\(([^)]+)\)\s*$/; const bareLinkRe = /^\[([^\]]+)\]\(([^)]+)\)\s*$/; const sectionRe = /^#\s+(.+?)\s*$/;
function pushLink(label, href, indent) { const cleanHref = href.split('#')[0]; if (!cleanHref.endsWith('.md')) return; const slug = wikiRelToSlug(cleanHref); const item = { label, slug };
if (!currentSection) { top.push({ type: 'link', ...item }); return; }
const depth = Math.floor(indent / 2); if (depth <= 1) { currentSection.items.push(item); } else { nestInto(currentSection.items, item, depth - 1); } }
for (const raw of lines) { const line = raw.replace(/\t/g, ' '); if (!line.trim()) continue; if (line.trim().startsWith('Dated session')) break;
const sec = line.match(sectionRe); if (sec) { const label = sec[1].trim(); if (label.toLowerCase() === 'summary') continue; currentSection = { label, items: [] }; top.push({ type: 'section', label, items: currentSection.items }); continue; }
const bare = line.match(bareLinkRe); if (bare && !line.trimStart().startsWith('-')) { pushLink(bare[1], bare[2], 0); continue; }
const m = line.match(linkRe); if (m) pushLink(m[2], m[3], m[1].length); }
const mapped = top.map((entry) => { if (entry.type === 'link') { return { label: entry.label, slug: entry.slug === '' ? 'index' : entry.slug, }; } return { label: entry.label, collapsed: false, items: entry.items.map(mapItem), }; }); // Splash homepage is a custom Astro route, not a docs slug. return [{ label: 'Home', link: '/' }, ...mapped];}
function writeSidebar(sidebar) { ensureDir(GENERATED_OUT); const json = JSON.stringify(sidebar, null, 2); const src = `/** Auto-generated by scripts/sync-wiki.mjs — do not edit. */\nexport const sidebar = ${json} as const;\n`; fs.writeFileSync(SIDEBAR_OUT, src, 'utf8');}
function main() { if (!fs.existsSync(WIKI_ROOT)) { console.error(`wiki root not found: ${WIKI_ROOT}`); process.exit(1); }
rmrf(DOCS_OUT); rmrf(ASSETS_OUT); ensureDir(DOCS_OUT); ensureDir(ASSETS_OUT); ensureDir(GENERATED_OUT);
// DESIGN.md is a root doorway, not design authority. Sync it explicitly for // old /constitution/ links instead of keeping a renderer-specific duplicate. writeDoc('DESIGN.md', path.join(REPO_ROOT, 'DESIGN.md')); let pageCount = 1;
const mdFiles = walkFiles(WIKI_ROOT, (name) => name.endsWith('.md')); for (const abs of mdFiles) { const rel = path.relative(WIKI_ROOT, abs).split(path.sep).join('/'); const real = fs.realpathSync(abs); writeDoc(rel, real); pageCount += 1; }
const wikiAssets = walkFiles(WIKI_ROOT, (name) => ASSET_EXT.has(path.extname(name).toLowerCase()), ); let assetCount = 0; for (const abs of wikiAssets) { const rel = path.relative(WIKI_ROOT, abs).split(path.sep).join('/'); const dest = path.join(DOCS_OUT, rel); ensureDir(path.dirname(dest)); fs.copyFileSync(fs.realpathSync(abs), dest); assetCount += 1; }
// Wiki pages keep repo-relative links to design references under assets/. // Mirror those files beside the generated docs so Astro can resolve the // same ../../assets/... paths without rewriting source documentation. if (fs.existsSync(REPO_ASSETS_ROOT)) { const repoAssets = walkFiles(REPO_ASSETS_ROOT, (name) => ASSET_EXT.has(path.extname(name).toLowerCase()), ); for (const abs of repoAssets) { const rel = path .relative(REPO_ASSETS_ROOT, abs) .split(path.sep) .join('/'); const dest = path.join(ASSETS_OUT, rel); ensureDir(path.dirname(dest)); fs.copyFileSync(fs.realpathSync(abs), dest); assetCount += 1; } }
const summary = fs.readFileSync(path.join(WIKI_ROOT, 'SUMMARY.md'), 'utf8'); const sidebar = parseSummary(summary); writeSidebar(sidebar);
console.log( `sync-wiki: wrote ${pageCount} pages + ${assetCount} assets → ${path.relative(REPO_ROOT, DOCS_OUT)}`, ); console.log( `sync-wiki: sidebar → ${path.relative(REPO_ROOT, SIDEBAR_OUT)} (${sidebar.length} top entries)`, );}
main();