From 87a27b5458006121c73522dc6fd9ed6a71cb7cb2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 21 Sep 2026 04:58:10 +0200 Subject: [PATCH] fix(agents): distinguish URL quotes and cover guidance variants --- docs/development/agent-workflow.md | 6 +-- tools/skills/agent-guidance-markdown.mjs | 36 ++++++++++++++--- tools/skills/validate-agent-guidance.mjs | 2 +- tools/skills/validate-agent-guidance.test.mjs | 39 +++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 42cb09109..11b549f78 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -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. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index a903180c6..5f697c1b9 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -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', diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index f500c4e24..1af1f3a17 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -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) diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index 84dfd50df..ea7364c92 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -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': '', + }) + ).length > 0 + ); + assert.deepEqual( + await diagnostics(t, { + 'AGENTS.md': '', + }), + [] + ); +});