diff --git a/scripts/validate-reference-links-test.js b/scripts/validate-reference-links-test.js index 8c8b147a..93c41cb3 100644 --- a/scripts/validate-reference-links-test.js +++ b/scripts/validate-reference-links-test.js @@ -151,3 +151,76 @@ test('reports every unresolvable link, not just the first per skill', () => { assert.equal(result.status, 1, result.stdout + result.stderr); assert.match(result.stdout, /1 skills checked — 2 error\(s\) — FAILED/); }); + +test('a link inside a fenced block is an example, not a link to resolve', () => { + const root = makeSandbox(); + writeFile(root, 'references/definition-of-done.md', '# DoD\n'); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + 'Shared checklists live two levels up:', + '', + 'See `../../references/definition-of-done.md`.', + '', + 'Do not write it this way:', + '', + '```markdown', + '[Definition of Done](references/definition-of-done.md)', + '```', + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/); +}); + +test('fence exemption follows CommonMark rather than a bare ``` match', () => { + const root = makeSandbox(); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + '~~~markdown', + '[a](references/missing-a.md)', + '```', // wrong marker: must not close the ~~~ block + '[b](references/missing-b.md)', + '~~~', + '', + ' ```markdown', // three-space indent is still a fence + '[c](references/missing-c.md)', + ' `````', // a longer closer is legal + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /0 error\(s\) — PASSED/); +}); + +test('a real broken link outside any fence is still reported', () => { + const root = makeSandbox(); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + '```markdown', + '[shown as an example](references/example-only.md)', + '```', + '', + 'See `references/definition-of-done.md`.', // genuinely wrong, outside the fence + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/); + assert.match(result.stdout, /L5: references\/definition-of-done\.md/); +}); diff --git a/scripts/validate-reference-links.js b/scripts/validate-reference-links.js index e9f30c87..70927faa 100644 --- a/scripts/validate-reference-links.js +++ b/scripts/validate-reference-links.js @@ -25,6 +25,12 @@ * mention paths that do not exist yet (`tasks/todo.md`, `PERF.md`, * `docs/ideas/[idea-name].md`), and those must not fail the build. * + * Fenced code blocks are exempt for the same reason. Text inside a fence is + * an example, not a link an agent will follow — and this rule in particular + * has to be documentable: the failure message below tells authors to write + * `../../references/.md` rather than `references/.md`, which no + * skill could show as an example without failing the very check explaining it. + * * Exit codes: 0 = all clear, 1 = one or more unresolvable links. */ @@ -41,11 +47,50 @@ const SKILLS_DIR = path.join(ROOT, 'skills'); // non-path character so `myreferences/x.md` does not match. const REFERENCE_LINK_RE = /(? { + const marker = line.match(/^ {0,3}(`{3,}|~{3,})/); + if (fence) { + fenced.add(i); + if (marker && marker[1][0] === fence.marker && marker[1].length >= fence.length) { + fence = null; + } + return; + } + if (marker) { + fence = { marker: marker[1][0], length: marker[1].length }; + fenced.add(i); + } + }); + + return fenced; +} + function findViolations(skillDir, skillFile) { const violations = []; const lines = fs.readFileSync(skillFile, 'utf8').split(/\r?\n/); + const fenced = fencedLineNumbers(lines); lines.forEach((line, i) => { + if (fenced.has(i)) return; for (const match of line.matchAll(REFERENCE_LINK_RE)) { const link = match[1]; if (!fs.existsSync(path.resolve(skillDir, link))) {