fix(agents): scan nested imports and share document suffixes

This commit is contained in:
4gray committed 2026-09-21 04:21:52 +02:00
1 parent 673d3fd791
commit 47cfc1ba4a
4 files changed
+40 -6

No files matched your search

+4 -2
View File
@@ -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.
+4
View File
@@ -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
)
+3 -4
View File
@@ -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;
@@ -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',
}),
[]
);
});
}