fix(agents): parse guidance navigation with Markdown tokens

This commit is contained in:
4gray committed 2026-09-20 18:36:14 +02:00
1 parent 3a2497476b
commit 8467b9406a
6 files changed
+251 -64

No files matched your search

+1 -1
View File
@@ -1827,7 +1827,7 @@ and playback positions; no deletion happens on merely opening the dialog.
Electron only: a `.m3u`/`.m3u8` path passed
on the command line, opened through a file association, or delivered by macOS'
`open-file` event is normalized to an absolute path in the main process
(`services/playlist-open-request.ts`) and queued there. The renderer
(`apps/electron-backend/src/app/services/playlist-open-request.ts`) and queued there. The renderer
(`apps/web/src/app/services/playlist-open-request.service.ts`) subscribes to the
`OPEN_FILE` push **before** calling `announcePlaylistOpenListener`, which is
what makes the main process flush. `OPEN_FILE` is the only way out of the
+4
View File
@@ -29,6 +29,10 @@ Later normal edits maintain the canonical docs, not duplicate historical prose.
Run `pnpm run agents:validate` after guidance changes. It checks line/byte budgets,
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.
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.
## Protected Markdown edits
+97
View File
@@ -0,0 +1,97 @@
import { Marked, Tokenizer } from 'marked';
// Lex only: no Markdown is rendered and no HTML is sanitized or re-emitted.
const markdownLexer = new Marked({
tokenizer: {
reflink(source, links) {
const token = Tokenizer.prototype.reflink.call(this, source, links);
if (token?.type !== 'text') return token;
// Marked otherwise turns unresolved references into ordinary text.
// Reuse its grammar, retaining only explicit full/collapsed forms;
// a bare [word] without a definition remains ordinary prose.
const full = this.rules.inline.reflink.exec(source);
const collapsed = this.rules.inline.nolink.exec(source);
const match =
full ?? (collapsed?.[0].endsWith('[]') ? collapsed : undefined);
if (!match) return token;
return {
type: 'unresolved-reference',
raw: match[0],
text: match[0],
label: match[2] || match[1],
};
},
},
});
function inlineText(tokens) {
return tokens
.map((token) => {
if (token.type === 'html') return '';
if (token.tokens) return inlineText(token.tokens);
return token.text ?? '';
})
.join('');
}
export function guidanceAnchors(markdown) {
const found = new Set();
const explicit = new Set();
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
if (token.type === 'heading') {
const slug = inlineText(token.tokens)
.toLowerCase()
.replace(/[^\p{L}\p{M}\p{N}_\-\s]/gu, '')
.replace(/\s/gu, '-');
let unique = slug;
let suffix = 0;
while (found.has(unique)) unique = `${slug}-${++suffix}`;
found.add(unique);
}
if (token.type === 'html') {
for (const match of token.raw.matchAll(
/<(?:a|[a-z][\w-]*)\b[^>]*\b(?:id|name)=["']([^"']+)["']/giu
))
explicit.add(match[1]);
}
});
return new Set([...found, ...explicit]);
}
function isLiteralRepositoryPath(token) {
const local =
/^(?:apps|libs|docs|tools|patches|\.codex|\.claude|\.github|\.changes|\.plans)\//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 (
local && !/[\s*?[\]{}<>|]/u.test(token) && !token.includes('YYYY-MM-DD')
);
}
export function guidanceReferences(markdown, includeLiterals) {
const result = [];
const destinations = new Set();
function add(target, literal = false) {
const key = `${literal}:${target}`;
if (!destinations.has(key)) {
destinations.add(key);
result.push({ target, literal });
}
}
markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
if (['link', 'image', 'def'].includes(token.type)) add(token.href);
if (token.type === 'unresolved-reference')
result.push({ unresolvedReference: token.label });
if (
includeLiterals &&
token.type === 'codespan' &&
isLiteralRepositoryPath(token.text)
)
add(token.text, true);
});
return result;
}
+9 -2
View File
@@ -8,7 +8,13 @@
"test": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["{projectRoot}/*.mjs", "{projectRoot}/project.json"],
"inputs": [
"{projectRoot}/*.mjs",
"{projectRoot}/project.json",
{
"externalDependencies": ["marked"]
}
],
"options": {
"command": "node --test tools/skills/validate-repository-skills.test.mjs tools/skills/validate-agent-guidance.test.mjs",
"cwd": "{workspaceRoot}"
@@ -21,7 +27,8 @@
"node --check tools/skills/validate-repository-skills.mjs",
"node --check tools/skills/validate-repository-skills.test.mjs",
"node --check tools/skills/validate-agent-guidance.mjs",
"node --check tools/skills/validate-agent-guidance.test.mjs"
"node --check tools/skills/validate-agent-guidance.test.mjs",
"node --check tools/skills/agent-guidance-markdown.mjs"
],
"cwd": "{workspaceRoot}"
},
+11 -61
View File
@@ -1,6 +1,10 @@
import { readFile, realpath } from 'node:fs/promises';
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
guidanceAnchors as anchors,
guidanceReferences as references,
} from './agent-guidance-markdown.mjs';
const SURFACES = [
'AGENTS.md',
@@ -38,67 +42,13 @@ function withoutFences(markdown) {
.join('\n');
}
function anchors(markdown) {
const found = new Set();
for (const match of withoutFences(markdown).matchAll(
/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/gmu
)) {
const slug = match[1]
.replace(/\[([^\]]+)\]\([^)]*\)/gu, '$1')
.replace(/<[^>]*>/gu, '')
.toLowerCase()
.replace(/[^\p{L}\p{M}\p{N}_\-\s]/gu, '')
.replace(/\s/gu, '-');
let unique = slug;
let suffix = 0;
while (found.has(unique)) unique = `${slug}-${++suffix}`;
found.add(unique);
}
for (const match of markdown.matchAll(
/<(?:a|[a-z][\w-]*)\b[^>]*\b(?:id|name)=["']([^"']+)["']/giu
)) {
found.add(match[1]);
}
return found;
}
function references(markdown, includeLiterals) {
const text = withoutFences(markdown);
const result = [];
// Inline destinations may use angle brackets or an optional quoted title.
const prose = text.replace(/`[^`\n]+`/gu, '');
for (const match of prose.matchAll(
/\[[^\]\n]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^\n]*?["'])?\s*\)/gu
)) {
result.push({ target: match[1] ?? match[2], literal: false });
}
for (const match of prose.matchAll(
/^\s{0,3}\[[^\]]+\]:\s*(?:<([^>]+)>|(\S+))/gmu
)) {
result.push({ target: match[1] ?? match[2], literal: false });
}
if (includeLiterals) {
for (const match of text.matchAll(/`([^`\r\n]+)`/gu)) {
const token = match[1];
const local =
/^(?:apps|libs|docs|tools|patches|\.codex|\.claude|\.github|\.changes|\.plans)\//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
);
if (
local &&
!/[\s*?[\]{}<>|]/u.test(token) &&
!token.includes('YYYY-MM-DD')
)
result.push({ target: token, literal: true });
}
}
return result;
}
async function validateReference(rootDir, source, { target, literal }) {
async function validateReference(
rootDir,
source,
{ target, literal, unresolvedReference }
) {
if (unresolvedReference !== undefined)
return `${source}: unresolved Markdown reference "${unresolvedReference}"`;
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(target)) return;
let decoded;
try {
@@ -181,3 +181,132 @@ test('requires all four guidance surfaces', async (t) => {
/agent-context-map.md.*missing/
);
});
test('balanced parentheses and formatted labels retain complete destinations', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
'[Read **[the guide]**](docs/example(1).md#hello-world)\n',
'docs/example(1).md': '# Hello, `World`!\n',
}),
[]
);
const errors = await diagnostics(t, {
'AGENTS.md': '[Read **[the guide]**](docs/missing(1).md)\n',
});
assert.match(errors.join('\n'), /does not exist: docs\/missing\(1\)\.md/);
});
test('angle and escaped Markdown destinations resolve literal filenames', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
'[Angle](<docs/guide (one).md>)\n[Escaped](docs/example\\(1\\).md)\n',
'docs/guide (one).md': '# Guide\n',
'docs/example(1).md': '# Example\n',
}),
[]
);
});
test('reference definitions resolve full, collapsed and shortcut links', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
'[Read **guide**][GUIDE]\n[Guide][]\n[Guide]\n\n[guide]: <docs/example(1).md#hello-world> "Title"\n',
'docs/example(1).md': '# Hello, `World`!\n',
}),
[]
);
const errors = await diagnostics(t, {
'AGENTS.md': '[Read guide][guide]\n\n[guide]: docs/missing(1).md\n',
});
assert.match(errors.join('\n'), /does not exist: docs\/missing\(1\)\.md/);
});
for (const reference of [
'[Guide][missing]',
'[Guide][]',
'[**Guide**][missing]',
'[Guide `code`][missing]',
'![Guide][missing]',
]) {
test(`diagnoses unresolved explicit reference ${reference}`, async (t) => {
assert.match(
(await diagnostics(t, { 'AGENTS.md': reference + '\n' })).join(
'\n'
),
/unresolved.*reference/i
);
});
}
test('lexer visits navigation inside tables, lists, and blockquotes', async (t) => {
const errors = await diagnostics(t, {
'AGENTS.md':
'| Guide |\n| --- |\n| [Table](docs/table(1).md) |\n\n- [List](docs/list(1).md)\n\n> [Quote][missing]\n',
});
assert.equal(errors.length, 3);
assert.match(errors.join('\n'), /docs\/table\(1\)\.md/);
assert.match(errors.join('\n'), /docs\/list\(1\)\.md/);
assert.match(errors.join('\n'), /unresolved.*reference/i);
});
test('fenced, indented, inline and escaped examples are not navigation', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': [
'```md',
'[Code](missing.md)',
'[Code][missing]',
'```',
'',
' [Indented](missing.md)',
' [Indented][]',
'',
'`[Inline](missing.md)` and ``[Code `label`][missing]``.',
'\\[Escaped][missing]',
].join('\n'),
}),
[]
);
});
test('heading anchors use nested token text, link labels and code spans', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
'[Heading](docs/example.md#a-bold-emphasis-label-code-hi)\n[Duplicate](docs/example.md#a-bold-emphasis-label-code-hi-1)',
'docs/example.md':
'# A **bold *emphasis*** [Label `code`](one(two).md) <em>Hi</em>\n\n# A **bold *emphasis*** [Label `code`](one(two).md) <em>Hi</em>\n',
}),
[]
);
});
test('HTML heading tokens are ignored without rendering or tag stripping', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md':
'[HTML](docs/example.md#safe-heading)\n[Nested](docs/example.md#unsafe-scriptalert1script)',
'docs/example.md':
'# Safe <span title="a > b">Heading</span>\n\n# Unsafe <scr<script>ipt>alert(1)</scr</script>ipt>\n',
}),
[]
);
});
test('explicit HTML anchors work but anchors in code examples do not', async (t) => {
assert.deepEqual(
await diagnostics(t, {
'AGENTS.md': '[Anchor](docs/example.md#explicit)',
'docs/example.md': '<a id="explicit"></a>\n',
}),
[]
);
const errors = await diagnostics(t, {
'AGENTS.md': '[Fake](docs/example.md#fake)',
'docs/example.md': '```html\n<a id="fake"></a>\n```\n',
});
assert.match(errors.join('\n'), /missing anchor/);
});