fix(agents): validate visible headings and spaced paths

This commit is contained in:
4gray committed 2026-09-20 22:39:02 +02:00
1 parent b0cf310074
commit b54dc4c405
3 files changed
+66 -10

No files matched your search

+4 -2
View File
@@ -37,6 +37,7 @@ Write generic filenames as prose; commands, templates, globs, URLs, package
aliases and dotted code symbols are excluded. Bare dotted names with conventional
file suffixes (such as .md, .json or .ts) are treated as filenames. Use a `./`
prefix or Markdown link for other ambiguous filenames that resemble code symbols.
Explicit relative literal paths may contain spaces; command-option snippets are excluded.
Multi-part dotfiles are path candidates too.
Conventional extensionless filenames such as Dockerfile, Makefile and LICENSE
are also path candidates; use an explicit `./` prefix for other extensionless files.
@@ -49,8 +50,9 @@ The parsed HTML tree also verifies that this paragraph is outside HTML container
including templates split across Markdown tokens. Generated HTML is inspected
in memory only; it is never executed or emitted.
Heading anchors decode HTML character references in text and use `github-slugger`
for GitHub-compatible character filtering and duplicate suffixes. Explicit HTML anchors
use `parse5`, excluding comments, scripts, styles and template contents.
for GitHub-compatible character filtering and duplicate suffixes.
Only headings present outside inert HTML containers contribute slugs or duplicate counters.
Explicit HTML anchors 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
+24 -8
View File
@@ -148,21 +148,37 @@ export function guidanceStandaloneImports(markdown) {
export function guidanceAnchors(markdown) {
const slugger = new GithubSlugger();
const found = new Set();
const html = [];
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
if (token.type === 'heading') {
found.add(slugger.slug(inlineText(token.tokens)));
}
if (token.type === 'html') html.push(token.raw);
const headings = [];
const marker = `data-guidance-${randomUUID()}`;
const renderer = new Marked({
renderer: {
heading(token) {
const index = headings.push(inlineText(token.tokens)) - 1;
return `<h${token.depth} ${marker}="${index}">${this.parser.parseInline(token.tokens)}</h${token.depth}>\n`;
},
},
});
return new Set([...found, ...htmlNavigation(html.join('\n')).anchors]);
const navigation = htmlNavigation(renderer.parse(markdown), (node) => {
const attribute = node.attrs?.find((attr) => attr.name === marker);
if (attribute)
found.add(slugger.slug(headings[Number(attribute.value)]));
});
return new Set([...found, ...navigation.anchors]);
}
function isLiteralRepositoryPath(token) {
// A typo in the directory or a new root filename must still be checked.
// Exclude recognizable prose/code forms instead of allowlisting paths.
if (/^(?:@|--|[a-z][a-z\d+.-]*:|\/\/)/iu.test(token)) return false;
if (/[^\p{L}\p{N}_./#-]/u.test(token)) return false;
const explicitRelative = /^(?:\.\/|\.\.\/)/u.test(token);
if (
(explicitRelative
? /[^\p{L}\p{N}_./# -]/u
: /[^\p{L}\p{N}_./#-]/u
).test(token)
)
return false;
if (/\s+-{1,2}[\p{L}]/u.test(token)) return false;
if (token.includes('YYYY-MM-DD') || /(?:^|\/)\.\.\.(?:\/|$)/u.test(token))
return false;
const path = token.split('#')[0];
@@ -883,3 +883,41 @@ for (const content of [
);
});
}
test('inert headings do not define or consume anchor slugs', async (t) => {
const body =
'<template>\n\n# Hidden\n\n# Visible\n\n</template>\n\n# Visible';
assert.match(
(
await diagnostics(t, {
'AGENTS.md': '[Hidden](docs/example.md#hidden)',
'docs/example.md': body,
})
).join('\n'),
/missing anchor/
);
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': '[Visible](docs/example.md#visible)',
'docs/example.md': body,
}),
[]
);
});
test('explicit relative literal paths support spaces', async (t) => {
assert.match(
(
await diagnostics(t, {
'AGENTS.md': '`./docs/My Guide.md`',
})
).join('\n'),
/does not exist/
);
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': '`./docs/My Guide.md`',
'docs/My Guide.md': '',
}),
[]
);
});