config-audit/scanners/lib/context-window.mjs
Kjell Tore Guttormsen 2082b7d112 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>
2026-06-23 21:44:52 +02:00

77 lines
3.4 KiB
JavaScript

/**
* 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, ',');
/**
* @typedef {object} ResolvedContextWindow
* @property {number} window - the context window budgets calibrate against
* @property {boolean} advisory - true when the window is unknown: keep the anchor
* but downgrade budget findings to info instead of
* firing them as a breach
* @property {'default'|'explicit'|'auto-unresolved'} source
*/
/**
* Resolve the raw `--context-window` CLI value into a window + advisory flag.
*
* Design (B8): the DEFAULT (no flag) is byte-identical to the pre-B8 behavior —
* the conservative 200k anchor at full severity. Only an explicit value changes
* calibration. `auto` asks the tool to figure out the window; until model→window
* probing ships (B8b) it cannot, so it keeps the conservative anchor but marks the
* result advisory so SKL/CML downgrade their budget findings to info rather than
* "crying wolf" with a breach on a window we cannot confirm.
*
* @param {string|number|null|undefined} arg
* @returns {ResolvedContextWindow}
*/
export function resolveContextWindow(arg) {
if (arg == null) {
return { window: CONTEXT_WINDOW_ANCHOR, advisory: false, source: 'default' };
}
if (String(arg).trim().toLowerCase() === 'auto') {
return { window: CONTEXT_WINDOW_ANCHOR, advisory: true, source: 'auto-unresolved' };
}
const n = typeof arg === 'number' ? arg : parseInt(String(arg).trim(), 10);
if (Number.isFinite(n) && n > 0) {
return { window: n, advisory: false, source: 'explicit' };
}
// Unparseable / non-positive: fall back to the conservative default (no advisory).
return { window: CONTEXT_WINDOW_ANCHOR, advisory: false, source: 'default' };
}
/**
* Scale a 200k-anchored budget to a given context window. Linear in the window,
* so it is the identity at the anchor (keeps the default byte-stable).
*
* @param {number} anchorValue - the budget/threshold defined at the 200k anchor
* @param {number} window - the target context window
* @returns {number}
*/
export function scaleForWindow(anchorValue, window) {
return Math.round(anchorValue * (window / CONTEXT_WINDOW_ANCHOR));
}