fix(agents): validate generic literal repository paths

This commit is contained in:
4gray committed 2026-09-20 18:54:43 +02:00
1 parent 8467b9406a
commit a10045f0b2
5 files changed
+99 -9

No files matched your search

+1 -1
View File
@@ -108,7 +108,7 @@ use repository documentation and available CLI discovery.
## General Guidelines for working with Nx
- For workspace exploration, use the `nx-workspace` skill when available;
otherwise inspect `project.json`, `pnpm nx show projects` and `pnpm nx graph`.
otherwise inspect project-local project.json files, `pnpm nx show projects` and `pnpm nx graph`.
- Run project tasks through local `pnpm nx`, not a global Nx installation.
- Use the Nx MCP server when available; otherwise use CLI discovery.
- Check `node_modules/@nx/<plugin>/PLUGIN.md` for plugin guidance when present.
+5
View File
@@ -31,6 +31,11 @@ Run `pnpm run agents:validate` after guidance changes. It checks line/byte budge
root imports and local navigation links/anchors, including migration destinations.
Markdown navigation is parsed with the already-declared `marked` dependency;
undefined explicit reference links are errors, and code examples are excluded.
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 recognized JavaScript globals are excluded. Use Markdown links for
ambiguous filenames that resemble code symbols.
Heading anchors are derived from parsed text, not HTML sanitization. The Nx test
hash includes `marked` so dependency changes invalidate parser coverage.
It cannot prove semantic equivalence; review changed contracts as well.
+1 -1
View File
@@ -11,7 +11,7 @@ are not prerequisites for reading repository contracts.
| Area / code ownership | Canonical documents | Repository skill |
| --- | --- | --- |
| Bootstrap, project placement, dependencies, aliases and lint configuration; root Nx config and `project.json` files | [Nx boundaries](../architecture/nx-workspace-boundaries.md), [security overrides](../architecture/dependency-security-overrides.md) | [Nx architecture](../../.codex/skills/iptvnator-nx-architecture/SKILL.md) |
| Bootstrap, project placement, dependencies, aliases and lint configuration; root Nx config and project-local project.json files | [Nx boundaries](../architecture/nx-workspace-boundaries.md), [security overrides](../architecture/dependency-security-overrides.md) | [Nx architecture](../../.codex/skills/iptvnator-nx-architecture/SKILL.md) |
| Angular conventions; docs and skills maintenance | [Agent workflow](../development/agent-workflow.md) | Use the area's skill below |
| Unit, E2E, lint and coverage; `tools/coverage` | [Validation map](../architecture/validation-map.md) | Use the area's validation section |
| Electron entry/events/preload and CDP; `apps/electron-backend` | [Debugging and trace flags](../development/electron-debugging.md), [Electron security](../architecture/electron-security.md) | Use the available global electron skill for automation |
+18 -7
View File
@@ -60,15 +60,26 @@ export function guidanceAnchors(markdown) {
}
function isLiteralRepositoryPath(token) {
const local =
/^(?:apps|libs|docs|tools|patches|\.codex|\.claude|\.github|\.changes|\.plans)\//u.test(
// A typo in the directory or a new root filename must still be checked.
// Exclude recognizable prose/code forms instead of allowlisting paths.
if (/^(?:@|--|[a-z][a-z\d+.-]*:|\/\/)/iu.test(token)) return false;
if (/[^\p{L}\p{N}_./#-]/u.test(token)) return false;
if (token.includes('YYYY-MM-DD') || /(?:^|\/)\.\.\.(?:\/|$)/u.test(token))
return false;
// Recognized JavaScript globals are code-symbol exceptions, not a claim
// that every dotted code expression can be distinguished from a filename.
if (
/^(?:window|document|console|process|globalThis|this|Math|JSON|Object|Array|Promise|Reflect|Symbol)\.[\p{L}_][\p{L}\p{N}_]*$/u.test(
token
) ||
/^(?:AGENTS\.md|CLAUDE\.md|README\.md|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|nx\.json|tsconfig\.base\.json|eslint\.config\.mjs|\.nvmrc)$/u.test(
token
);
)
)
return false;
const path = token.split('#')[0];
return (
local && !/[\s*?[\]{}<>|]/u.test(token) && !token.includes('YYYY-MM-DD')
path.includes('/') ||
/^(?:\.[\p{L}\p{N}_-]+|[\p{L}\p{N}_-][\p{L}\p{N}_.-]*\.[\p{L}][\p{L}\p{N}_-]*)$/u.test(
path
)
);
}
@@ -165,6 +165,80 @@ test('literal root paths validate without interpreting commands or globs', async
/does not exist/
);
});
for (const path of [
'lib/definitely-missing.ts',
'services/definitely-missing.ts',
'electron-builder.json',
'custom.config-format',
'.custom-config',
'arbitrary/directory',
]) {
test(`checks generic literal repository path ${path}`, async (t) => {
assert.match(
(await diagnostics(t, { [map]: '`' + path + '`' })).join('\n'),
/does not exist/
);
assert.deepEqual(
await diagnostics(t, { [map]: '`' + path + '`', [path]: '' }),
[]
);
});
}
test('AGENTS rejects the reported unlisted directory and root filename', async (t) => {
const errors = await diagnostics(t, {
'AGENTS.md':
'`services/definitely-missing.ts` and `electron-builder.json`',
});
assert.equal(errors.length, 2);
assert.match(
errors.join('\n'),
/does not exist: services\/definitely-missing\.ts/
);
assert.match(errors.join('\n'), /does not exist: electron-builder\.json/);
});
test('literal paths retain fragment checks', async (t) => {
assert.deepEqual(
await diagnostics(t, { [map]: '`docs/example.md#hello-world`' }),
[]
);
assert.match(
(await diagnostics(t, { [map]: '`docs/example.md#missing`' })).join(
'\n'
),
/missing anchor/
);
});
test('literal detection excludes commands, templates, URLs, aliases and code expressions', async (t) => {
const examples = [
'pnpm nx test web',
'node tools/example.mjs',
'--config=missing.json',
'docs/*.md',
'docs/{one,two}.md',
'docs/<topic>.md',
'.plans/YYYY-MM-DD-short-topic.md',
'docs/.../example.md',
'https://example.com/guide.md',
'file:///tmp/example.md',
'@iptvnator/services',
'@angular/core',
'node:fs/promises',
'window.electron',
'document.body',
'process.env',
'this.store',
'store.setState()',
'source?.id',
'a/b+c',
'$HOME/example.md',
];
assert.deepEqual(
await diagnostics(t, {
[map]: examples.map((value) => '`' + value + '`').join(' '),
}),
[]
);
});
test('rejects traversal and symlink escapes', async (t) => {
const rootDir = await fixture(t, {
'AGENTS.md': '[Escape](../outside.md) [Symlink](outside)',