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:
parent
157690993f
commit
0a631e3061
5 changed files with 250 additions and 57 deletions
100
tests/lib/skill-listing-budget.test.mjs
Normal file
100
tests/lib/skill-listing-budget.test.mjs
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue