/** * Context-budget capture — the ratchet's write side. * * Captures the current ALWAYS-ON + EAGER token ledgers from * `lib/context-bill.ts` into `test/fixtures/context-budget.json`, with * deliberate headroom baked into every ceiling: * * - alwaysOnTotal: actual × 1.05 (full-frontmatter catalog, aggregate) * - eagerPerInvocation: actual × 1.10 (per-skill SKILL.md + forced refs) * * Why these two ledgers and no others: they are the ledgers nothing else * measures (plan OV8). The shrink floor lives in skill-size-budget.test.ts, * growth ratios + minBytes floors in parity-suite.test.ts, the name+description * discovery cap in catalog-budget.test.ts. TOTAL overlaps those guards, so it * is deliberately not budgeted here. * * Ratchet protocol (mirrors catalog-budget.test.ts): * - Legitimate growth (a real feature grew a skill past its ceiling): * re-run `bun test/helpers/capture-context-budget.ts` and commit the * refreshed fixture IN THE SAME COMMIT as the growth, so the diff shows * the conscious decision. * - After a reduction phase lands: re-run the capture so the ceilings * ratchet DOWN and the win is locked. * * Test-fixture skill trees under test/fixtures/ are excluded — they exist to * test context-bill itself and must not couple the ratchet to test data. */ import * as fs from 'fs'; import * as path from 'path'; import { buildBill, type Bill } from '../../lib/context-bill'; export const REPO_ROOT = path.resolve(import.meta.dir, '..', '..'); export const BUDGET_FIXTURE_PATH = path.join(REPO_ROOT, 'test', 'fixtures', 'context-budget.json'); export const ALWAYS_ON_HEADROOM = 1.05; export const EAGER_HEADROOM = 1.10; /** Skills that exist only as context-bill test data — never budgeted. */ export function isFixtureSkill(name: string): boolean { return name.startsWith('test/'); } export interface ContextBudget { _comment: string; alwaysOnTotal: number; eagerPerInvocation: Record; } /** The bill the ratchet grades: repo tree minus test-fixture skill dirs. */ export function buildRatchetBill(root: string = REPO_ROOT): Bill { const bill = buildBill(root); const skills = bill.skills.filter((s) => !isFixtureSkill(s.name)); return { ...bill, skills, totals: { ...bill.totals, skillCount: skills.length, alwaysOnBytes: skills.reduce((n, s) => n + s.frontmatterBytes, 0), alwaysOnTokens: skills.reduce((n, s) => n + s.frontmatterTokens, 0), eagerBytesBySkill: Object.fromEntries(skills.map((s) => [s.name, s.eagerBytes])), eagerTokensBySkill: Object.fromEntries(skills.map((s) => [s.name, Math.round(s.eagerTokens)])), }, }; } export function captureContextBudget(root: string = REPO_ROOT): ContextBudget { const bill = buildRatchetBill(root); const eagerPerInvocation: Record = {}; for (const s of [...bill.skills].sort((a, b) => a.name.localeCompare(b.name))) { eagerPerInvocation[s.name] = Math.ceil(s.eagerTokens * EAGER_HEADROOM); } return { _comment: 'Context-budget ratchet ceilings (~tokens). Regenerate: bun test/helpers/capture-context-budget.ts. ' + `Headroom: alwaysOnTotal x${ALWAYS_ON_HEADROOM}, eagerPerInvocation x${EAGER_HEADROOM}. ` + 'Graded by test/context-budget-ratchet.test.ts via lib/context-bill.ts checkBudget.', alwaysOnTotal: Math.ceil(bill.totals.alwaysOnTokens * ALWAYS_ON_HEADROOM), eagerPerInvocation, }; } // CLI: write the fixture. if (import.meta.main) { const budget = captureContextBudget(); fs.writeFileSync(BUDGET_FIXTURE_PATH, JSON.stringify(budget, null, 2) + '\n'); const n = Object.keys(budget.eagerPerInvocation).length; console.log( `Wrote ${path.relative(REPO_ROOT, BUDGET_FIXTURE_PATH)}: alwaysOnTotal=${budget.alwaysOnTotal} tok, ${n} eager ceilings`, ); }