From 02bfa81e356c8f73eec39686d408b5ced7501e55 Mon Sep 17 00:00:00 2001 From: Cockpit Worker c1f193bf83e40045 Date: Mon, 24 Aug 2026 20:17:03 +0000 Subject: [PATCH 1/2] fix(docs-check): require an actual markdown link for index coverage check-docs.mjs treated docs/README.md index coverage as satisfied by any substring match of "/", so a bare mention of a doc's path in prose, a comment, or a fenced example counted as an index entry. Index coverage now parses real relative markdown link targets the same way link-integrity does, and only counts a doc as indexed when a link actually resolves to it. --- scripts/check-docs.mjs | 12 ++++++++++-- test/docs-integrity.test.mjs | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 03d7393..4b2e219 100644 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -17,7 +17,15 @@ const failures = []; const rate = (pass, total) => (total === 0 ? 1 : pass / total); // ---- 1. Index coverage: every doc in the four categories is indexed. ---- -const indexText = readFileSync(join(root, "docs/README.md"), "utf8"); +const indexPath = join(root, "docs/README.md"); +// Strip fenced code blocks so example links/prose aren't mistaken for real links. +const indexText = readFileSync(indexPath, "utf8").replace(/```[\s\S]*?```/g, ""); +const indexedTargets = new Set(); +for (const m of indexText.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) { + const target = m[1]; + if (/^[a-z][a-z+.-]*:/i.test(target) || target.startsWith("#")) continue; + indexedTargets.add(resolve(dirname(indexPath), target.split("#")[0])); +} const categories = ["adr", "design", "specs", "plans"]; let docsTotal = 0; let docsIndexed = 0; @@ -25,7 +33,7 @@ for (const cat of categories) { for (const name of readdirSync(join(root, "docs", cat))) { if (!name.endsWith(".md") || name === "TEMPLATE.md") continue; docsTotal++; - if (indexText.includes(`${cat}/${name}`)) docsIndexed++; + if (indexedTargets.has(join(root, "docs", cat, name))) docsIndexed++; else failures.push(`index: docs/${cat}/${name} has no line in docs/README.md`); } } diff --git a/test/docs-integrity.test.mjs b/test/docs-integrity.test.mjs index 249d69e..475d436 100644 --- a/test/docs-integrity.test.mjs +++ b/test/docs-integrity.test.mjs @@ -59,6 +59,33 @@ test("docs gate accepts cross-skill links that stay in the synced skills tree", assert.match(stdout, /ok link_integrity: 1\.000/); }); +test("docs gate rejects index coverage from a bare document path without a markdown link", async () => { + const root = await createFixture(); + await writeFile(path.join(root, "docs/adr/example.md"), "# Example\n"); + await writeFile( + path.join(root, "docs/README.md"), + "[Metric](specs/pr-acceptance-metric.md)\n\nSee adr/example.md for details.\n", + ); + + await assert.rejects( + runDocsGate(root), + (error) => + error.stderr.includes("index: docs/adr/example.md has no line in docs/README.md"), + ); +}); + +test("docs gate accepts index coverage from an actual relative markdown link", async () => { + const root = await createFixture(); + await writeFile(path.join(root, "docs/adr/example.md"), "# Example\n"); + await writeFile( + path.join(root, "docs/README.md"), + "[Metric](specs/pr-acceptance-metric.md)\n[Example](adr/example.md)\n", + ); + + const { stdout } = await runDocsGate(root); + assert.match(stdout, /ok docs_index_coverage: 1\.000/); +}); + async function createFixture() { const root = await mkdtemp(path.join(os.tmpdir(), "core-docs-gate-")); for (const dir of [ From 6b778da1e97457110fc835914f60206f223cdc46 Mon Sep 17 00:00:00 2001 From: Cockpit Worker 9fa9e62d660f5226 Date: Mon, 24 Aug 2026 20:28:50 +0000 Subject: [PATCH 2/2] fix(docs-check): exclude HTML comments from index-coverage link parsing Review round 1 blocker defect:scripts/check-docs.mjs:html-comment-links: a commented-out markdown link (e.g. ) in docs/README.md was counted as coverage even though it renders no actual link. Strip HTML comment regions before extracting links, and add a regression test proving a commented-out link still fails the missing-index diagnostic. Co-Authored-By: Claude Sonnet 5 --- scripts/check-docs.mjs | 7 +++++-- test/docs-integrity.test.mjs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 4b2e219..a9ae9e6 100644 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -18,8 +18,11 @@ const rate = (pass, total) => (total === 0 ? 1 : pass / total); // ---- 1. Index coverage: every doc in the four categories is indexed. ---- const indexPath = join(root, "docs/README.md"); -// Strip fenced code blocks so example links/prose aren't mistaken for real links. -const indexText = readFileSync(indexPath, "utf8").replace(/```[\s\S]*?```/g, ""); +// Strip fenced code blocks and HTML comments so example/commented-out links +// aren't mistaken for real links. +const indexText = readFileSync(indexPath, "utf8") + .replace(/```[\s\S]*?```/g, "") + .replace(//g, ""); const indexedTargets = new Set(); for (const m of indexText.matchAll(/\[[^\]]*\]\(([^)\s]+)\)/g)) { const target = m[1]; diff --git a/test/docs-integrity.test.mjs b/test/docs-integrity.test.mjs index 475d436..11eab2a 100644 --- a/test/docs-integrity.test.mjs +++ b/test/docs-integrity.test.mjs @@ -74,6 +74,21 @@ test("docs gate rejects index coverage from a bare document path without a markd ); }); +test("docs gate rejects index coverage from a link inside an HTML comment", async () => { + const root = await createFixture(); + await writeFile(path.join(root, "docs/adr/example.md"), "# Example\n"); + await writeFile( + path.join(root, "docs/README.md"), + "[Metric](specs/pr-acceptance-metric.md)\n\n\n", + ); + + await assert.rejects( + runDocsGate(root), + (error) => + error.stderr.includes("index: docs/adr/example.md has no line in docs/README.md"), + ); +}); + test("docs gate accepts index coverage from an actual relative markdown link", async () => { const root = await createFixture(); await writeFile(path.join(root, "docs/adr/example.md"), "# Example\n");