Merge #517: exempt fenced examples from the references link check

This commit is contained in:
Addy Osmani authored and GitHub committed 2026-09-17 16:37:57 -07:00
commit e79ff3bb0d
2 files changed
+118

No files matched your search

+73
View File
@@ -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/);
});
+45
View File
@@ -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/<file>.md` rather than `references/<file>.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 = /(?<![A-Za-z0-9._/-])((?:\.\.\/)*references\/[A-Za-z0-9._-]+\.md)/g;
/**
* Line indices (0-based) that sit inside a fenced code block.
*
* Scans line by line rather than matching one regex over the document, so the
* three fence forms CommonMark allows are all handled: tilde fences, fences
* indented up to three spaces, and a closing fence longer than its opener. A
* fence closes only on the same marker character repeated at least as many
* times, so a ``` line inside a ~~~ block does not end it.
*
* Deliberately local rather than shared with `scripts/lib/skill-lint.js`:
* that module's `stripFencedCodeBlocks` is a single regex with the gaps
* described in #437, and #444 is already rewriting it. Duplicating a correct
* scanner here is the smaller cost than forking a rewrite in flight; once
* #444 lands, the two should collapse into one helper under `scripts/lib/`.
*/
function fencedLineNumbers(lines) {
const fenced = new Set();
let fence = null;
lines.forEach((line, i) => {
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))) {