import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(fileURLToPath(new URL("..", import.meta.url))); // `/pom resume [state-path]` is documented in the README command map and in the POM.yml // `commands:` block, but it is deliberately absent from the HELP template literal in // src/command.ts. HELP is canonical for what the built-in help advertises, so the verb is // neither removed from the docs nor added to HELP. It is recorded here instead, so the // divergence stays visible and the parity check still passes deterministically. // Any command divergence that is not listed here is a failure. const KNOWN_COMMAND_DIVERGENCES = new Set(["resume"]); // Roots that src/*.ts loads through `new URL("../", import.meta.url)`. Check 1 derives // this set from source instead of hardcoding it, so a new bundled-resource root cannot be // added in code and silently omitted from the published package. const RESOURCE_SOURCES = ["src/prompts.ts", "src/themes.ts", "src/events.ts"]; const BUNDLED_ROOT = /new URL\(\s*["'`]\.\.\/([A-Za-z0-9._-]+)/g; // Generated-layout names that drifted in earlier revisions and must never come back. const FORBIDDEN_LAYOUT_NAMES = ["06_exports", "06_logs"]; const REQUIRED_LAYOUT_NAMES = ["06_ledgers", "07_exports"]; const LAYOUT_DOCS = ["docs/INVENTORY.md", "skills/pom/references/artifacts.md"]; const VERIFICATION_STATUSES = ["PASS", "FAIL", "NOT RUN", "BLOCKED"]; const MARKDOWN_LINK = /\[[^\]]*\]\(\s*([^)\s]+?)(?:\s+"[^"]*")?\s*\)/g; const NAVIGATION_PAGES = ["index.md", "navigation.md"]; // Canonical project coordinates. These live on infrastructure the maintainer owns // (loca.zone). There is no public forge mirror, so a plausible-looking third-party URL // must never be substituted, and the earlier `UNRESOLVED:` placeholders must never return. const METADATA_FIELDS = ["repository", "homepage", "bugs"]; const CANONICAL_HOST = "loca.zone"; const PLACEHOLDER_MARKER = "UNRESOLVED"; const TOTAL_CHECKS = 7; const checks = []; const failed = []; function text(rel) { return readFileSync(join(root, rel), "utf8"); } function lineNumbers(body, pattern) { return body .split("\n") .map((line, index) => (pattern.test(line) ? index + 1 : 0)) .filter(Boolean); } /** Blanks fenced blocks and inline code spans so illustrative links are not link-checked. */ function withoutCode(body) { const blank = (matched) => matched.replace(/[^\n]/g, " "); return body.replace(/^```[\s\S]*?^```/gm, blank).replace(/`[^`\n]*`/g, blank); } /** Reads the indented body of a top-level or nested block key, e.g. `commands:`. */ function blockLines(rel, key) { const lines = text(rel).split("\n"); const start = lines.findIndex((line) => new RegExp(`^\\s*${key}:\\s*$`).test(line)); if (start < 0) return null; const indent = lines[start].length - lines[start].trimStart().length; const body = []; for (const line of lines.slice(start + 1)) { if (line.trim() === "") continue; if (line.length - line.trimStart().length <= indent) break; body.push(line.trim()); } return body; } /** `/pom` -> `dashboard`; `/pom theme install` -> `theme`. */ function verbOf(command) { const match = /^\/pom(?:\s+([a-z][a-z0-9-]*))?\b/.exec(command); if (!match) return null; return match[1] ?? "dashboard"; } function check(index, name, run) { const problems = []; const notes = []; try { run((message) => problems.push(message), (message) => notes.push(message)); } catch (error) { problems.push(`unexpected error: ${error.message}`); } console.log(`${problems.length === 0 ? "PASS" : "FAIL"} check ${index}/${TOTAL_CHECKS} ${name}`); for (const problem of problems) console.log(` ${problem}`); for (const note of notes) console.log(` note: ${note}`); if (problems.length > 0) failed.push(name); } // 1. Resource/package parity: every bundled-resource root loaded by source must ship. checks.push((index) => check(index, "resource-package-parity", (fail) => { const roots = new Map(); for (const rel of RESOURCE_SOURCES) { if (!existsSync(join(root, rel))) { fail(`${rel}: resource-loading module is missing; check 1 cannot derive bundled roots from source`); continue; } const body = text(rel); for (const match of body.matchAll(BUNDLED_ROOT)) { if (!roots.has(match[1])) roots.set(match[1], new Set()); roots.get(match[1]).add(rel); } } if (roots.size === 0) { fail( `no bundled-resource roots discovered in ${RESOURCE_SOURCES.join(", ")}; the ` + `\`new URL("../", import.meta.url)\` pattern in scripts/check-docs.mjs no longer ` + "matches source and must be repaired (this is a broken check, not an empty result)", ); return; } const files = JSON.parse(text("package.json")).files; if (!Array.isArray(files)) { fail("package.json: `files` must be an array listing every published path"); return; } for (const [name, sources] of [...roots].sort(([left], [right]) => left.localeCompare(right))) { if (!files.includes(name)) fail(`package.json: \`files\` must contain "${name}" because ${[...sources].join(" and ")} load it at runtime`); if (!existsSync(join(root, name))) fail(`${name}: directory is loaded by ${[...sources].join(" and ")} but does not exist in the repository`); } }), ); // 2. Command parity: HELP, the README command map, and POM.yml `commands:` must agree. checks.push((index) => check(index, "command-parity", (fail, note) => { const command = text("src/command.ts"); const help = /const HELP\s*=\s*`([\s\S]*?)`\s*;/.exec(command); if (!help) { fail("src/command.ts: could not locate the `const HELP = `...`;` template literal that defines the command surface"); return; } const helpVerbs = new Set(); for (const line of help[1].split("\n")) { const trimmed = line.trim(); if (!trimmed.startsWith("/pom")) continue; const verb = verbOf(trimmed); if (verb) helpVerbs.add(verb); } const readmeLines = text("README.md").split("\n"); const heading = readmeLines.findIndex((line) => /^##\s+Command map\s*$/.test(line)); if (heading < 0) { fail("README.md: missing the `## Command map` heading that check 2 reads the command table from"); return; } const after = readmeLines.slice(heading + 1); const end = after.findIndex((line) => /^##\s/.test(line)); const readmeVerbs = new Set(); for (const line of end < 0 ? after : after.slice(0, end)) { if (!line.trimStart().startsWith("|")) continue; for (const span of line.match(/`[^`]+`/g) ?? []) { const verb = verbOf(span.slice(1, -1).trim()); if (verb) readmeVerbs.add(verb); } } const commandBlock = blockLines("POM.yml", "commands"); if (!commandBlock) { fail("POM.yml: missing the top-level `commands:` block that check 2 reads the command map from"); return; } const manifestVerbs = new Set(); for (const entry of commandBlock) { const value = /:\s*(.+)$/.exec(entry); if (!value) continue; const verb = verbOf(value[1].trim().replace(/^["']|["']$/g, "")); if (verb) manifestVerbs.add(verb); } if (helpVerbs.size === 0) fail("src/command.ts: HELP advertises no `/pom` verbs"); if (readmeVerbs.size === 0) fail("README.md: the `## Command map` table lists no `/pom` commands"); if (manifestVerbs.size === 0) fail("POM.yml: the `commands:` block lists no `/pom` commands"); const sources = [ ["src/command.ts HELP", helpVerbs], ["README.md `## Command map`", readmeVerbs], ["POM.yml `commands:`", manifestVerbs], ]; const union = [...new Set(sources.flatMap(([, verbs]) => [...verbs]))].sort(); for (const verb of union) { const missing = sources.filter(([, verbs]) => !verbs.has(verb)).map(([label]) => label); if (missing.length === 0) continue; if (KNOWN_COMMAND_DIVERGENCES.has(verb)) { note(`/pom ${verb} is absent from ${missing.join(", ")}; allowlisted in KNOWN_COMMAND_DIVERGENCES`); continue; } const present = sources.filter(([, verbs]) => verbs.has(verb)).map(([label]) => label); fail( `/pom ${verb} is documented in ${present.join(", ")} but absent from ${missing.join(", ")}; ` + "add it there or record it in KNOWN_COMMAND_DIVERGENCES in scripts/check-docs.mjs", ); } for (const verb of KNOWN_COMMAND_DIVERGENCES) { if (!union.includes(verb)) fail( `KNOWN_COMMAND_DIVERGENCES lists "${verb}" but no source documents /pom ${verb}; ` + "remove the stale allowlist entry from scripts/check-docs.mjs", ); } }), ); // 3. Layout contract: the generated project tree names in docs must match src/persistence.ts. checks.push((index) => check(index, "layout-contract", (fail) => { for (const rel of LAYOUT_DOCS) { if (!existsSync(join(root, rel))) { fail(`${rel}: layout documentation is missing`); continue; } const body = text(rel); for (const name of FORBIDDEN_LAYOUT_NAMES) { const lines = lineNumbers(body, new RegExp(`\\b${name}\\b`)); if (lines.length > 0) fail( `${rel}:${lines.join(",")} names "${name}", which src/persistence.ts bootstrapProject never ` + `creates; the canonical names are ${REQUIRED_LAYOUT_NAMES.join(" and ")}`, ); } } const inventory = text("docs/INVENTORY.md"); for (const name of REQUIRED_LAYOUT_NAMES) { if (!new RegExp(`\\b${name}\\b`).test(inventory)) fail(`docs/INVENTORY.md: must document the canonical generated directory "${name}"`); } }), ); // 4. Prompt-order contract: POM.yml must mirror the composition order in src/prompts.ts. checks.push((index) => check(index, "prompt-order-contract", (fail) => { const items = blockLines("POM.yml", "prompt_precedence"); if (!items) { fail("POM.yml: missing the `prompt_precedence:` list under `knowledge:`"); return; } const entries = items.filter((item) => item.startsWith("- ")).map((item) => item.slice(2).trim()); if (entries.length < 3) { fail(`POM.yml: \`prompt_precedence\` must list the full composition order, found ${entries.length} entries`); return; } const first = entries[0]; const last = entries[entries.length - 1]; if (!/immutable/i.test(first) || !/law/i.test(first)) fail( `POM.yml: \`prompt_precedence\` entry 1 must name the immutable POM production law that ` + `composePromptStack prepends, found "${first}"`, ); if (!/executable stage contract/i.test(last)) fail( `POM.yml: \`prompt_precedence\` must end with the executable stage contract appended by ` + `renderStagePrompt, found "${last}" as the final entry`, ); const misplaced = entries .slice(0, -1) .map((entry, position) => (/executable stage contract/i.test(entry) ? position + 1 : 0)) .filter(Boolean); if (misplaced.length > 0) fail( `POM.yml: \`prompt_precedence\` mentions the executable stage contract at entry ` + `${misplaced.join(",")}; renderStagePrompt appends it last, so it must be the final entry only`, ); if (!entries.some((entry) => /replac/i.test(entry) && /\bID\b/i.test(entry))) fail( "POM.yml: `prompt_precedence` must state that project-local fragments replace bundled fragments " + "by matching basename ID, as src/prompts.ts composePromptStack does", ); }), ); // 5. Link/orphan check: relative doc links must resolve, and every page must be reachable. checks.push((index) => check(index, "link-and-orphan", (fail) => { const pages = readdirSync(join(root, "docs")) .filter((name) => name.endsWith(".md")) .sort(); if (pages.length === 0) { fail("docs: contains no Markdown pages"); return; } const reachable = new Set(); for (const page of pages) { const rel = `docs/${page}`; const body = withoutCode(text(rel)); for (const match of body.matchAll(MARKDOWN_LINK)) { const target = match[1]; if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("/")) continue; const path = target.split("#")[0].split("?")[0]; if (path === "") continue; const resolved = resolve(join(root, "docs"), path); if (NAVIGATION_PAGES.includes(page)) reachable.add(resolved); if (!existsSync(resolved)) fail( `${rel}: broken relative link \`${target}\` resolves to ${relative(root, resolved)}, ` + "which does not exist on disk", ); } } for (const page of pages) { if (page === "index.md") continue; if (reachable.has(resolve(join(root, "docs", page)))) continue; fail( `docs/${page}: unreachable page, no link from ${NAVIGATION_PAGES.map((name) => `docs/${name}`).join(" or ")}; ` + "add it to the navigation tables so the wiki has no orphans", ); } }), ); // 6. Release-evidence freshness: verification gates must be evidence objects, never literals. checks.push((index) => check(index, "release-evidence", (fail) => { const release = JSON.parse(text("RELEASE.json")); const verification = release.verification; if (verification === undefined || verification === null || typeof verification !== "object" || Array.isArray(verification)) { fail("RELEASE.json: `verification` must be an object mapping each gate to an evidence object"); return; } const gates = Object.entries(verification); if (gates.length === 0) { fail("RELEASE.json: `verification` lists no gates"); return; } for (const [gate, value] of gates) { if (typeof value !== "object" || value === null || Array.isArray(value)) { fail( `RELEASE.json: verification.${gate} is the bare ${typeof value} ${JSON.stringify(value)}; every gate must ` + `be an evidence object carrying a \`status\` of ${VERIFICATION_STATUSES.join(" | ")}, never a hard-coded literal`, ); continue; } if (typeof value.status !== "string") { fail(`RELEASE.json: verification.${gate} is missing a string \`status\` field`); continue; } if (!VERIFICATION_STATUSES.includes(value.status)) fail( `RELEASE.json: verification.${gate}.status is "${value.status}"; allowed values are ` + `${VERIFICATION_STATUSES.join(" | ")}`, ); } }), ); // 7. Package metadata: canonical coordinates must stay resolved and stay on owned infrastructure. checks.push((index) => check(index, "package-metadata", (fail, note) => { const pkg = JSON.parse(text("package.json")); for (const field of METADATA_FIELDS) { const value = pkg[field]; if (value === undefined) { fail(`package.json: \`${field}\` is missing; canonical project coordinates are required`); continue; } const urls = []; if (typeof value === "string") urls.push(value); else if (typeof value === "object" && value !== null) { if (typeof value.url === "string") urls.push(value.url); else fail(`package.json: \`${field}\` is an object without a string \`url\``); } else { fail(`package.json: \`${field}\` must be a string or an object with a \`url\``); continue; } for (const url of urls) { if (url.includes(PLACEHOLDER_MARKER)) { fail( `package.json: \`${field}\` still carries the ${PLACEHOLDER_MARKER} placeholder "${url}"; ` + "set a real canonical URL before publishing", ); continue; } let host; try { host = new URL(url.replace(/^git\+/, "")).host; } catch { fail(`package.json: \`${field}\` value "${url}" is not a parseable absolute URL`); continue; } if (host !== CANONICAL_HOST && !host.endsWith(`.${CANONICAL_HOST}`)) fail( `package.json: \`${field}\` points at "${host}", which is outside the canonical ${CANONICAL_HOST} ` + "infrastructure; no public forge mirror has been confirmed for this package", ); } } const repository = typeof pkg.repository?.url === "string" ? pkg.repository.url : ""; if (repository.includes(CANONICAL_HOST)) note(`${repository} is reserved on owned infrastructure and is not yet serving Git`); }), ); checks.forEach((run, position) => run(position + 1)); if (failed.length === 0) { console.log("POM docs verification PASS"); } else { console.log(`POM docs verification FAIL (${failed.length}/${checks.length}: ${failed.join(", ")})`); process.exitCode = 1; }