diff --git a/skills/rdd-audit/SKILL.md b/skills/rdd-audit/SKILL.md index cc4c552..1c15676 100644 --- a/skills/rdd-audit/SKILL.md +++ b/skills/rdd-audit/SKILL.md @@ -30,12 +30,14 @@ 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: +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 -node /audit-citations.mjs --repository== +modernpath audit citations --repository== ``` Repeat `--repository` for multiple repositories. Git worktrees and explicitly @@ -46,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 @@ -67,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 @@ -76,6 +80,28 @@ 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. +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 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;