size-ledger.ts
69 lines · scripts/size-ledger.ts at 3f79d81
1#!/usr/bin/env -S bun --no-env-file --config=/dev/null
2/**
3 * Size ledger for a carved skill's union bytes (GSTA-132; format in
4 * test/helpers/size-ledger.ts).
5 *
6 * size-ledger.ts record <skill> --why <text> [--branch <name>]
7 * set this branch's entry to what this tree's union bytes differ from the
8 * base plus every other entry; a zero difference deletes the entry
9 * size-ledger.ts check <skill>
10 * print measured, expected and each entry; exit 1 on a mismatch
11 * size-ledger.ts fold <skill> --measured-at <text>
12 * move the entries' sum into base.json and delete them (a tidy-up PR,
13 * never a stamp commit); refuses on a mismatch
14 */
15import { spawnSync } from 'child_process';
16import { CARVE_GUARDS } from '../test/helpers/carve-guards';
17import { readSkillForParity } from '../test/helpers/parity-harness';
18import { foldLedger, ledgerExpected, ledgerFailure, readSizeLedger, recordEntry } from '../test/helpers/size-ledger';
20const USAGE = 'usage: size-ledger.ts record <skill> --why <text> [--branch <name>] | check <skill> | fold <skill> --measured-at <text>';
22function fail(msg: string, code = 2): never {
23 console.error(`size-ledger: ${msg}`);
24 process.exit(code);
25}
27function flag(args: string[], name: string): string | undefined {
28 const i = args.indexOf(name);
29 if (i < 0) return undefined;
30 const v = args[i + 1];
31 if (!v || v.startsWith('--')) fail(`${name} needs a value`);
32 return v;
33}
35const [cmd, skill, ...rest] = process.argv.slice(2);
36if (!cmd || !skill || !['record', 'check', 'fold'].includes(cmd)) fail(USAGE);
37const root = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8', timeout: 10_000 }).stdout?.trim() || process.cwd();
38const dir = CARVE_GUARDS[skill]?.sizeLedger;
39if (!dir) fail(`${skill} has no sizeLedger in test/helpers/carve-guards.ts`);
40const measured = readSkillForParity(root, skill, true).unionBytes;
41const ledger = readSizeLedger(root, dir);
42if ('error' in ledger) fail(ledger.error, 1);
44if (cmd === 'check') {
45 console.log(`base ${ledger.base.union_bytes} (${ledger.base.measured_at})`);
46 for (const e of ledger.entries) console.log(`${e.bytes > 0 ? '+' : ''}${e.bytes} ${e.file}: ${e.why}`);
47 console.log(`expected ${ledgerExpected(ledger)} measured ${measured}`);
48 const failure = ledgerFailure(skill, measured, ledger);
49 if (failure) fail(failure, 1);
50 console.log('SIZE_LEDGER: ok');
51} else if (cmd === 'record') {
52 const why = flag(rest, '--why');
53 if (!why?.trim()) fail('record needs --why "<what changed>"');
54 const branch = flag(rest, '--branch') ?? spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf-8', timeout: 10_000 }).stdout?.trim();
55 if (!branch || branch === 'HEAD') fail('detached HEAD: pass --branch <name>');
56 if (branch === 'main' || branch === 'master') fail(`refusing to record on ${branch}; record on the branch that made the change`);
57 let r: { file: string; bytes: number };
58 try { r = recordEntry(root, ledger, measured, branch, why); } catch (err) { fail((err as Error).message); }
59 console.log(r.bytes === 0
60 ? `SIZE_LEDGER: ${dir}/${r.file} not needed (this tree matches the ledger without it); expected ${measured}`
61 : `SIZE_LEDGER: ${dir}/${r.file} ${r.bytes > 0 ? '+' : ''}${r.bytes}; expected ${measured}`);
62} else {
63 const at = flag(rest, '--measured-at');
64 if (!at?.trim()) fail('fold needs --measured-at "<tree it was measured on>"');
65 const failure = ledgerFailure(skill, measured, ledger);
66 if (failure) fail(`fold refused: ${failure}`, 1);
67 foldLedger(root, ledger, at);
68 console.log(`SIZE_LEDGER: folded ${ledger.entries.length} entries into ${dir}/base.json = ${measured}`);
69}