fix(agents): preserve filenames across prose punctuation

This commit is contained in:
4gray committed 2026-09-21 00:46:30 +02:00
1 parent 4a73cf3828
commit 82d582bb74
3 files changed
+59 -12

No files matched your search

+3 -2
View File
@@ -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.
+22 -10
View File
@@ -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 (
@@ -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': '*' },
}),
}),
[]
);
});
}