Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"check:env-parity": "node scripts/check-env-parity.mjs",
"sweep:branch-ledger": "node scripts/sweep-branch-ledger.mjs",
"docs:check-links": "node scripts/check-docs-links.mjs",
"docs:check-scripts": "node scripts/check-docs-script-refs.mjs",
"sitemap:update": "node scripts/run-tsx.mjs scripts/generate-site-map.ts",
"sitemap:check": "node scripts/run-tsx.mjs scripts/generate-site-map.ts --check",
"brand:update": "node scripts/run-tsx.mjs scripts/generate-brand-assets.ts",
Expand Down
117 changes: 117 additions & 0 deletions scripts/check-docs-script-refs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env node
/**
* check-docs-script-refs.mjs — verify that every `npm run <script>` mentioned in
* the maintained docs corresponds to a real script in package.json.
*
* check-docs-links.mjs validates file PATHS referenced in docs, but nothing checks
* the hundreds of `npm run <script>` references — so a renamed/removed script leaves
* stale instructions that the agents (Codex/Claude/Cursor) then follow. This closes
* that gap.
*
* Only references inside inline code spans (`npm run x`) and fenced code blocks are
* scanned, so prose like "npm run the build" is never misread. Placeholder tokens
* (containing <…>) and an explicit allowlist are skipped.
*
* Scans README.md, AGENTS.md, and docs/**\/*.md excluding docs/archive, docs/audit,
* and dated point-in-time filenames (historical records). Pass --all to include them.
*
* Advisory: run `npm run docs:check-scripts` before doc handoffs. Deliberately NOT
* in verify:cheap/CI so historical docs cannot block unrelated PRs.
*/
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const scanAll = process.argv.includes("--all");

const DATED_DOC = /\b20\d{2}-\d{2}(-\d{2})?\b/;
const HISTORICAL_DIRS = new Set(["archive", "audit"]);

// Script tokens that appear in docs as illustrative placeholders, or real scripts
// that were renamed but are legitimately referenced in historical records (e.g. the
// branch-review-ledger "Checks" cells record what was run at the time).
const ALLOWLIST = new Set([
"<script>",
"<name>",
"your-script",
"test:e2e:advisory", // renamed to test:e2e:regression (2026-07); kept for historical ledger accuracy
]);
Comment on lines +34 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope the retired-script exception to its historical record.

Line 38 suppresses test:e2e:advisory in every scanned file, so a newly added active-doc reference to that removed script passes validation. Keep placeholder tokens global, but apply this exception only to docs/branch-review-ledger.md (or exclude that record separately) and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-docs-script-refs.mjs` around lines 34 - 39, Update the
ALLOWLIST handling in the documentation reference checker so test:e2e:advisory
is ignored only when scanning docs/branch-review-ledger.md, while placeholder
tokens remain globally allowed. Add a regression test proving active-document
references to the retired script still fail validation outside that historical
ledger.


/** Script names defined in package.json. */
export function parsePackageScripts(pkgJsonText) {
const pkg = JSON.parse(pkgJsonText);
return new Set(Object.keys(pkg.scripts ?? {}));
}

/**
* Extract `npm run <script>` tokens that appear inside inline code spans or fenced
* code blocks. Returns the unique script names referenced. `pnpm run` / `yarn run`
* are matched too for completeness.
*/
export function extractScriptRefs(markdown) {
const codeRegions = [];
for (const m of markdown.matchAll(/```[\s\S]*?```/g)) codeRegions.push(m[0]);
for (const m of markdown.matchAll(/`[^`\n]+`/g)) codeRegions.push(m[0]);

const names = new Set();
for (const region of codeRegions) {
for (const m of region.matchAll(/\b(?:npm|pnpm|yarn)\s+run\s+([A-Za-z0-9][A-Za-z0-9:_-]*)/g)) {
names.add(m[1]);
Comment on lines +59 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## scripts/check-docs-script-refs.mjs\n'
ast-grep outline scripts/check-docs-script-refs.mjs --view expanded || true
printf '\n## relevant lines\n'
nl -ba scripts/check-docs-script-refs.mjs | sed -n '1,180p'
printf '\n## search for parsePackageScripts and related tests\n'
rg -n "parsePackageScripts|check-docs-script-refs|lint\.fix|npm\\s+run|pnpm\\s+run|yarn\\s+run" -S .

Repository: BigSimmo/Database

Length of output: 898


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## scripts/check-docs-script-refs.mjs (relevant section)\n'
sed -n '40,80p' scripts/check-docs-script-refs.mjs

printf '\n## tests and references\n'
rg -n "extractScriptRefs|parsePackageScripts|findStaleRefs|lint\\.fix|npm\\s+run|pnpm\\s+run|yarn\\s+run" -S . --glob '!**/node_modules/**'

Repository: BigSimmo/Database

Length of output: 50376


Allow dotted npm script names in docs refs. scripts/check-docs-script-refs.mjs:59-60 skips names like lint.fix, so stale references to valid package scripts are missed. Add . to the token class and cover it with a regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-docs-script-refs.mjs` around lines 59 - 60, Update the
script-name pattern in the region reference scanning loop to include dots in
matched npm, pnpm, and yarn script names, preserving the existing allowed
characters. Add a regression test covering a dotted script name such as lint.fix
and verify it is detected in documentation references.

}
}
return [...names];
}

/** Referenced script names that are neither defined nor allowlisted. */
export function findStaleRefs(refs, validScripts, allowlist = ALLOWLIST) {
return refs.filter((name) => !validScripts.has(name) && !allowlist.has(name) && !name.includes("<"));
}

function collectDocs(dirRelative, targets) {
for (const entry of readdirSync(path.join(repoRoot, dirRelative), { withFileTypes: true })) {
const entryRelative = path.posix.join(dirRelative, entry.name);
if (entry.isDirectory()) {
if (HISTORICAL_DIRS.has(entry.name) && !scanAll) continue;
collectDocs(entryRelative, targets);
continue;
}
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
if (!scanAll && DATED_DOC.test(entry.name)) continue;
targets.push(entryRelative);
}
}

function main() {
const validScripts = parsePackageScripts(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
const targets = ["README.md", "AGENTS.md"];
collectDocs("docs", targets);

let stale = 0;
let checked = 0;
for (const target of targets) {
let markdown;
try {
markdown = readFileSync(path.join(repoRoot, target), "utf8");
} catch {
continue;
}
const refs = extractScriptRefs(markdown);
checked += refs.length;
const bad = findStaleRefs(refs, validScripts);
if (bad.length > 0) {
stale += bad.length;
console.error(`\n${target}:`);
for (const name of bad) console.error(` STALE npm run ${name} (no such script in package.json)`);
}
}

if (stale > 0) {
console.error(`\ndocs script-ref check FAILED: ${stale} stale reference(s) across ${checked} checked.`);
process.exit(1);
}
console.log(`docs script-ref check passed: ${checked} npm-run reference(s) resolve to real scripts.`);
}

const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (invokedDirectly) main();
40 changes: 40 additions & 0 deletions tests/docs-script-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { extractScriptRefs, findStaleRefs, parsePackageScripts } from "../scripts/check-docs-script-refs.mjs";

describe("parsePackageScripts", () => {
it("returns the set of script names", () => {
const set = parsePackageScripts(JSON.stringify({ scripts: { build: "x", "verify:cheap": "y" } }));
expect(set.has("build")).toBe(true);
expect(set.has("verify:cheap")).toBe(true);
expect(set.size).toBe(2);
});
});

describe("extractScriptRefs", () => {
it("extracts npm run tokens from inline code spans and fenced blocks", () => {
const md = ["Run `npm run verify:cheap` first.", "```bash", "npm run build", "pnpm run lint", "```"].join("\n");
const refs = extractScriptRefs(md);
expect(refs).toEqual(expect.arrayContaining(["verify:cheap", "build", "lint"]));
});

it("ignores npm run mentions in prose (outside code)", () => {
expect(extractScriptRefs("just npm run the thing normally")).toEqual([]);
});

it("captures the script name with flags following it", () => {
expect(extractScriptRefs("`npm run eval:quality -- --rag-only`")).toEqual(["eval:quality"]);
});
});

describe("findStaleRefs", () => {
const valid = new Set(["build", "verify:cheap"]);
it("flags references with no matching script", () => {
expect(findStaleRefs(["build", "verify:gone"], valid)).toEqual(["verify:gone"]);
});
it("skips allowlisted and placeholder tokens", () => {
expect(findStaleRefs(["<script>", "your-script"], valid)).toEqual([]);
});
it("returns empty when everything resolves", () => {
expect(findStaleRefs(["build", "verify:cheap"], valid)).toEqual([]);
});
});