diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 366d26c03..126fefa30 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -30,12 +30,13 @@ 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. +undefined explicit references (including shortcut images) 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. +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 `./` +prefix or Markdown link for other 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. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index 7d2a68c8b..874b1f0a1 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -8,12 +8,16 @@ const markdownLexer = new Marked({ if (token?.type !== 'text') return token; // Marked otherwise turns unresolved references into ordinary text. - // Reuse its grammar, retaining only explicit full/collapsed forms; + // Retain explicit full/collapsed forms and shortcut images; // 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); + full ?? + (collapsed?.[0].endsWith('[]') || + collapsed?.[0].startsWith('![') + ? collapsed + : undefined); if (!match) return token; return { type: 'unresolved-reference', @@ -66,15 +70,17 @@ function isLiteralRepositoryPath(token) { 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. + const path = token.split('#')[0]; + // Bare dotted identifiers are ambiguous. Recognize conventional file + // suffixes; other filenames can be made explicit with ./ or a Markdown link. + // This applies to user-defined symbols as well as JavaScript globals. if ( - /^(?:window|document|console|process|globalThis|this|Math|JSON|Object|Array|Promise|Reflect|Symbol)\.[\p{L}_][\p{L}\p{N}_]*$/u.test( - token + /^[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)+$/u.test(path) && + !/\.(?:md|mdx|json|jsonc|ya?ml|[cm]?[jt]sx?|html?|[sc]ss|sass|less|toml|xml|txt|sh|py|sql|svg|png|jpe?g|webp|gif|m3u8?|conf|ini|lock)$/iu.test( + path ) ) return false; - const path = token.split('#')[0]; return ( path.includes('/') || /^(?:\.[\p{L}\p{N}_-]+|[\p{L}\p{N}_-][\p{L}\p{N}_.-]*\.[\p{L}][\p{L}\p{N}_-]*)$/u.test( diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index 94c9e0b02..7edd3bb98 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -196,6 +196,21 @@ test('AGENTS rejects the reported unlisted directory and root filename', async ( ); assert.match(errors.join('\n'), /does not exist: electron-builder\.json/); }); +test('ambiguous dotted filenames can be explicitly marked as paths', async (t) => { + for (const reference of ['`./custom.symbol`', '[File](custom.symbol)']) { + assert.match( + (await diagnostics(t, { [map]: reference })).join('\n'), + /does not exist/ + ); + } + assert.deepEqual( + await diagnostics(t, { + [map]: '`./custom.symbol`', + 'custom.symbol': '', + }), + [] + ); +}); test('literal paths retain fragment checks', async (t) => { assert.deepEqual( await diagnostics(t, { [map]: '`docs/example.md#hello-world`' }), @@ -224,6 +239,11 @@ test('literal detection excludes commands, templates, URLs, aliases and code exp '@angular/core', 'node:fs/promises', 'window.electron', + 'Date.now', + 'String.raw', + 'Buffer.from', + 'customStore.selectedItem', + 'process.env.NODE_ENV', 'document.body', 'process.env', 'this.store', @@ -304,6 +324,7 @@ for (const reference of [ '[**Guide**][missing]', '[Guide `code`][missing]', '![Guide][missing]', + '![Diagram]', ]) { test(`diagnoses unresolved explicit reference ${reference}`, async (t) => { assert.match(