mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
fix(agents): handle image fragments and quoted imports
This commit is contained in:
1 parent
87c3e466d2
commit
e028712e61
4 files changed
+75
-10
No files matched your search
@@ -50,6 +50,9 @@ for GitHub-compatible character filtering and duplicate suffixes. Explicit HTML
|
||||
use `parse5`, excluding comments, scripts, styles and template contents.
|
||||
Rendered HTML anchor hrefs and image sources use the same local-reference checks
|
||||
as Markdown links, including decoded attributes and fragment validation.
|
||||
Image references check file existence without interpreting image fragments as
|
||||
Markdown headings; document links keep anchor checks even when sharing a target.
|
||||
Inline guidance imports are rejected after punctuation as well as whitespace.
|
||||
The Nx test hash includes `marked`, `parse5` and `github-slugger` so dependency changes invalidate parser coverage.
|
||||
It cannot prove semantic equivalence; review changed contracts as well.
|
||||
|
||||
|
||||
@@ -65,7 +65,10 @@ function htmlNavigation(html) {
|
||||
(node.tagName === 'a' && attribute.name === 'href') ||
|
||||
(node.tagName === 'img' && attribute.name === 'src')
|
||||
)
|
||||
references.push(attribute.value);
|
||||
references.push({
|
||||
target: attribute.value,
|
||||
image: node.tagName === 'img',
|
||||
});
|
||||
}
|
||||
for (const child of node.childNodes ?? []) visit(child);
|
||||
}
|
||||
@@ -157,15 +160,20 @@ export function guidanceReferences(markdown, includeLiterals) {
|
||||
const result = [];
|
||||
const destinations = new Set();
|
||||
const html = [];
|
||||
function add(target, literal = false) {
|
||||
const key = `${literal}:${target}`;
|
||||
const definitions = [];
|
||||
const usedTargets = new Set();
|
||||
function add(target, literal = false, image = false) {
|
||||
const key = `${literal}:${image}:${target}`;
|
||||
usedTargets.add(target);
|
||||
if (!destinations.has(key)) {
|
||||
destinations.add(key);
|
||||
result.push({ target, literal });
|
||||
result.push({ target, literal, image });
|
||||
}
|
||||
}
|
||||
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
|
||||
if (['link', 'image', 'def'].includes(token.type)) add(token.href);
|
||||
if (['link', 'image'].includes(token.type))
|
||||
add(token.href, false, token.type === 'image');
|
||||
if (token.type === 'def') definitions.push(token.href);
|
||||
if (token.type === 'html') html.push(token.raw);
|
||||
if (token.type === 'unresolved-reference')
|
||||
result.push({ unresolvedReference: token.label });
|
||||
@@ -176,7 +184,8 @@ export function guidanceReferences(markdown, includeLiterals) {
|
||||
)
|
||||
add(token.text, true);
|
||||
});
|
||||
for (const target of htmlNavigation(html.join('\n')).references)
|
||||
add(target);
|
||||
for (const reference of htmlNavigation(html.join('\n')).references)
|
||||
add(reference.target, false, reference.image);
|
||||
for (const target of definitions) if (!usedTargets.has(target)) add(target);
|
||||
return result;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function within(root, path) {
|
||||
async function validateReference(
|
||||
rootDir,
|
||||
source,
|
||||
{ target, literal, unresolvedReference }
|
||||
{ target, literal, image, unresolvedReference }
|
||||
) {
|
||||
if (unresolvedReference !== undefined)
|
||||
return `${source}: unresolved Markdown reference "${unresolvedReference}"`;
|
||||
@@ -51,7 +51,11 @@ async function validateReference(
|
||||
const actual = await realpath(absolute);
|
||||
if (!within(rootDir, actual))
|
||||
return `${source}: referenced path escapes repository root: ${target}`;
|
||||
if (anchor && !anchors(await readFile(actual, 'utf8')).has(anchor)) {
|
||||
if (
|
||||
anchor &&
|
||||
!image &&
|
||||
!anchors(await readFile(actual, 'utf8')).has(anchor)
|
||||
) {
|
||||
return `${source}: missing anchor "${anchor}" in ${target}`;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -93,7 +97,9 @@ export async function validateAgentGuidance({ rootDir }) {
|
||||
const unfenced = guidanceProse(markdown);
|
||||
const imports = guidanceStandaloneImports(markdown);
|
||||
const prose = unfenced.replace(/`[^`\n]+`/gu, '');
|
||||
const inlineImports = [...prose.matchAll(/(?:^|[\s(])@([^\s]+)/gu)]
|
||||
const inlineImports = [
|
||||
...prose.matchAll(/(?:^|[^\p{L}\p{N}_@])@([^\s]+)/gu),
|
||||
]
|
||||
.map((match) => match[1])
|
||||
.filter(
|
||||
(token) =>
|
||||
|
||||
@@ -591,3 +591,50 @@ for (const tag of ['template', 'div']) {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (const punctuation of ['"', "'", '[', '{', ':']) {
|
||||
test(`rejects inline imports after ${punctuation}`, async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'CLAUDE.md':
|
||||
'@AGENTS.md\n\nRead ' +
|
||||
punctuation +
|
||||
'@docs/example.md',
|
||||
})
|
||||
).join('\n'),
|
||||
/imports are not allowed/
|
||||
);
|
||||
});
|
||||
}
|
||||
for (const image of [
|
||||
'',
|
||||
'<img src="docs/diagram.png#gh-light-mode-only">',
|
||||
'![Diagram][image]\n\n[image]: docs/diagram.png#gh-dark-mode-only',
|
||||
]) {
|
||||
test(`image fragments are not Markdown headings: ${image}`, async (t) => {
|
||||
assert.deepEqual(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md': image,
|
||||
'docs/diagram.png': 'image fixture',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
assert.match(
|
||||
(await diagnostics(t, { 'AGENTS.md': image })).join('\n'),
|
||||
/does not exist/
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('sharing an image reference does not suppress document anchor checks', async (t) => {
|
||||
assert.match(
|
||||
(
|
||||
await diagnostics(t, {
|
||||
'AGENTS.md':
|
||||
'![Preview][target] [Read][target]\n\n[target]: docs/example.md#missing',
|
||||
})
|
||||
).join('\n'),
|
||||
/missing anchor/
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user