feat(skl,cml): --context-window calibration, advisory when unknown (v5.11 B8) [skip-docs]
SKL-002 (skill-listing budget) and CML char-budget now calibrate to a
resolved context window instead of always anchoring at 200k:
- resolveContextWindow(): --context-window <n> calibrates; 'auto' keeps the
conservative 200k anchor but marks advisory (model→window probing deferred
to B8b); no flag → 200k anchor, byte-identical to pre-B8 default.
- scaleForWindow(): linear off the 200k anchor (identity at the anchor).
- SKL + CML each keep an untouched default branch (window===200k && !advisory)
for byte-stability and a calibrated branch; advisory downgrades the budget
finding from a breach (low/medium) to info.
- Flag wired through scan-orchestrator + posture; runAllScanners resolves once
and threads { contextWindow } to scanners (others ignore the 3rd arg).
- CPS intentionally excluded: it has no window-anchored budget (fixed
150-line volatility heuristic), so there is nothing to calibrate.
15 new tests; e2e CLI verified (1M suppresses SKL-002, auto → info, default
unchanged); full suite 1279 green; snapshots byte-stable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
27988801be
commit
2082b7d112
10 changed files with 364 additions and 45 deletions
|
|
@ -9,13 +9,18 @@ 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';
|
||||
import { CONTEXT_WINDOW_ANCHOR, LARGE_CONTEXT_WINDOW, LARGE_CONTEXT_SCALE, scaleForWindow, withCommas } from './lib/context-window.mjs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const SCANNER = 'CML';
|
||||
const MAX_RECOMMENDED_LINES = 200;
|
||||
const MAX_ABSOLUTE_LINES = 500;
|
||||
|
||||
// Shared remediation for the char-budget finding (byte-identical across the
|
||||
// default and the B8 window-calibrated branches).
|
||||
const CHAR_BUDGET_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.';
|
||||
|
||||
// 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
|
||||
|
|
@ -39,10 +44,20 @@ const RECOMMENDED_SECTIONS = [
|
|||
* @param {{ files: import('./lib/file-discovery.mjs').ConfigFile[] }} discovery
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function scan(targetPath, discovery) {
|
||||
export async function scan(targetPath, discovery, opts = {}) {
|
||||
const start = Date.now();
|
||||
const claudeFiles = discovery.files.filter(f => f.type === 'claude-md');
|
||||
|
||||
// B8 — calibrate the char-budget threshold to the resolved context window. The
|
||||
// default (no opts) is the conservative 200k anchor (40k chars) at full
|
||||
// severity — byte-identical to the pre-B8 finding. An unknown (advisory) window
|
||||
// keeps the anchor but downgrades the finding to info instead of a breach.
|
||||
const cw = opts.contextWindow;
|
||||
const window = (cw && typeof cw.window === 'number') ? cw.window : CONTEXT_WINDOW_ANCHOR;
|
||||
const advisory = !!(cw && cw.advisory);
|
||||
const isDefaultWindow = window === CONTEXT_WINDOW_ANCHOR && !advisory;
|
||||
const charThreshold = scaleForWindow(CLAUDE_MD_CHAR_WARN_ANCHOR, window);
|
||||
|
||||
if (claudeFiles.length === 0) {
|
||||
return scannerResult(SCANNER, 'ok', [
|
||||
finding({
|
||||
|
|
@ -122,17 +137,35 @@ export async function scan(targetPath, discovery) {
|
|||
// 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,
|
||||
}));
|
||||
if (chars > charThreshold) {
|
||||
if (isDefaultWindow) {
|
||||
// Conservative 200k anchor — byte-identical to the pre-B8 finding.
|
||||
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: CHAR_BUDGET_RECOMMENDATION,
|
||||
autoFixable: false,
|
||||
}));
|
||||
} else {
|
||||
// B8 — window-calibrated. Advisory (unknown window) downgrades to info.
|
||||
const winLabel = withCommas(window);
|
||||
const threshLabel = withCommas(charThreshold);
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: advisory ? SEVERITY.info : SEVERITY.medium,
|
||||
title: 'CLAUDE.md exceeds Claude Code\'s performance-warning threshold',
|
||||
description: `${file.relPath} is ${withCommas(chars)} chars, over the ~${threshLabel}-char performance-warning threshold Claude Code applies at a ${winLabel}-token context window (it scales the ~40.0k-char @ 200k warning by the context window, CC 2.1.169) — it loads in full on every turn.` +
|
||||
(advisory ? ' Your context window is unknown, so this anchors on the conservative 200k window — advisory.' : ''),
|
||||
file: file.absPath,
|
||||
evidence: `${withCommas(chars)} chars > ${threshLabel} (calibrated to a ${winLabel}-token context window). This is an estimate, not measured telemetry.`,
|
||||
recommendation: CHAR_BUDGET_RECOMMENDATION,
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Empty file ---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue