From f2f7a948571741dd8ce95010ae43979456bd697d Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 20 Sep 2026 21:35:52 +0200 Subject: [PATCH] fix(agents): decode link entities and allow package subpaths --- docs/development/agent-workflow.md | 2 ++ tools/skills/agent-guidance-markdown.mjs | 4 +-- tools/skills/validate-agent-guidance.mjs | 9 +++-- tools/skills/validate-agent-guidance.test.mjs | 35 +++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index f6986ae53..85e790ced 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -56,6 +56,8 @@ Inline guidance imports are rejected after punctuation as well as whitespace. Declared scoped dependencies, scope wildcards and matching TypeScript path aliases are recognized as package/alias mentions. Traversal and document-file imports are rejected before those exemptions. TypeScript configuration is parsed as JSONC. +Declared packages also permit safe subpaths; exact aliases stay exact. +Markdown destinations decode HTML entities before URI parsing, matching rendered links. The import scan includes visible HTML text and excludes code and non-rendered containers. Image source sets use `parse-srcset` to check each candidate URL. Root-relative literals never suppress source-relative definition checks. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index 5628c9857..5f40a7953 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -198,8 +198,8 @@ export function guidanceReferences(markdown, includeLiterals) { } markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => { if (['link', 'image'].includes(token.type)) - add(token.href, false, token.type === 'image'); - if (token.type === 'def') definitions.push(token.href); + add(decodeEntities(token.href), false, token.type === 'image'); + if (token.type === 'def') definitions.push(decodeEntities(token.href)); if (token.type === 'html') html.push(token.raw); if (token.type === 'unresolved-reference') result.push({ unresolvedReference: token.label }); diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index bf0cd2c45..52ae615c5 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -89,11 +89,14 @@ async function packageMentions(rootDir) { } const manifest = await readJson('package.json'); const config = await readJson('tsconfig.base.json'); - const names = [ + const packages = [ ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {}), ...Object.keys(manifest.optionalDependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {}), + ]; + const names = [ + ...packages, ...Object.keys(config.compilerOptions?.paths ?? {}), ]; const declared = names @@ -111,7 +114,9 @@ async function packageMentions(rootDir) { return declared.some((name) => { const star = name.indexOf('*'); return star < 0 - ? token === name + ? token === name || + (packages.includes(`@${name}`) && + token.startsWith(`${name}/`)) : token.startsWith(name.slice(0, star)) && token.endsWith(name.slice(star + 1)); }); diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index 65aa86f3e..6bb8d37e1 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -752,3 +752,38 @@ for (const target of [ ); }); } + +test('Markdown destination entities resolve rendered filenames', async (t) => { + for (const reference of [ + '[Guide](docs/a&b.md)', + '[Guide][ref]\n\n[ref]: docs/a&b.md', + ]) { + assert.deepEqual( + await diagnostics(t, { + 'AGENTS.md': reference, + 'docs/a&b.md': '# Guide', + }), + [] + ); + assert.match( + ( + await diagnostics(t, { + 'AGENTS.md': reference, + 'docs/a&b.md': '# Wrong name', + }) + ).join('\n'), + /does not exist/ + ); + } +}); +test('declared packages allow safe subpaths', async (t) => { + assert.deepEqual( + await diagnostics(t, { + 'package.json': JSON.stringify({ + dependencies: { '@angular/core': '*' }, + }), + 'AGENTS.md': 'Use @angular/core/testing.', + }), + [] + ); +});