"use strict";
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..");
const read = (f) => fs.readFileSync(path.join(root, f), "utf8");
const GUIDE_GLOB_DIR = "docs-site/src/content/docs/guides";
const EXTRA_GUIDE_PAGES = ["docs-site/src/content/docs/quick-start.mdx"];
const CORPUS_STATIC_FILES = ["demo/setup.sh"];
const CORPUS_TAPE_DIR = "demo";
const ALLOWLIST = {
exec: "run-a-command wrapper; tests/cli.rs (agent-exec tape covers the scoped variant)",
"agent init": "agent grant onboarding; tests/cli.rs (multi-identity flow doesn't render as one tape)",
"agent connect": "MCP editor wiring; tests/cli.rs",
"agent grant": "grant creation; tests/cli.rs",
"agent ls": "grant listing; tests/cli.rs",
"agent revoke": "grant revocation; tests/cli.rs",
describe: "key-description metadata; tests/cli.rs",
"policy set": "agent policy write; tests/cli.rs",
mcp: "long-running stdio server; tests/mcp_interop.rs (no finite tape)",
diff: "git-ref secret diff; tests/cli.rs",
"setup-merge-driver": "one-time git config; tests/cli.rs",
import: "reverse of export; tests/cli.rs + tests/adversarial.rs",
};
function loadValidPaths() {
const ref = read("docs/cli-reference.md");
const paths = new Set();
const re = /\* \[`murk([^`↴]*)`/g;
let m;
while ((m = re.exec(ref)) !== null) {
const rest = m[1].trim(); paths.add(rest);
}
if (paths.size <= 1) {
console.error("::error::could not parse commands from docs/cli-reference.md");
process.exit(1);
}
return paths;
}
function resolvePath(tokens, validPaths) {
const words = tokens.filter((t) => /^[a-z][a-z-]*$/.test(t)); if (words.length === 0) return ""; for (let depth = Math.min(2, words.length); depth >= 1; depth--) {
const candidate = words.slice(0, depth).join(" ");
if (validPaths.has(candidate)) return candidate;
}
return null; }
function* murkInvocations(text) {
const normalized = text
.split("\n")
.map((line) => line.replace(/(^|\s)#.*$/, "$1"))
.join("\n")
.replace(/\$\(MURK\)/g, "murk");
const re = /(?<![\w./-])murk[ \t]+([^\n|&;`"'()<>]*)/g;
let m;
while ((m = re.exec(normalized)) !== null) {
yield m[1].trim().split(/\s+/).filter(Boolean);
}
}
function* fencedBlocks(md) {
const re = /```[^\n]*\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(md)) !== null) yield m[1];
}
function guidePages() {
const dir = path.join(root, GUIDE_GLOB_DIR);
const pages = fs
.readdirSync(dir)
.filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
.map((f) => path.join(GUIDE_GLOB_DIR, f));
return [...pages, ...EXTRA_GUIDE_PAGES];
}
function tapeFiles() {
const dir = path.join(root, CORPUS_TAPE_DIR);
return fs
.readdirSync(dir)
.filter((f) => f.endsWith(".tape"))
.map((f) => path.join(CORPUS_TAPE_DIR, f));
}
function makefileCorpus() {
const mk = read("Makefile");
const demoLine = mk.match(/^test-demos:[^\n]*/m);
if (!demoLine) {
console.error("::error::could not find the test-demos target in the Makefile");
process.exit(1);
}
const targets = (demoLine[0].match(/test-[a-z-]+/g) || []).filter(
(t) => t !== "test-demos",
);
const bodies = [];
for (const t of targets) {
const re = new RegExp(`^${t}:[^\\n]*\\n((?:\\t[^\\n]*\\n?)+)`, "m");
const m = mk.match(re);
if (m) bodies.push(m[1]);
}
return bodies.join("\n");
}
const validPaths = loadValidPaths();
const covered = new Set();
const corpusTexts = [
...[...CORPUS_STATIC_FILES, ...tapeFiles()].map(read),
makefileCorpus(),
];
for (const text of corpusTexts) {
for (const tokens of murkInvocations(text)) {
const p = resolvePath(tokens, validPaths);
if (p) covered.add(p);
}
}
const unknown = []; const uncovered = []; const documented = new Set();
for (const page of guidePages()) {
const md = read(page);
for (const block of fencedBlocks(md)) {
for (const tokens of murkInvocations(block)) {
const p = resolvePath(tokens, validPaths);
if (p === "") continue; if (p === null) {
unknown.push({ page, cmd: `murk ${tokens.join(" ")}` });
continue;
}
documented.add(p);
if (!covered.has(p) && !(p in ALLOWLIST)) uncovered.push({ page, cmd: p });
}
}
}
const staleAllow = Object.keys(ALLOWLIST).filter(
(p) => !documented.has(p) || covered.has(p),
);
let failed = false;
if (unknown.length) {
failed = true;
console.error("::error::guide examples reference commands that do not exist in the murk CLI:");
for (const u of unknown) console.error(` ${u.cmd} (${u.page})`);
}
if (uncovered.length) {
failed = true;
const seen = new Set();
console.error(
"::error::guide examples use commands not exercised by any demo/test flow (add a demo, or add to ALLOWLIST with a reason):",
);
for (const u of uncovered) {
if (seen.has(u.cmd)) continue;
seen.add(u.cmd);
console.error(` murk ${u.cmd}`);
}
}
if (staleAllow.length) {
failed = true;
console.error("::error::stale ALLOWLIST entries — no longer documented or now demo-covered, remove them:");
for (const p of staleAllow) console.error(` murk ${p}`);
}
if (failed) process.exit(1);
console.log(
`OK: ${documented.size} guide command(s) all resolve to real CLI commands and map to a tested flow` +
(Object.keys(ALLOWLIST).length ? ` (${Object.keys(ALLOWLIST).length} allowlisted)` : ""),
);