diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index dc7c20170..0a7ef2c14 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -36,7 +36,8 @@ Backticked concrete paths in root guidance and the context map are checked from the repository root, including unknown top-level directories and filenames. 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 `./` +file suffixes (such as .md, .json or .ts) are treated as filenames. Document formats +share the suffix set used by package-import guards, including PDF and AsciiDoc. Use a `./` prefix or Markdown link for other ambiguous filenames that resemble code symbols. Explicit relative literals denote paths, including spaces, filesystem punctuation and hyphenated words. Put executable command examples in fenced code when their syntax also looks like a path. @@ -78,7 +79,8 @@ whose visible text equals their URI, so colon-labeled prose remains checked. A closing bracket 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. +including opening parentheses, brackets and braces. Each at-sign candidate is +checked independently, including imports nested next to a package mention. 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, including document paths with fragments or queries. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index 7c11103d7..82b9aa3aa 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -4,6 +4,9 @@ import GithubSlugger from 'github-slugger'; import { Marked, Tokenizer } from 'marked'; import { parseFragment } from 'parse5'; +export const DOCUMENT_EXTENSION = + /\.(?:md|markdown|mdown|mkd|mdx|txt|json|ya?ml|html?|rst|rest|adoc|asciidoc|pdf|doc[xm]?|dot[xm]?|od[tspgfbm]|ot[tspg]|fod[tspg]|rtf|org|tex|latex)$/iu; + // Inspection only: generated HTML is parsed in memory, never executed or emitted. const markdownLexer = new Marked({ tokenizer: { @@ -280,6 +283,7 @@ function isLiteralRepositoryPath(token) { // This applies to user-defined symbols as well as JavaScript globals. if ( /^[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)+$/u.test(path) && + !DOCUMENT_EXTENSION.test(path) && !/\.(?:md|mdx|json|jsonc|ya?ml|[cm]?[jt]sx?|html?|css|scss|sass|less|toml|xml|txt|sh|py|sql|svg|png|jpe?g|webp|gif|m3u8?|conf|ini|lock)$/iu.test( path ) diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index 45f85c27a..a88386d99 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -10,6 +10,7 @@ import { } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { + DOCUMENT_EXTENSION, guidanceAnchors as anchors, guidanceProse, guidanceStandaloneImports, @@ -160,9 +161,7 @@ async function packageMentions(rootDir) { /(?:^|\/)(?:AGENTS|CLAUDE|INSTRUCTIONS|README|LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CHANGELOG)$/u.test( path ) || - /\.(?:txt|json|ya?ml|html?|rst|rest|adoc|asciidoc|pdf|doc[xm]?|dot[xm]?|od[tspgfbm]|ot[tspg]|fod[tspg]|rtf|org|tex|latex)$/iu.test( - path - ) + DOCUMENT_EXTENSION.test(path) ) return false; if (packages.includes(token)) return true; @@ -213,7 +212,7 @@ export async function validateAgentGuidance({ rootDir }) { const imports = guidanceStandaloneImports(markdown); const inlineImports = []; for (const match of prose.matchAll( - /(?:^|[^\p{L}\p{N}_@])@([^\s]+)/gu + /(?=(?:^|[^\p{L}\p{N}_@])@([^\s]+))/gu )) { const token = match[1]; if (isPackageMention(token)) continue; diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index fb439347b..1fca537cf 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1557,3 +1557,32 @@ for (const opening of ['(', '[', '{']) { ); }); } + +test('package prose cannot hide nested guidance import', async (t) => { + assert.ok( + ( + await diagnostics(t, { + 'package.json': JSON.stringify({ + dependencies: { '@angular/core': '*' }, + }), + 'CLAUDE.md': '@AGENTS.md\n\nUse @angular/core(@INSTRUCTIONS)', + INSTRUCTIONS: 'Guidance', + }) + ).some((message) => message.includes('additional or inline')) + ); +}); +for (const extension of ['pdf', 'rst', 'adoc', 'markdown', 'docm', 'latex']) { + test(`bare document literal is checked: ${extension}`, async (t) => { + const name = 'manual.' + extension; + assert.ok( + (await diagnostics(t, { 'AGENTS.md': '`' + name + '`' })).length > 0 + ); + assert.deepEqual( + await diagnostics(t, { + 'AGENTS.md': '`' + name + '`', + [name]: 'Document', + }), + [] + ); + }); +}