feat(cml): context-window-scaled CLAUDE.md char budget (mirrors CC 40.0k warning)

Add a char-based CML finding that mirrors Claude Code's own startup warning
("Large CLAUDE.md will impact performance (X chars > 40.0k)"). CC 2.1.169 scales
that threshold with the model's context window, so the finding anchors on the
conservative 200k window (we cannot observe the user's window; the anchor fires
earliest) and discloses the relaxed ~200,000-char figure at 1M context. MEDIUM
severity (token cost, not an adherence cliff — consistent with the v5.2.0 reframe).

Keyed on chars, not lines, so it is complementary to the existing 200/500-line
checks (which stay untouched): a file can be long by lines yet under budget (short
lines, e.g. large-cascade at 37k chars / 1024 lines), or short by lines yet over it.

Extract the shared 200k/1M context-window constants to scanners/lib/context-window.mjs
(single source of truth; skill-listing-budget.mjs now imports + re-exports them).

40.0k figure and context-window scaling verified against the CC changelog (2.1.169,
2026-06-08) and the live startup-warning text. +6 tests, new fixture large-claude-chars
(48,531 chars / 100 lines). Suite 918/918, self-audit PASS configGrade A 97.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
Kjell Tore Guttormsen 2026-06-19 13:34:40 +02:00
commit b0bf8c5817
7 changed files with 253 additions and 8 deletions

View file

@ -9,11 +9,21 @@ import { finding, scannerResult, resetCounter } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { parseFrontmatter, extractSections, findImports } from './lib/yaml-parser.mjs';
import { lineCount, truncate } from './lib/string-utils.mjs';
import { LARGE_CONTEXT_WINDOW, LARGE_CONTEXT_SCALE, withCommas } from './lib/context-window.mjs';
const SCANNER = 'CML';
const MAX_RECOMMENDED_LINES = 200;
const MAX_ABSOLUTE_LINES = 500;
// Claude Code's own startup warning ("Large CLAUDE.md will impact performance
// (X chars > 40.0k)") fires once a CLAUDE.md passes ~40.0k chars on a
// 200k-context model. CC 2.1.169 made that threshold scale with the model's
// context window. We mirror it in the same unit CC uses (chars, not lines):
// anchor on the conservative 200k window (we cannot observe the user's window,
// and the anchor fires earliest) and disclose the relaxed 1M figure.
const CLAUDE_MD_CHAR_WARN_ANCHOR = 40_000; // chars @ 200k context (CC startup warning)
const CLAUDE_MD_CHAR_WARN_LARGE = CLAUDE_MD_CHAR_WARN_ANCHOR * LARGE_CONTEXT_SCALE; // 200,000 @ 1M
/** Recommended sections for a project CLAUDE.md */
const RECOMMENDED_SECTIONS = [
{ pattern: /project|overview|description|what/i, label: 'Project overview' },
@ -87,6 +97,26 @@ export async function scan(targetPath, discovery) {
}));
}
// --- Char budget (mirrors Claude Code's own startup warning) ---
// Keyed on chars, not lines: CC's "Large CLAUDE.md will impact performance"
// warning is char-based (~40.0k @ 200k context) and CC 2.1.169 scales that
// threshold with the context window. A file can be long by lines yet under
// this budget (short lines), or short by lines yet over it (long lines), so
// this is complementary to the line-count checks above.
const chars = content.length;
if (chars > CLAUDE_MD_CHAR_WARN_ANCHOR) {
findings.push(finding({
scanner: SCANNER,
severity: SEVERITY.medium,
title: 'CLAUDE.md exceeds Claude Code\'s performance-warning threshold',
description: `${file.relPath} is ${withCommas(chars)} chars. Claude Code shows a startup warning ("Large CLAUDE.md will impact performance ... chars > 40.0k") once a CLAUDE.md passes ~40.0k chars on a 200k-context model — it loads in full on every turn. CC 2.1.169 scales that threshold with the context window, so on a ${withCommas(LARGE_CONTEXT_WINDOW)}-token model it relaxes to ~${withCommas(CLAUDE_MD_CHAR_WARN_LARGE)} chars and you are likely within it.`,
file: file.absPath,
evidence: `${withCommas(chars)} chars > 40.0k (200k-context anchor; ~${withCommas(CLAUDE_MD_CHAR_WARN_LARGE)} at ${withCommas(LARGE_CONTEXT_WINDOW)} context). This is an estimate, not measured telemetry.`,
recommendation: 'Split detail into @imports and .claude/rules/ files so only the relevant rules load, and keep the top of CLAUDE.md byte-stable for cache hits.',
autoFixable: false,
}));
}
// --- Empty file ---
if (lines < 3) {
findings.push(finding({

View file

@ -0,0 +1,28 @@
/**
* Context-window constants single source of truth.
*
* Several Claude Code budgets scale with the model's context window:
* - the skill listing is allotted ~2% of context (CC 2.1.32, changelog L2860);
* - the "CLAUDE.md is too long" warning threshold scales with it (CC 2.1.169).
*
* We cannot observe the user's actual context window, so scanners anchor on a
* conservative 200k window (the smallest common size it fires earliest, the
* safe default when the window is unknown) and disclose the relaxed 1M figure.
*
* Zero external dependencies.
*/
// Conservative anchor: the smallest common context window. Budgets anchored
// here fire earliest, which is the safe default when the window is unknown.
export const CONTEXT_WINDOW_ANCHOR = 200_000;
// Large context window (Opus/Sonnet 1M tier). Used to disclose how a budget
// relaxes on large-context models.
export const LARGE_CONTEXT_WINDOW = 1_000_000;
// How much larger the 1M window is than the 200k anchor (= 5). A budget that
// scales linearly with the window relaxes by this factor at 1M.
export const LARGE_CONTEXT_SCALE = LARGE_CONTEXT_WINDOW / CONTEXT_WINDOW_ANCHOR;
// Dependency-free thousands separator (repo invariant: zero external deps).
export const withCommas = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');

View file

@ -23,6 +23,7 @@ import { join } from 'node:path';
import { estimateTokens, enumeratePlugins, enumerateSkills } from './active-config-reader.mjs';
import { readTextFile } from './file-discovery.mjs';
import { parseFrontmatter, parseJson } from './yaml-parser.mjs';
import { CONTEXT_WINDOW_ANCHOR, LARGE_CONTEXT_WINDOW, withCommas } from './context-window.mjs';
// Verified per-description skill-listing cap (CC 2.1.105, changelog L1502).
// Descriptions longer than this are truncated in the listing the model sees.
@ -32,14 +33,13 @@ export const DESCRIPTION_CAP = 1536;
// model reads is allotted ~2% of the context window. The context window is
// unknown, so we anchor on a conservative 200k window — the smallest common
// size, which fires earliest — and disclose the assumption in the evidence.
// The 200k/1M window constants live in context-window.mjs (single source of
// truth, shared with the CML CLAUDE.md char-budget check); re-exported here so
// existing importers of this module keep working.
export const BUDGET_FRACTION = 0.02;
export const CONTEXT_WINDOW_ANCHOR = 200_000;
export const AGGREGATE_BUDGET_TOKENS = Math.round(BUDGET_FRACTION * CONTEXT_WINDOW_ANCHOR); // 4000
export const LARGE_CONTEXT_WINDOW = 1_000_000;
export const LARGE_CONTEXT_BUDGET_TOKENS = Math.round(BUDGET_FRACTION * LARGE_CONTEXT_WINDOW); // 20000
// Dependency-free thousands separator (repo invariant: zero external deps).
export const withCommas = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
export { CONTEXT_WINDOW_ANCHOR, LARGE_CONTEXT_WINDOW, withCommas };
// The honest framing required because the budget depends on a context window we
// cannot observe (jf. TOK CALIBRATION_NOTE). Appended to budget-overflow evidence.