From bbbaebce83d9a91a7810389e8341eaf28549d9eb Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 21 Sep 2026 00:10:57 +0200 Subject: [PATCH] fix(agents): count all Markdown line endings in budgets --- docs/development/agent-workflow.md | 1 + tools/skills/validate-agent-guidance.mjs | 4 ++- tools/skills/validate-agent-guidance.test.mjs | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 8cec5a627..bf63e7a44 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -29,6 +29,7 @@ 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. +Line budgets count LF, CRLF and standalone CR endings consistently. Markdown navigation is parsed with the already-declared `marked` dependency; 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 diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index db88f64c8..7ed5aa9d6 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -157,7 +157,9 @@ export async function validateAgentGuidance({ rootDir }) { const lines = markdown === '' ? 0 - : markdown.replace(/\r?\n$/u, '').split(/\r?\n/u).length; + : markdown + .replace(/(?:\r\n|[\r\n])$/u, '') + .split(/\r\n|[\r\n]/u).length; const bytes = Buffer.byteLength(markdown, 'utf8'); if (lines > maxLines) diagnostics.push( diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index cb6378003..2e997b531 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1081,3 +1081,31 @@ test('valid image-map navigation and inert areas pass', async (t) => { [] ); }); + +for (const ending of ['\n', '\r\n', '\r']) { + for (const [source, limit] of [ + ['AGENTS.md', 200], + ['CLAUDE.md', 30], + ]) { + test(`line budget handles ${JSON.stringify(ending)} in ${source}`, async (t) => { + const lines = [ + source === 'CLAUDE.md' ? '@AGENTS.md' : '# Guidance', + ...Array(limit - 1).fill('# Section'), + ]; + assert.deepEqual( + await diagnostics(t, { [source]: lines.join(ending) + ending }), + [] + ); + const result = await diagnostics(t, { + [source]: [...lines, '# Extra'].join(ending) + ending, + }); + assert.ok( + result.some((message) => + message.includes( + `at most ${limit} lines allowed (received ${limit + 1})` + ) + ) + ); + }); + } +}