#!/usr/bin/env node // tools/asset-catalog/scripts/lint-subsystem-mds.mjs // Source: Phase 3 D-15 + D-22 (SDOC-01 lint). // // Validates docs/extracted-server/ subsystem MDs: // 1. Every locked-name MD exists with required frontmatter // 2. Every script ID in extracted/server-5-4/scripts/ appears in EXACTLY ONE // subsystem partition AND every object ID in extracted/server-5-4/objects/ // appears in EXACTLY ONE subsystem partition (deterministic enumeration, // WARNING 6 — not a >= 130 heuristic count) // 3. Cross-refs to ./protocol.md / ./save-formats.md / ../extracted-engine/* exist // 4. AUTOGEN blocks present in every subsystem MD // 5. CLAUDE.md hard-rule cross-ref enforcement (WARNING 8): each subsystem MD // that touches packets MUST contain BOTH the literal `CLAUDE.md` string AND // a `hard rule.*#2` (or `#3` for admin-anti-port) regex match — AND-ed, // not OR-ed. // // Usage: node tools/asset-catalog/scripts/lint-subsystem-mds.mjs // Exit: 0 success, 1 failure, 2 usage error. import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const arg = process.argv[2]; if (arg === '--help' || arg === '-h') { process.stdout.write('Usage: node lint-subsystem-mds.mjs \n'); process.exit(0); } if (!arg) { process.stderr.write('Usage: node lint-subsystem-mds.mjs \n'); process.exit(2); } if (!existsSync(arg)) { process.stderr.write(`docs dir not found: ${arg}\n`); process.exit(1); } const REQUIRED_MDS = { 'README.md': { frontmatter: false, mvp: null, subsystem: null }, 'account-auth.md': { frontmatter: true, mvp: 'yes', subsystem: 'account-auth' }, 'world-simulation.md': { frontmatter: true, mvp: 'yes', subsystem: 'world-simulation' }, 'room-management.md': { frontmatter: true, mvp: 'yes', subsystem: 'room-management' }, 'chat.md': { frontmatter: true, mvp: 'yes', subsystem: 'chat' }, 'persistence.md': { frontmatter: true, mvp: 'yes', subsystem: 'persistence' }, 'packet-protocol.md': { frontmatter: true, mvp: null, subsystem: 'packet-protocol' }, 'admin-anti-port.md': { frontmatter: true, mvp: 'no', subsystem: 'admin' }, 'client-server-bridge.md': { frontmatter: true, mvp: 'yes', subsystem: 'client-server-bridge' }, 'message-board.md': { frontmatter: true, mvp: 'no', subsystem: 'message-board' }, 'unknown-actions-status.md': { frontmatter: false, mvp: null, subsystem: null }, }; // WARNING 8: each cross-ref entry is a list of regexes; ALL regexes must match // the file content (AND-ed). Two AND-ed checks for CLAUDE.md hard-rule citations: // the literal `CLAUDE.md` string AND a `hard rule.*#N` pattern. Partial reference // (one without the other) fails the lint. const REQUIRED_CROSSREFS = { 'account-auth.md': [/save-formats/, /protocol\./, /CLAUDE\.md/, /hard rule.*#2/i], 'persistence.md': [/save-formats/, /\.\.\/adr\/0002/], 'packet-protocol.md': [/protocol\./, /extracted-engine\/client-networking/], 'message-board.md': [/save-formats/], 'client-server-bridge.md': [/extracted-engine\/client-networking/, /packet-protocol/], 'chat.md': [/protocol\./, /extracted-engine\/client-networking/], 'admin-anti-port.md': [ /CLAUDE\.md/, /hard rule.*#3/i, /interface AdminKick/, /interface AdminMute/, /interface AdminBan/, ], 'room-management.md': [/protocol\./], 'world-simulation.md': [/scene-room|operations/i], }; function readFrontMatter(text) { if (!text.startsWith('---\n') && !text.startsWith('---\r\n')) return null; const lines = text.split(/\r?\n/); if (lines[0] !== '---') return null; const out = {}; for (let i = 1; i < lines.length; i++) { if (lines[i] === '---') return out; const m = lines[i].match(/^(\w[\w-]*)\s*:\s*(.+?)\s*$/); if (m) out[m[1]] = m[2].trim(); } return null; // no closing --- } let errors = 0; // 1. Required MDs + frontmatter + AUTOGEN + cross-refs (WARNING 8 AND-ed) for (const [filename, spec] of Object.entries(REQUIRED_MDS)) { const path = join(arg, filename); if (!existsSync(path)) { process.stderr.write(`missing required MD: ${path}\n`); errors++; continue; } const txt = readFileSync(path, 'utf-8'); if (spec.frontmatter) { const fm = readFrontMatter(txt); if (!fm) { process.stderr.write(`${filename}: missing YAML frontmatter\n`); errors++; continue; } if (spec.subsystem && fm.subsystem !== spec.subsystem) { process.stderr.write( `${filename}: subsystem='${fm.subsystem}', expected '${spec.subsystem}'\n`, ); errors++; } if (spec.mvp && fm.mvp !== spec.mvp) { process.stderr.write( `${filename}: mvp='${fm.mvp}', expected '${spec.mvp}'\n`, ); errors++; } } if (spec.frontmatter && filename !== 'admin-anti-port.md') { if (!/AUTOGEN:scripts:start/.test(txt)) { process.stderr.write(`${filename}: missing AUTOGEN:scripts block\n`); errors++; } if (!/AUTOGEN:scripts:end/.test(txt)) { process.stderr.write(`${filename}: missing AUTOGEN:scripts:end\n`); errors++; } } // WARNING 8: AND-ed cross-ref enforcement — every regex in the file's REQUIRED_CROSSREFS // entry MUST match the file content; partial matches are NOT acceptable. const refs = REQUIRED_CROSSREFS[filename] || []; for (const re of refs) { if (!re.test(txt)) { process.stderr.write( `${filename}: missing required cross-ref pattern ${re} (D-15 + WARNING 8 AND-ed enforcement)\n`, ); errors++; } } } // 2. SUBSYSTEM-MAP.json deterministic per-ID partition (WARNING 6 — exactly one) const mapPath = join(arg, 'SUBSYSTEM-MAP.json'); if (!existsSync(mapPath)) { process.stderr.write(`missing SUBSYSTEM-MAP.json\n`); errors++; } else { let map; try { map = JSON.parse(readFileSync(mapPath, 'utf-8')); } catch (e) { process.stderr.write(`SUBSYSTEM-MAP.json invalid JSON: ${e.message}\n`); process.exit(1); } const knownSubsystems = new Set([ 'account-auth', 'world-simulation', 'room-management', 'chat', 'persistence', 'packet-protocol', 'admin', 'client-server-bridge', 'message-board', 'misc', ]); for (const k of Object.keys(map)) { if (!knownSubsystems.has(k)) { process.stderr.write(`SUBSYSTEM-MAP unknown key '${k}'\n`); errors++; } if (!Array.isArray(map[k]?.scripts)) { process.stderr.write(`SUBSYSTEM-MAP.${k}.scripts must be array\n`); errors++; } if (!Array.isArray(map[k]?.objects)) { process.stderr.write(`SUBSYSTEM-MAP.${k}.objects must be array\n`); errors++; } } // WARNING 6: enumerate every script ID in extracted/server-5-4/scripts/ // and assert each appears in EXACTLY ONE subsystem partition. Same for objects. // Resolve extracted/server-5-4/ relative to docsDir's grandparent (repo root) // first, then fall back to CWD-relative. const docsDirAbs = resolve(arg); const repoRootCandidate = resolve(docsDirAbs, '..', '..'); const scriptDir = dirname(fileURLToPath(import.meta.url)); const toolDirRoot = resolve(scriptDir, '..', '..', '..'); const scriptCandidates = [ join(repoRootCandidate, 'extracted', 'server-5-4', 'scripts'), 'extracted/server-5-4/scripts', join(toolDirRoot, 'extracted', 'server-5-4', 'scripts'), ]; const objectCandidates = [ join(repoRootCandidate, 'extracted', 'server-5-4', 'objects'), 'extracted/server-5-4/objects', join(toolDirRoot, 'extracted', 'server-5-4', 'objects'), ]; const scriptDirAbs = scriptCandidates.find(p => existsSync(p)); const objectDirAbs = objectCandidates.find(p => existsSync(p)); if (scriptDirAbs) { const scriptIds = new Set(); for (const f of readdirSync(scriptDirAbs)) { const m = f.match(/^(\d+)-/); if (m) scriptIds.add(parseInt(m[1], 10)); } const idToSubs = new Map(); for (const k of Object.keys(map)) { for (const id of map[k]?.scripts ?? []) { if (!idToSubs.has(id)) idToSubs.set(id, []); idToSubs.get(id).push(k); } } for (const id of scriptIds) { const subs = idToSubs.get(id); if (!subs || subs.length === 0) { process.stderr.write( `SUBSYSTEM-MAP: script ${id} not assigned to any subsystem (WARNING 6 zero-orphan)\n`, ); errors++; } else if (subs.length > 1) { process.stderr.write( `SUBSYSTEM-MAP: script ${id} assigned to multiple subsystems [${subs.join(',')}] — must be exactly one\n`, ); errors++; } } } else { process.stderr.write( `lint-subsystem-mds: extracted/server-5-4/scripts not found; skipping per-ID partition check (looked in ${scriptCandidates.join(', ')})\n`, ); } if (objectDirAbs) { const objectIds = new Set(); for (const e of readdirSync(objectDirAbs, { withFileTypes: true })) { if (!e.isDirectory()) continue; const m = e.name.match(/^(\d+)-/); if (m) objectIds.add(parseInt(m[1], 10)); } const idToSubs = new Map(); for (const k of Object.keys(map)) { for (const id of map[k]?.objects ?? []) { if (!idToSubs.has(id)) idToSubs.set(id, []); idToSubs.get(id).push(k); } } for (const id of objectIds) { const subs = idToSubs.get(id); if (!subs || subs.length === 0) { process.stderr.write( `SUBSYSTEM-MAP: object ${id} not assigned to any subsystem (WARNING 6 zero-orphan)\n`, ); errors++; } else if (subs.length > 1) { process.stderr.write( `SUBSYSTEM-MAP: object ${id} assigned to multiple subsystems [${subs.join(',')}] — must be exactly one\n`, ); errors++; } } } else { process.stderr.write( `lint-subsystem-mds: extracted/server-5-4/objects not found; skipping per-ID partition check (looked in ${objectCandidates.join(', ')})\n`, ); } } if (errors > 0) { process.stderr.write(`lint-subsystem-mds: ${errors} error(s)\n`); process.exit(1); } process.stdout.write( `lint-subsystem-mds: OK (${Object.keys(REQUIRED_MDS).length} MDs validated; partition deterministic-clean)\n`, ); process.exit(0);