diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 7a919bf83..567b87c50 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -64,7 +64,7 @@ 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. Extensionless inline candidates are also imports when they resolve to repository files, -after removing prose punctuation, including Unicode separators. +checking the full filename before prefixes at ASCII/Unicode prose separators. 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. @@ -72,7 +72,8 @@ Declared packages also permit safe subpaths; exact aliases stay exact. Declared package mentions may include a version (including semver comparators) or dist-tag qualifier. Qualifier handling includes unscoped names; terminal sentence punctuation is removed before matching a declared package, as are straight/curly apostrophe possessives. -Non-ASCII punctuation separates package mentions from adjacent prose. +Unicode punctuation and ASCII commas, semicolons, colons, question/exclamation marks +separate package mentions from adjacent prose. Markdown destinations decode HTML entities before URI parsing, matching rendered links. Heading-anchor lookup is limited to Markdown targets. Source-file line fragments, PDF page fragments and other non-Markdown fragments retain file-existence checks. diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index 4d7119b4d..d4634bd8a 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -115,7 +115,7 @@ async function packageMentions(rootDir) { const scopes = new Set(declared.map((name) => name.split('/')[0])); return (raw) => { // ASCII punctuation also belongs to package names and version ranges. - let token = raw.split(/(?=[^\x00-\x7f])\p{P}/u, 1)[0]; + let token = raw.split(/[,;:!?]|(?=[^\x00-\x7f])\p{P}/u, 1)[0]; token = token.replace(/[?!.,;:)"'\]}]+$/u, ''); token = token.replace(/['’]s$/iu, ''); token = token.replace( @@ -188,15 +188,27 @@ export async function validateAgentGuidance({ rootDir }) { inlineImports.push(token); continue; } - const candidate = token - .split(/(?=[^\x00-\x7f])\p{P}/u, 1)[0] - .replace(/[?!.,;:)"'\]}]+$/u, ''); - try { - if ((await stat(resolve(rootDir, candidate))).isFile()) - inlineImports.push(token); - } catch (error) { - if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') - throw error; + // Check real filenames before interpreting punctuation as prose. + const candidates = new Set([ + token, + token.replace(/[?!.,;:)"'\]}]+$/u, ''), + ]); + for (const boundary of token.matchAll( + /[,;:!?]|(?=[^\x00-\x7f])\p{P}/gu + )) + candidates.add(token.slice(0, boundary.index)); + for (const candidate of candidates) { + try { + if ( + (await stat(resolve(rootDir, candidate))).isFile() + ) { + inlineImports.push(token); + break; + } + } catch (error) { + if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') + throw error; + } } } if ( diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index b68b652d0..fb23d6960 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1136,3 +1136,37 @@ for (const suffix of ['—then', ',next', '”']) { ); }); } + +for (const name of ['INSTRUCTIONS—v2', 'INSTRUCTIONS,extra']) { + test(`full punctuation filename remains an import: ${name}`, async (t) => { + assert.ok( + ( + await diagnostics(t, { + 'CLAUDE.md': '@AGENTS.md\n\nRead @' + name, + [name]: 'Guidance', + }) + ).some((message) => message.includes('additional or inline')) + ); + }); +} +for (const suffix of [',then', ';then', ':then', '!then', '?then']) { + test(`ASCII prose boundary handles imports and packages: ${suffix}`, async (t) => { + assert.ok( + ( + await diagnostics(t, { + 'CLAUDE.md': '@AGENTS.md\n\nRead @INSTRUCTIONS' + suffix, + INSTRUCTIONS: 'Guidance', + }) + ).some((message) => message.includes('additional or inline')) + ); + assert.deepEqual( + await diagnostics(t, { + 'AGENTS.md': 'Use @angular/core' + suffix + ' continue', + 'package.json': JSON.stringify({ + dependencies: { '@angular/core': '*' }, + }), + }), + [] + ); + }); +}