mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-10-26 15:05:04 +00:00
Merge #586: reuse shared fence parsing in the reference-link validator
This commit is contained in:
3 files changed
+60
-41
No files matched your search
@@ -304,11 +304,12 @@ function lintSkill(dirName, skillsDir, knownSkills) {
|
||||
return lintSkillContent(dirName, content, knownSkills);
|
||||
}
|
||||
|
||||
// Export only the linting functions. The policy collections (REQUIRED_SECTIONS,
|
||||
// Export only functions. The policy collections (REQUIRED_SECTIONS,
|
||||
// SECTION_EXEMPT_SKILLS, SKILL_REF_PATTERNS, and the regexes) stay private so a
|
||||
// test or future consumer cannot mutate shared state and change lint results for
|
||||
// the rest of the process. Exercise the rules through these functions.
|
||||
module.exports = {
|
||||
stripFencedCodeBlocks,
|
||||
parseFrontmatter,
|
||||
extractSkillReferences,
|
||||
lintSkillContent,
|
||||
|
||||
@@ -17,6 +17,8 @@ function makeSandbox() {
|
||||
const scriptsDir = path.join(root, 'scripts');
|
||||
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-reference-links.js'));
|
||||
fs.mkdirSync(path.join(scriptsDir, 'lib'));
|
||||
fs.copyFileSync(path.join(__dirname, 'lib', 'skill-lint.js'), path.join(scriptsDir, 'lib', 'skill-lint.js'));
|
||||
sandboxes.push(root);
|
||||
return root;
|
||||
}
|
||||
@@ -224,3 +226,56 @@ test('a real broken link outside any fence is still reported', () => {
|
||||
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
||||
assert.match(result.stdout, /L5: references\/definition-of-done\.md/);
|
||||
});
|
||||
|
||||
for (const [ending, newline] of [['LF', '\n'], ['CRLF', '\r\n']]) {
|
||||
test(`inline backticks do not hide subsequent broken links (${ending})`, () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, 'skills/example/SKILL.md', [
|
||||
'```js``` is inline code, not a fence opener.',
|
||||
'See [missing](references/missing.md).',
|
||||
'',
|
||||
].join(newline));
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 1, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /L2: references\/missing\.md/);
|
||||
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
||||
});
|
||||
|
||||
for (const marker of ['```', '~~~']) {
|
||||
test(`trailing text does not close a ${marker} fence (${ending})`, () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, 'skills/example/SKILL.md', [
|
||||
marker + 'markdown',
|
||||
marker + ' still part of the example',
|
||||
'[example](references/example-only.md)',
|
||||
marker + ' \t',
|
||||
'[real link](references/missing.md)',
|
||||
'',
|
||||
].join(newline));
|
||||
|
||||
const result = run(root);
|
||||
|
||||
assert.equal(result.status, 1, result.stdout + result.stderr);
|
||||
assert.match(result.stdout, /L5: references\/missing\.md/);
|
||||
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
||||
assert.doesNotMatch(result.stdout, /example-only\.md/);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('tilde fence info strings may contain backticks', () => {
|
||||
const root = makeSandbox();
|
||||
writeFile(root, 'skills/example/SKILL.md', [
|
||||
'~~~example `code`',
|
||||
'[example](references/example-only.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/);
|
||||
});
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { stripFencedCodeBlocks } = require('./lib/skill-lint');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SKILLS_DIR = path.join(ROOT, 'skills');
|
||||
@@ -47,50 +48,12 @@ 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);
|
||||
// Share the linter's fence rules; blanked lines preserve diagnostic positions.
|
||||
const lines = stripFencedCodeBlocks(fs.readFileSync(skillFile, 'utf8')).split('\n');
|
||||
|
||||
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))) {
|
||||
|
||||
Reference in new issue
Block a user