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:
Kjell Tore Guttormsen 2026-06-23 21:44:52 +02:00
commit 2082b7d112
10 changed files with 364 additions and 45 deletions

View file

@ -14,6 +14,7 @@ import { envelope } from './lib/output.mjs';
import { discoverConfigFiles, discoverConfigFilesMulti, discoverFullMachinePaths } from './lib/file-discovery.mjs';
import { loadSuppressions, applySuppressions, formatSuppressionSummary } from './lib/suppression.mjs';
import { humanizeEnvelope } from './lib/humanizer.mjs';
import { resolveContextWindow } from './lib/context-window.mjs';
// Scanner registry — import order determines execution order
import { scan as scanClaudeMd } from './claude-md-linter.mjs';
@ -94,6 +95,11 @@ export async function runAllScanners(targetPath, opts = {}) {
// and CNF duplicate-hook findings with config that loads on zero turns. (B3)
const excludeCache = opts.excludeCache !== false;
// B8 — resolve the context window once and thread it to budget-aware scanners
// (SKL, CML). Undefined opts.contextWindow → conservative 200k anchor, which is
// byte-identical to the pre-B8 default; other scanners ignore the third arg.
const contextWindow = resolveContextWindow(opts.contextWindow);
// Shared file discovery — scanners reuse this
let discovery;
if (opts.fullMachine) {
@ -112,7 +118,7 @@ export async function runAllScanners(targetPath, opts = {}) {
resetCounter();
const scanStart = Date.now();
try {
const result = await scanner.fn(resolvedPath, discovery);
const result = await scanner.fn(resolvedPath, discovery, { contextWindow });
results.push(result);
const count = result.findings.length;
const label = opts.humanizedProgress
@ -206,10 +212,13 @@ async function main() {
let outputFile = null;
let saveBaseline = false;
let baselinePath = null;
let contextWindow = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output-file' && args[i + 1]) {
outputFile = args[++i];
} else if (args[i] === '--context-window' && args[i + 1]) {
contextWindow = args[++i];
} else if (args[i] === '--save-baseline') {
saveBaseline = true;
} else if (args[i] === '--baseline' && args[i + 1]) {
@ -254,6 +263,7 @@ async function main() {
filterFixtures,
excludeCache,
humanizedProgress,
contextWindow,
});
// Default mode runs the humanizer; --json and --raw bypass for v5.0.0 byte-equal output.