From c6075afa212ff0364bd47c97f2e03fe05e6b7531 Mon Sep 17 00:00:00 2001 From: Jussi Rajala Date: Mon, 12 Oct 2026 15:28:34 +0300 Subject: [PATCH 1/2] docs(rdd-audit): name the CLI's citation audit and retire audit-citations.mjs The citation audit that rdd-audit prescribes, and that rdd-cold-review and rdd-completion-review reach through it, moves into the `modernpath` CLI as `modernpath audit citations` (ModernPath decision USER:2026-10-09:citation-audit-in-cli and :retire-audit-citations-mjs). A customer session on Windows had no Node.js to run the script, the skill copy installed under .claude/skills named a script that is not installed beside it, the script reported nearly every citation missing on Windows, and its elided-path pattern never matched. - rdd-audit's citation check names `modernpath audit citations`, with the same arguments, grammar, exit statuses and report. - rdd-audit states what to do when the audit cannot run: keep the error, read each citation the review relies on, and report the check as not run, never as passed; a reviewer without a shell gets the run, or the error, from the agent that runs it. - The coverage reference says "the citation audit it names" instead of the supplied script; its pinned wording is unchanged. - skills/rdd-audit/audit-citations.mjs and its test are removed. Co-Authored-By: Claude Opus 5.5 --- skills/rdd-audit/SKILL.md | 16 +- skills/rdd-audit/audit-citations.mjs | 250 ------------------ skills/rdd-audit/audit-citations.test.mjs | 213 --------------- .../references/coverage.md | 2 +- 4 files changed, 13 insertions(+), 468 deletions(-) delete mode 100755 skills/rdd-audit/audit-citations.mjs delete mode 100644 skills/rdd-audit/audit-citations.test.mjs diff --git a/skills/rdd-audit/SKILL.md b/skills/rdd-audit/SKILL.md index cc4c552..8baccf9 100644 --- a/skills/rdd-audit/SKILL.md +++ b/skills/rdd-audit/SKILL.md @@ -30,12 +30,13 @@ prove semantic coverage or authorize a lifecycle transition. ## Check citations -Use `audit-citations.mjs` beside this skill for local Markdown citations. Pass -the actual document roots and source repositories explicitly. Substitute the -paths and repository keys in this command: +Use `modernpath audit citations`, the citation audit the `modernpath` CLI +ships, for local Markdown citations. Pass the actual document roots and source +repositories explicitly. Substitute the paths and repository keys in this +command: ```text -node /audit-citations.mjs --repository== +modernpath audit citations --repository== ``` Repeat `--repository` for multiple repositories. Git worktrees and explicitly @@ -76,6 +77,13 @@ skip. The default minimum is one; `--min` is a citation-count guard against an empty or incomplete scan, not an extraction-coverage threshold. Establish an expected citation count independently before using a higher minimum. +If the audit cannot run — an exit `2` you cannot correct, or a CLI without the +citation audit — keep the error, read each citation the review relies on, and +report the citation check as not run, with the error. Never report it as passed. +When the agent that writes the review cannot run commands, the agent that runs +the audit passes it the command line, exit status and output, or the error; +the reviewer lists the citation check as not run when the audit did not run. + ## Verify meaning and measurement Open each suspected defect and check the full relevant expression, declaration, diff --git a/skills/rdd-audit/audit-citations.mjs b/skills/rdd-audit/audit-citations.mjs deleted file mode 100755 index c15d2b0..0000000 --- a/skills/rdd-audit/audit-citations.mjs +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env node -// Resolve cited files in explicitly selected Markdown documents. -// Usage: node audit-citations.mjs --repository== -// Exits 0 for a passing check, 1 for findings or insufficient citations, -// and 2 for missing document targets, invalid source inventory, or refused prompt targets. - -import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from "node:fs"; -import { join, resolve, relative, basename, isAbsolute } from "node:path"; - -// Unknown prefixed source extensions are reported, not silently skipped. -const EXT = "heex|leex|eex|exs|tsx|yaml|proto|json|xslt|xml|jsp|xsl|mjs|cjs|ex|go|js|ts|yml|sh|py|rb|rs|java|kt|toml|sql|cs|vb|fs|php|swift|scala|erl"; - -// Consume optional navigation suffixes, but validate only the cited file. -const PREFIXED = new RegExp(`(?:CODE|TEST): ?((?:[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+:)?[A-Za-z0-9_./\\[\\]\\-]+?\\.(?:${EXT}))(?!\\.?[A-Za-z0-9])(?::([^\\s\\x60<>|;)]*))?`, "g"); -const UNKNOWN_PREFIX = /(?:CODE|TEST): ?[^\s`<>|;)]+/g; -// Bare path:locator citations also resolve only the file. -const BARE = new RegExp("`([A-Za-z0-9_./\\[\\]\\-]+\\.(?:" + EXT + ")):([^\\s`<>|;)]*)", "g"); -const DOCREF = /DOC: ?([A-Za-z0-9_./\-]+\.md)(?:#([^\s)|`\u00b7]+))?/g; -const ELIDED = /(?:CODE:|`)(?:[A-Za-z0-9_.\\[\\]\-]+\/)*\.\.\.\/[A-Za-z0-9_.\/\\[\\]\-]*\.(?:exs|tsx|ex|go|js|ts|py|rb|rs|java|kt|sql|yml|yaml|json|sh|heex)\b/g; - -const roots = process.argv.slice(2).filter((a) => !a.startsWith("--")); -// The minimum is a caller-supplied citation count, not semantic coverage. -const minArg = process.argv.find((a) => a.startsWith("--min=")); -const minCitations = minArg ? Math.max(1, parseInt(minArg.slice(6), 10) || 1) : 1; -const targets = roots.length ? roots : ["docs", ...(existsSync("ARCHITECTURE.md") ? ["ARCHITECTURE.md"] : [])]; - -// Prompt corpora contain deliberate examples; auditing them requires opt-in. -const prompts = targets.filter((t) => { - const p = t.replace(/^\.\//, ""); - return p.startsWith(".claude") || p.startsWith(".modernpath/rdd/skills"); -}); -if (prompts.length) { - console.error( - `refusing to audit ${prompts.join(", ")} — installed skills are teaching material, and their\n` + - "citations may be deliberate examples rather than claims about this checkout.\n" + - "Pass --force-prompts only if you intend to read every hit before acting.", - ); - if (!process.argv.includes("--force-prompts")) process.exit(2); -} - -// The parent workspace need not be Git. A declared root is one repository; -// never silently discover and mix neighboring checkouts into the source scope. -const declared = process.argv.filter(a => a.startsWith("--repository=")); -const repositories = []; -const SKIP = new Set([".git", "node_modules", ".elixir_ls", "tmp", "coverage"]); -function walk(dir, out) { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if ((entry.name.startsWith(".") && entry.name !== ".modernpath") || SKIP.has(entry.name) || entry.isSymbolicLink()) continue; - const path = join(dir, entry.name); - if (entry.isDirectory()) walk(path, out); - else if (entry.isFile()) out.push(path); - } - return out; -} -function inside(root, file) { - const path = relative(root, file); - return path !== ".." && !path.startsWith("../") && !isAbsolute(path); -} -try { - const specs = declared.length ? declared.map(a => a.slice("--repository=".length)) : [`${basename(process.cwd())}=${process.cwd()}`]; - for (const spec of specs) { - const separator = spec.indexOf("="); - const key = spec.slice(0, separator); - if (separator < 1 || !/^[A-Za-z0-9_.-]+$/.test(key)) throw new Error("expected --repository=key=directory"); - let root = realpathSync(spec.slice(separator + 1)); - if (!declared.length && !existsSync(join(root, ".git"))) { - root = realpathSync(execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim()); - } - if (!statSync(root).isDirectory() || repositories.some(r => r.key === key || r.root === root)) throw new Error("repository keys and roots must be distinct directories"); - let revision = "unversioned"; - let files; - let committedFiles; - if (existsSync(join(root, ".git"))) { - const git = (...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", maxBuffer: 64 << 20, stdio: ["ignore", "pipe", "pipe"] }); - revision = git("rev-parse", "HEAD").trim(); - committedFiles = new Set(git("ls-tree", "-r", "--name-only", "-z", revision).split("\0").filter(Boolean)); - files = git("ls-files", "-z", "--cached", "--others", "--exclude-standard").split("\0").filter(Boolean).map(path => join(root, path)); - } else { - if (!declared.length) throw new Error("not a Git repository; declare every source root with --repository=key=directory"); - files = walk(root, []); - } - files = [...new Set(files.filter(path => existsSync(path) && statSync(path).isFile() && inside(root, realpathSync(path))))]; - repositories.push({ key, root, revision, files, committedFiles }); - } -} catch (error) { - console.error(`source inventory refused: ${error.message}`); - process.exit(2); -} - -const resolutionErrors = new Map(); -// A path suffix with multiple matches is ambiguous. Typed references resolve -// only exact paths at the declared revision. -function candidates(path, exactOnly = false) { - const typed = path.match(/^([A-Za-z0-9_.-]+)@([A-Za-z0-9_.-]+):(.+)$/); - let found; - if (typed) { - const repo = repositories.find(r => r.key === typed[1]); - if (!repo || repo.revision !== typed[2]) { - resolutionErrors.set(path, "unknown repository or revision differs from the declared checkout"); - return []; - } - const exact = resolve(repo.root, typed[3]); - if (!inside(repo.root, exact)) return []; - if (repo.committedFiles) { - if (!repo.committedFiles.has(relative(repo.root, exact))) { - resolutionErrors.set(path, "no such file at the cited revision"); - return []; - } - found = [exact]; - } else { - found = repo.files.includes(exact) ? [exact] : []; - } - } else { - const exact = repositories.flatMap(repo => repo.files.filter(file => relative(repo.root, file) === path)); - found = exact.length || exactOnly ? exact : repositories.flatMap(repo => repo.files.filter(file => file.endsWith("/" + path))); - } - found = [...new Set(found)]; - if (found.length > 1) { - resolutionErrors.set(path, `ambiguous path (${found.length} matches); name repository@revision:exact/path`); - return []; - } - return found; -} - -const missingTargets = targets.filter(target => !existsSync(target)); -if (missingTargets.length) { - console.error(`document inventory refused: missing target(s): ${missingTargets.join(", ")}`); - process.exit(2); -} - -function markdownFiles(target) { - if (statSync(target).isFile()) return target.endsWith(".md") ? [target] : []; - return readdirSync(target, { withFileTypes: true }).flatMap((e) => - e.isDirectory() ? markdownFiles(join(target, e.name)) - : e.name.endsWith(".md") ? [join(target, e.name)] : []); -} - -const docs = targets.flatMap(markdownFiles).sort(); -let ok = 0; -let examples = 0; -const broken = []; -const elided = []; - -// Only explicitly marked teaching fences are exempt; ordinary code fences -// still contain auditable citations. -function exemptFenceRanges(lines) { - const ranges = []; - let open = null; - let offset = 0; - const starts = lines.map((l) => { const s = offset; offset += l.length + 1; return s; }); - for (let i = 0; i < lines.length; i++) { - if (!/^\s*```/.test(lines[i])) continue; - if (open === null) { - open = /example-citation/.test(lines[i]) ? starts[i] : -1; - } else { - if (open >= 0) ranges.push([open, starts[i] + lines[i].length]); - open = null; - } - } - return ranges; -} - -for (const doc of docs) { - const text = readFileSync(doc, "utf8"); - const lines = text.split("\n"); - const exemptRanges = exemptFenceRanges(lines); - const inExemptFence = (idx) => exemptRanges.some(([a, b]) => idx >= a && idx < b); - - for (const m of text.matchAll(ELIDED)) { - const line = text.slice(0, m.index).split("\n").length; - // Same per-line opt-out as `check()`: a document teaching the grammar has to - // SHOW a rejected form, and an elided path is one of them. - if (//.test(lines[line - 1] ?? "")) continue; - if (inExemptFence(m.index)) continue; - elided.push({ doc, line, snippet: lines[line - 1].trim().slice(0, 90) }); - } - - // Prefixed citations first; record their spans so a bare match inside one is - // not counted twice. - const spans = []; - for (const m of text.matchAll(PREFIXED)) { - spans.push([m.index, m.index + m[0].length]); - if (inExemptFence(m.index)) { examples++; continue; } - check(doc, text, m); - } - for (const m of text.matchAll(UNKNOWN_PREFIX)) { - if (spans.some(([a, b]) => m.index >= a && m.index < b) || inExemptFence(m.index)) continue; - const lineNo = text.slice(0, m.index).split("\n").length; - if (//.test(lines[lineNo - 1] ?? "")) continue; - const value = m[0].replace(/^(?:CODE|TEST): ?/, ""); - if (value.endsWith(".md")) { - if (candidates(value, true).length) ok++; - else broken.push({ doc, lineNo, path: value, why: "no such document" }); - continue; - } - // Directory references are orientation, not file/line claims. Their contents - // are inventoried separately; bare grammar examples are not source files. - if (value.endsWith("/") || !/^[A-Za-z0-9_./@:\[\]-]+\.[A-Za-z0-9]+(?::.*)?$/.test(value)) continue; - broken.push({ doc, lineNo, path: m[0], why: "unsupported or malformed source citation; not silently omitted" }); - } - for (const m of text.matchAll(BARE)) { - if (spans.some(([a, b]) => m.index >= a && m.index < b)) continue; - if (inExemptFence(m.index)) { examples++; continue; } - check(doc, text, m); - } - for (const m of text.matchAll(DOCREF)) { - const lineNo = text.slice(0, m.index).split("\n").length; - if (inExemptFence(m.index) || //.test(lines[lineNo - 1] ?? "")) { examples++; continue; } - const found = candidates(m[1], true); - if (!found.length) { broken.push({ doc, lineNo, path: m[1], why: resolutionErrors.get(m[1]) || "no such document" }); continue; } - ok++; - } -} - -function check(doc, text, m) { - const [, path] = m; - const lineNo = text.slice(0, m.index).split("\n").length; - - // Explicit teaching examples are excluded from the checked denominator. - if (//.test(text.split("\n")[lineNo - 1] ?? "")) { examples++; return; } - - const found = candidates(path); - if (!found.length) { - broken.push({ doc, lineNo, path, why: resolutionErrors.get(path) || "no such file" }); - return; - } - ok++; -} - -const total = ok + broken.length; -const belowMinimum = total < minCitations; -console.log(`${ok}/${total} citations resolve to files across ${docs.length} documents` + - (examples ? ` (${examples} teaching examples skipped; not checked citations)` : "")); -console.log("File resolution only; source contents and navigation suffixes are not checked."); - -if (elided.length) { - console.log(`\n${elided.length} elided path(s) — a citation that resolves to nothing:`); - for (const e of elided) console.log(` ${e.doc}:${e.line} ${e.snippet}`); -} -if (broken.length) { - console.log(`\n${broken.length} broken:`); - for (const b of broken) console.log(` ${b.doc}:${b.lineNo} ${b.path} — ${b.why}`); -} -if (belowMinimum) { - console.log(`\ninsufficient citations: ${total} checked; --min requires ${minCitations}`); -} -if (!elided.length && !broken.length && !belowMinimum) console.log("no elided paths"); - -process.exit(broken.length || elided.length || belowMinimum ? 1 : 0); diff --git a/skills/rdd-audit/audit-citations.test.mjs b/skills/rdd-audit/audit-citations.test.mjs deleted file mode 100644 index 6d99841..0000000 --- a/skills/rdd-audit/audit-citations.test.mjs +++ /dev/null @@ -1,213 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { execFileSync, spawnSync } from "node:child_process"; - -const script = fileURLToPath(new URL("./audit-citations.mjs", import.meta.url)); -function fixture(t) { - const root = mkdtempSync(join(tmpdir(), "rdd-audit-")); - t.after(() => rmSync(root, { recursive: true, force: true })); - const file = (path, content) => { const target = join(root, path); mkdirSync(join(target, ".."), { recursive: true }); writeFileSync(target, content); return target; }; - const run = (...args) => spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: "utf8" }); - return { root, file, run }; -} - -test("declared non-Git repositories resolve XML/JSP/XSL/XSLT with exact typed paths", t => { - const f = fixture(t); - for (const extension of ["xml", "jsp", "xsl", "xslt"]) f.file(`legacy/src/catalog.${extension}`, "one\ntwo\n"); - f.file("report.md", ["xml", "jsp", "xsl", "xslt"].map(ext => `CODE:legacy@unversioned:src/catalog.${ext}:2`).join("\n")); - const result = f.run(`--repository=legacy=${join(f.root, "legacy")}`, "report.md"); - assert.equal(result.status, 0, result.stdout + result.stderr); - assert.match(result.stdout, /4\/4 citations resolve/); -}); - -test("ambiguous suffixes across repositories fail even when one candidate is long enough", t => { - const f = fixture(t); - f.file("a/src/catalog.xml", "one\n"); - f.file("b/src/catalog.xml", "one\ntwo\nthree\n"); - f.file("report.md", "CODE:catalog.xml:3"); - const result = f.run(`--repository=a=${join(f.root, "a")}`, `--repository=b=${join(f.root, "b")}`, "report.md"); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /ambiguous/); -}); - -test("exact root paths win over suffixes and missing documents never select a namesake", t => { - const f = fixture(t); - f.file("repo/AGENTS.md", "# Root"); - f.file("repo/nested/AGENTS.md", "# Nested"); - f.file("repo/nested/BACKLOG.md", "# Other backlog"); - f.file("repo/check.mjs", "export {};\n"); - f.file("report.md", "DOC:AGENTS.md\nCODE:check.mjs:1"); - const args = [`--repository=repo=${join(f.root, "repo")}`, "report.md"]; - assert.equal(f.run(...args).status, 0); - f.file("report.md", "DOC:BACKLOG.md"); - const result = f.run(...args); - assert.equal(result.status, 1); - assert.match(result.stdout, /no such document/); -}); - -test("Git worktrees declared from a non-Git parent preserve revision identity", t => { - const f = fixture(t); - const repo = join(f.root, "repo"); - f.file("repo/code.xml", "one\n"); - const git = (...args) => execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); - git("init"); git("add", "."); git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "fixture"); - const revision = git("rev-parse", "HEAD").trim(); - const worktree = join(f.root, "checkout"); - git("worktree", "add", "--detach", worktree); - f.file("report.md", `CODE:legacy@${revision}:code.xml:1`); - assert.equal(f.run(`--repository=legacy=${worktree}`, "report.md").status, 0); - f.file("report.md", `CODE:legacy@${"a".repeat(40)}:code.xml:1`); - const wrong = f.run(`--repository=legacy=${worktree}`, "report.md"); - assert.equal(wrong.status, 1, wrong.stdout + wrong.stderr); - assert.match(wrong.stdout, /revision/); -}); - -test("a nonempty corpus with no recognized references is not a passing audit", t => { - const f = fixture(t); - f.file("repo/code.xml", "one"); - f.file("report.md", "No traceable citations were emitted."); - const result = f.run(`--repository=legacy=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /insufficient citations/); -}); - -test("the default single-repository audit still works from a nested directory", t => { - const f = fixture(t); - f.file("project/code.ex", "one\ntwo\n"); - f.file("project/report.md", "CODE:code.ex:2"); - const git = (...args) => execFileSync("git", ["-C", f.root, ...args], { stdio: ["ignore", "pipe", "pipe"] }); - git("init"); git("add", "."); git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "fixture"); - const result = spawnSync(process.execPath, [script, "report.md"], { cwd: join(f.root, "project"), encoding: "utf8" }); - assert.equal(result.status, 0, result.stdout + result.stderr); -}); - -test("unknown prefixed source extensions cannot disappear beside a valid citation", t => { - const f = fixture(t); - f.file("repo/code.xml", "one"); - f.file("report.md", "CODE:code.xml:1\nCODE:missing.unknown:1"); - const result = f.run(`--repository=legacy=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /missing.unknown/); -}); - -test("exempt teaching examples cannot satisfy the checked-citation minimum", t => { - const f = fixture(t); - f.file("repo/code.xml", "one"); - f.file("report.md", "CODE:missing.xml:2 \n"); - const args = [`--repository=legacy=${join(f.root, "repo")}`, "report.md"]; - const empty = f.run(...args); - assert.equal(empty.status, 1, empty.stdout + empty.stderr); - assert.match(empty.stdout, /0\/0 citations resolve/); - f.file("report.md", "CODE:missing.xml:2 \nCODE:code.xml:1\n"); - const mixed = f.run(...args); - assert.equal(mixed.status, 0, mixed.stdout + mixed.stderr); - assert.match(mixed.stdout, /1\/1 citations resolve/); -}); - -test("source citations resolve files regardless of navigation suffixes or contents", t => { - const f = fixture(t); - f.file("repo/subject.ex", "# REQ-EXAMPLE-001\n"); - f.file("repo/subject_test.go", "// REQ-EXAMPLE-001\n"); - f.file("report.md", [ - "CODE:subject.ex", - "CODE:subject.ex:validate/1", - "CODE:subject.ex:validator", - "CODE:subject.ex:Sample.Auth", - "CODE:subject.ex:900-999", - "CODE:subject.ex:0,2-1", - "TEST:subject_test.go", - "TEST:subject_test.go:TestParent/Child", - "`subject.ex:missing_symbol`", - ].join("\n")); - const result = f.run(`--repository=repo=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 0, result.stdout + result.stderr); - assert.match(result.stdout, /9\/9 citations resolve to files/); - assert.match(result.stdout, /source contents and navigation suffixes are not checked/); -}); - -test("navigation suffixes never rescue a missing source file", t => { - const f = fixture(t); - f.file("repo/subject.ex", "# REQ-EXAMPLE-001\n"); - f.file("report.md", "CODE:subject.ex\nCODE:missing.ex:validate/1\nTEST:missing_test.exs:TestParent/Child\n"); - const result = f.run(`--repository=repo=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /1\/3 citations resolve to files/); - assert.match(result.stdout, /missing.ex.*no such file/); - assert.match(result.stdout, /missing_test.exs.*no such file/); -}); - -test("absence wording does not excuse missing files", t => { - const f = fixture(t); - f.file("repo/subject.py", "# REQ-EXAMPLE-001\n"); - f.file("report.md", "CODE:subject.py\nMissing validation in `CODE:absent.py:1`.\n"); - const result = f.run(`--repository=repo=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /1\/2 citations resolve to files/); - assert.match(result.stdout, /absent.py.*no such file/); -}); - -test("plain gap descriptions are not supporting citations", t => { - const f = fixture(t); - f.file("repo/subject.py", "# REQ-EXAMPLE-001\n"); - f.file("report.md", "CODE:subject.py\nThere is no `test_missing.py`; verification remains unresolved.\n"); - const result = f.run(`--repository=repo=${join(f.root, "repo")}`, "report.md"); - assert.equal(result.status, 0, result.stdout + result.stderr); - assert.match(result.stdout, /1\/1 citations resolve to files/); -}); - -test("a missing document root cannot disappear beside a valid report", t => { - const f = fixture(t); - f.file("repo/subject.py", "# REQ-EXAMPLE-001\n"); - f.file("report.md", "CODE:subject.py\n"); - const result = f.run(`--repository=repo=${join(f.root, "repo")}`, "report.md", "missing-docs"); - assert.equal(result.status, 2, result.stdout + result.stderr); - assert.match(result.stderr, /missing-docs/); -}); - -test("revision-qualified files must exist in the commit regardless of working-tree changes", t => { - const f = fixture(t); - const repo = join(f.root, "repo"); - f.file("repo/subject.py", "# REQ-EXAMPLE-001\n"); - const git = (...args) => execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); - git("init"); git("add", "."); git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-m", "fixture"); - const revision = git("rev-parse", "HEAD").trim(); - f.file("repo/subject.py", "changed contents\n"); - f.file("repo/untracked.py", "new file\n"); - const args = [`--repository=repo=${repo}`, "report.md"]; - - f.file("report.md", `CODE:repo@${revision}:subject.py\nCODE:untracked.py\n`); - let result = f.run(...args); - assert.equal(result.status, 0, result.stdout + result.stderr); - assert.match(result.stdout, /2\/2 citations resolve to files/); - - f.file("report.md", `CODE:repo@${revision}:untracked.py\n`); - result = f.run(...args); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /no such file at the cited revision/); - - rmSync(join(repo, "subject.py")); - f.file("report.md", `CODE:repo@${revision}:subject.py\n`); - result = f.run(...args); - assert.equal(result.status, 0, result.stdout + result.stderr); - f.file("report.md", "CODE:subject.py\n"); - result = f.run(...args); - assert.equal(result.status, 1, result.stdout + result.stderr); -}); - -test("document citations resolve files without inspecting headings", t => { - const f = fixture(t); - f.file("repo/guide.md", "REQ-EXAMPLE-001\n"); - const args = [`--repository=repo=${join(f.root, "repo")}`, "report.md"]; - f.file("report.md", "DOC:guide.md#Trace completeness\nDOC:guide.md#missing-heading\nDOC:guide.md\n"); - let result = f.run(...args); - assert.equal(result.status, 0, result.stdout + result.stderr); - assert.match(result.stdout, /3\/3 citations resolve to files/); - f.file("report.md", "DOC:missing.md#Trace completeness\n"); - result = f.run(...args); - assert.equal(result.status, 1, result.stdout + result.stderr); - assert.match(result.stdout, /missing.md.*no such document/); -}); diff --git a/skills/rdd-reverse-engineer/references/coverage.md b/skills/rdd-reverse-engineer/references/coverage.md index 65cdaf8..5ff2af1 100644 --- a/skills/rdd-reverse-engineer/references/coverage.md +++ b/skills/rdd-reverse-engineer/references/coverage.md @@ -7,7 +7,7 @@ authorized context. Do not shrink a denominator to make a result pass. ## Citation validity -Use `rdd-audit` and its supplied `audit-citations.mjs` for citations in recovered +Use `rdd-audit` and the citation audit it names for citations in recovered local documents. It checks cited file resolution and a minimum recognized citation **count** (`--min=N`). It does not enforce percentage coverage. Historical source availability is checked through the sanctioned immutable source reader; From 6b2208ebdb6f60e2d2737296dadecc622d3b74c3 Mon Sep 17 00:00:00 2001 From: Jussi Rajala Date: Tue, 13 Oct 2026 00:45:26 +0300 Subject: [PATCH 2/2] docs(rdd-audit): tell a run that never started from a finding, and say what a not-run check means Fixes from the cold review of this pull request: - The check names the command the package's usual way, "In ModernPath, use `modernpath audit citations`", so the README's Distribution rule still holds and a reader without the ModernPath CLI gets a coherent instruction. - A run that printed no report did not run, whatever its exit status. A CLI without the command answers `unknown command "audit"` with exit 1, which read like a finding. `modernpath audit citations --help` exits 0 only when the command exists. - Exit 2 now lists every cause the command has: an unreadable document, missing Git and an invalid flag. - A citation check that did not run does not by itself block the gate the review serves. A run refused by its environment is a stop (PROCESS.md "Delegated passes"). - When a delegated pass cannot run commands, the orchestrating session hands it the command line, exit status, standard output and error, and each declared repository's revision. The pass lists the check as not run when that command line does not name the documents and repositories under review. - The checker's description includes backticked `path.ext:locator` citations, which it already resolves. Co-Authored-By: Claude Opus 5.5 --- skills/rdd-audit/SKILL.md | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/skills/rdd-audit/SKILL.md b/skills/rdd-audit/SKILL.md index 8baccf9..1c15676 100644 --- a/skills/rdd-audit/SKILL.md +++ b/skills/rdd-audit/SKILL.md @@ -30,10 +30,11 @@ prove semantic coverage or authorize a lifecycle transition. ## Check citations -Use `modernpath audit citations`, the citation audit the `modernpath` CLI -ships, for local Markdown citations. Pass the actual document roots and source -repositories explicitly. Substitute the paths and repository keys in this -command: +Check local Markdown citations with the citation audit the consuming project's +tooling ships. In ModernPath, use `modernpath audit citations`, part of the +`modernpath` CLI; `.modernpath/cli-reference.md` lists its flags and exit +statuses. Pass the actual document roots and source repositories explicitly. +Substitute the paths and repository keys in this command: ```text modernpath audit citations --repository== @@ -47,7 +48,8 @@ exist in that commit, regardless of working-tree changes. Unqualified references and `@unversioned` roots use local files. Use the sanctioned source reader for captured dirty snapshots or other historical revisions. -The checker resolves cited files for `CODE:`, `TEST:` and `DOC:`. The referenced +The checker resolves cited files for `CODE:`, `TEST:` and `DOC:`, and for +backticked `path.ext:locator` citations. The referenced file carries the requirement or other item ID it supports. Optional symbol, line, test-name and heading suffixes are ignored. The checker does not read source contents or match strings within them. File resolution does not establish @@ -68,8 +70,9 @@ The checker uses these exit statuses: - `0`: the file-resolution check passed and the minimum citation count was met. - `1`: broken, ambiguous, unsupported, or elided references, or fewer than `--min=N` citations checked. -- `2`: a requested document target is missing, the source inventory is invalid, - or a guarded prompt target was refused. +- `2`: a requested document target is missing or unreadable, the source + inventory is invalid or Git is missing for a Git root, a flag is invalid, or + a guarded prompt target was refused. Exit `2` means the audit could not run on the requested inputs. Retain the error, correct the inputs or invocation, and rerun; it is not a pass or an automatic @@ -77,12 +80,27 @@ skip. The default minimum is one; `--min` is a citation-count guard against an empty or incomplete scan, not an extraction-coverage threshold. Establish an expected citation count independently before using a higher minimum. +A run that printed no report did not run, whatever its exit status: the report +starts with a `N/M citations resolve to files` line. A CLI without the citation +audit answers `unknown command "audit"` with exit `1`, and a shell without the +CLI cannot start it at all. `modernpath audit citations --help` exits `0` only +when the command exists. + If the audit cannot run — an exit `2` you cannot correct, or a CLI without the citation audit — keep the error, read each citation the review relies on, and report the citation check as not run, with the error. Never report it as passed. -When the agent that writes the review cannot run commands, the agent that runs -the audit passes it the command line, exit status and output, or the error; -the reviewer lists the citation check as not run when the audit did not run. +A citation check that did not run does not by itself block the gate the review +serves: the review records it as not run, with the error, and the human sees +that at the gate. A run refused by its environment — a permission denial or an +authentication failure — is a stop: report the refusal verbatim (`PROCESS.md` +"Delegated passes"). + +When a delegated pass cannot run commands, the orchestrating session runs the +audit and passes it the command line, the exit status, the standard output and +standard error, and the revision of each declared repository — or the error. +The pass lists the citation check as not run when the audit did not run, or +when that command line does not name the documents and repositories under +review. ## Verify meaning and measurement