fix(agents): distinguish URL quotes and cover guidance variants

This commit is contained in:
4gray committed 2026-09-21 04:58:10 +02:00
1 parent 7b4a49821b
commit 87a27b5458
4 files changed
+74 -9

No files matched your search

+3 -3
View File
@@ -69,7 +69,7 @@ Image and media references require a nonempty path that resolves to a file, not
Direct file URLs and file-scheme HTML bases are rejected; use portable repository-relative paths.
Image references check file existence without interpreting image fragments as
Markdown headings; document links keep anchor checks even when sharing a target.
HTML video, audio, source and track `src` assets and video posters use the same
HTML image-input, video, audio, source and track `src` assets and video posters use the same
existence checks as images. Entity decoding uses full HTML text/attribute rules,
including references whose semicolon may be omitted.
Inline guidance imports are rejected after punctuation as well as whitespace.
@@ -77,7 +77,7 @@ At-signs inside external URIs (including explicit opaque autolinks such as mailt
per HTML text node, preserving adjacent imports. A colon directly before an import
does not make that import a URI. Opaque schemes are excluded only in parsed links
whose visible text equals their URI, so colon-labeled prose remains checked.
A closing bracket or quote followed by punctuation and an at-sign terminates a bare URL exclusion.
A closing bracket or matching enclosing quote followed by punctuation and an at-sign terminates a bare URL exclusion.
Extensionless inline candidates are also imports when they resolve to repository files,
checking the full filename before prefixes at ASCII/Unicode prose separators,
including opening parentheses, brackets and braces. Each at-sign candidate is
@@ -88,7 +88,7 @@ rejected before those exemptions, including document paths with fragments or que
All recognized Markdown extensions share the document-import guard; reStructuredText
and AsciiDoc, PDF, Word, OpenDocument, RTF, Org and TeX documents are also excluded
from package exemptions. Recognized extensionless guidance names (including AGENTS,
CLAUDE, INSTRUCTIONS and README) are excluded in package subpaths too. URL-encoded
CLAUDE, INSTRUCTIONS and README, case-insensitively) are excluded in package subpaths too. URL-encoded
paths do not receive package exemptions. TypeScript configuration is parsed as JSONC.
Declared packages also permit safe subpaths; exact aliases stay exact.
Declared package mentions may include a version (including semver comparators and wildcard ranges) or dist-tag qualifier.
+31 -5
View File
@@ -90,7 +90,14 @@ function htmlNavigation(html, inspect = () => {}) {
].includes(node.tagName) &&
attribute.name === 'src') ||
(node.tagName === 'video' && attribute.name === 'poster') ||
(node.tagName === 'object' && attribute.name === 'data')
(node.tagName === 'object' && attribute.name === 'data') ||
(node.tagName === 'input' &&
attribute.name === 'src' &&
node.attrs.some(
(attr) =>
attr.name === 'type' &&
attr.value.toLowerCase() === 'image'
))
)
references.push({
target: attribute.value
@@ -141,7 +148,7 @@ function htmlNavigation(html, inspect = () => {}) {
}
export function guidanceProse(markdown) {
function text(node) {
function text(node, preceding = '') {
if (
['script', 'style', 'template', 'pre', 'code'].includes(
node.tagName
@@ -163,10 +170,29 @@ export function guidanceProse(markdown) {
}
if (node.nodeName === '#text')
return node.value.replace(
/(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]*?(?=[)\]}>"'”’][.,;:!?]*@|\s|$)/giu,
' '
/(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]*?(?=[)\]}>][.,;:!?]*@|\s|$)/giu,
(url, offset) => {
const opening = (
preceding + node.value.slice(0, offset)
).at(-1);
const closing = {
'"': '"',
"'": "'",
'“': '”',
'”': '”',
'‘': '’',
'’': '’',
}[opening];
const boundary = closing ? url.indexOf(closing) : -1;
return boundary >= 0 &&
/^[.,;:!?]*@/u.test(url.slice(boundary + 1))
? ' ' + url.slice(boundary)
: ' ';
}
);
const content = (node.childNodes ?? []).map(text).join('');
let content = '';
for (const child of node.childNodes ?? [])
content += text(child, preceding + content);
return [
'address',
'article',
+1 -1
View File
@@ -166,7 +166,7 @@ async function packageMentions(rootDir) {
if (
/%[\da-f]{2}/iu.test(token) ||
MARKDOWN_EXTENSION.test(path) ||
/(?:^|\/)(?:AGENTS|CLAUDE|INSTRUCTIONS|README|LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CHANGELOG)$/u.test(
/(?:^|\/)(?:AGENTS|CLAUDE|INSTRUCTIONS|README|LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CHANGELOG)$/iu.test(
path
) ||
DOCUMENT_EXTENSION.test(path)
@@ -1630,3 +1630,42 @@ for (const embedded of [false, true]) {
);
});
}
for (const url of [
"https://example.com/don't@docs/guide.md",
"https://example.com/path'@docs/guide.md",
]) {
test(`internal URL apostrophe remains URL prose: ${url}`, async (t) => {
assert.deepEqual(await diagnostics(t, { 'AGENTS.md': url }), []);
});
}
for (const name of ['readme', 'instructions', 'Agents']) {
test(`guidance basename matching ignores case: ${name}`, async (t) => {
assert.ok(
(
await diagnostics(t, {
'package.json': JSON.stringify({
dependencies: { '@angular/core': '*' },
}),
'CLAUDE.md':
'@AGENTS.md\n\nRead @angular/core/docs/' + name,
})
).some((message) => message.includes('additional or inline'))
);
});
}
test('image input source is validated', async (t) => {
assert.ok(
(
await diagnostics(t, {
'AGENTS.md': '<input type="IMAGE" src="missing.png">',
})
).length > 0
);
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': '<input type="text" src="missing.png">',
}),
[]
);
});