refactor(skl): extract skill-listing budget to shared lib (single source of truth)

Chunk 1 of the disableBundledSkills GAP feature. Moves the per-description cap,
aggregate budget constants, calibration note, and the enumerate-and-measure step
out of skill-listing-scanner into scanners/lib/skill-listing-budget.mjs — so SKL
(diagnoses overflow) and the upcoming GAP check (prescribes disableBundledSkills)
consume one budget definition instead of two divergent copies.

- New lib: assessSkillListingBudget (pure aggregate math) + measureActiveSkillListing
  (HOME-scoped enumerate-and-measure wrapper).
- SKL delegates measurement; all finding strings kept byte-identical. 18/18 SKL
  tests pass unchanged → behavior-neutral refactor.
- 12 new lib unit tests pin the budget contract. Suite 875 -> 887.
- README badge + CLAUDE.md test counts synced (self-audit --check-readme: passed).

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-18 21:23:01 +02:00
commit 0a631e3061
5 changed files with 250 additions and 57 deletions

View file

@ -0,0 +1,100 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
assessSkillListingBudget,
DESCRIPTION_CAP,
AGGREGATE_BUDGET_TOKENS,
CONTEXT_WINDOW_ANCHOR,
LARGE_CONTEXT_WINDOW,
LARGE_CONTEXT_BUDGET_TOKENS,
withCommas,
BUDGET_CALIBRATION_NOTE,
} from '../../scanners/lib/skill-listing-budget.mjs';
// This lib is the single source of truth for the skill-listing budget that both
// the SKL scanner (diagnoses overflow) and the GAP scanner (recommends the
// disableBundledSkills lever) consume. The constants and aggregate math here
// must match what SKL shipped in v5.2.0 — these tests pin that contract.
describe('skill-listing-budget — constants', () => {
it('DESCRIPTION_CAP is the verified 1536-char listing cap (CC 2.1.105)', () => {
assert.equal(DESCRIPTION_CAP, 1536);
});
it('AGGREGATE_BUDGET_TOKENS is 2% of the conservative 200k anchor', () => {
assert.equal(CONTEXT_WINDOW_ANCHOR, 200_000);
assert.equal(AGGREGATE_BUDGET_TOKENS, 4000);
});
it('LARGE_CONTEXT_BUDGET_TOKENS is 2% of the 1M window', () => {
assert.equal(LARGE_CONTEXT_WINDOW, 1_000_000);
assert.equal(LARGE_CONTEXT_BUDGET_TOKENS, 20000);
});
});
describe('withCommas', () => {
it('inserts thousands separators', () => {
assert.equal(withCommas(1_000_000), '1,000,000');
assert.equal(withCommas(4000), '4,000');
assert.equal(withCommas(999), '999');
assert.equal(withCommas(0), '0');
});
});
describe('BUDGET_CALIBRATION_NOTE', () => {
it('discloses the 1M-context scaling and flags this as an estimate, not telemetry', () => {
assert.match(BUDGET_CALIBRATION_NOTE, /1,000,000/);
assert.match(BUDGET_CALIBRATION_NOTE, /20,000/);
assert.match(BUDGET_CALIBRATION_NOTE, /estimate/i);
});
});
describe('assessSkillListingBudget — aggregate math', () => {
it('sums each description length capped at the 1536 cap', () => {
// 1000 + 1000 + min(5000,1536) = 3536
const r = assessSkillListingBudget([1000, 1000, 5000]);
assert.equal(r.aggregateChars, 3536);
});
it('scanned counts every entry, including empty descriptions', () => {
const r = assessSkillListingBudget([0, 0, 100]);
assert.equal(r.scanned, 3);
assert.equal(r.aggregateChars, 100);
});
it('aggregateTokens mirrors estimateTokens markdown (ceil chars/4)', () => {
const r = assessSkillListingBudget([16]); // 16 chars -> 4 tokens
assert.equal(r.aggregateTokens, 4);
});
it('is NOT over budget exactly at the budget (16×1000 chars -> 4000 tok)', () => {
const r = assessSkillListingBudget(Array(16).fill(1000));
assert.equal(r.aggregateChars, 16000);
assert.equal(r.aggregateTokens, 4000);
assert.equal(r.overBudget, false);
assert.equal(r.overBy, 0);
});
it('is over budget one skill past the budget (17×1000 chars -> 4250 tok)', () => {
const r = assessSkillListingBudget(Array(17).fill(1000));
assert.equal(r.aggregateTokens, 4250);
assert.equal(r.overBudget, true);
assert.equal(r.overBy, 250);
});
it('exposes budgetTokens and handles the empty case', () => {
const r = assessSkillListingBudget([]);
assert.equal(r.budgetTokens, AGGREGATE_BUDGET_TOKENS);
assert.equal(r.scanned, 0);
assert.equal(r.aggregateChars, 0);
assert.equal(r.aggregateTokens, 0);
assert.equal(r.overBudget, false);
assert.equal(r.overBy, 0);
});
it('treats negative or non-finite lengths as zero contribution', () => {
const r = assessSkillListingBudget([-5, NaN, 100]);
assert.equal(r.aggregateChars, 100);
assert.equal(r.scanned, 3);
});
});