mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
docs/inventory/evidence.json holds one row per inventory key: the skill folders discoverTemplates() finds (root skill as gstack, symlinked folders excluded) plus seven shared parts, 71 keys. lib/inventory.ts validates each row by its status's own fields, with named codes (LEDGER_*), and joins the ledger with lib/context-bill.ts buildBill. bin/gstack-inventory writes docs/inventory.md, --check validates (exit 1 with codes), --json prints. Paid dispatches per run for /ship and /autoplan are quoted from their own docs and found by line, so a moved citation fails --check (INVENTORY_SOURCE_MISSING). Every current row is grandfathered and seeded from GEVA-2's table (org-gstack-gbrain/research/gstack-part-evidence.json): in-flight fair tests and proposed registry rows as plans, earlier unfair results under prior with why (stub, unmasked, ceiling, ...), five safety rows. gstack-skill-bill prints the skill's EVIDENCE line. Evidence: plan none (owner ruling R56, GST-914); the ledger itself is the measurement record Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
189 lines
12 KiB
TypeScript
189 lines
12 KiB
TypeScript
/**
|
|
* R56 (ETHOS.md: 9. Earn Its Place): every part of gstack has a row in
|
|
* docs/inventory/evidence.json, validated by its status's own fields, and
|
|
* bin/gstack-inventory ranks the parts by load per run into docs/inventory.md.
|
|
*/
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { discoverTemplates } from '../scripts/discover-skills';
|
|
import { DISPATCHES, LEDGER_PATH, ROOT_KEY, SHARED_KEYS, SHARED_PARTS, inventoryKeys, keyOf, validateEntry, validateLedger } from '../lib/inventory';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const BIN = path.join(ROOT, 'bin', 'gstack-inventory');
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-inventory-'));
|
|
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
|
|
const codes = (key: string, entry: unknown) => validateEntry(key, entry).map(p => p.code);
|
|
const run = (args: string[]) => spawnSync(BIN, args, { cwd: ROOT, encoding: 'utf8', timeout: 120_000 });
|
|
|
|
const VERDICT = { sources: ['gstack-evals run r9 results.json'], models: ['claude-opus-5-5', 'gpt-6.1-sol'], prompt: 'preset', scope: 'the full procedure, told', masked: true };
|
|
const NEW_UNTESTED = { status: 'untested', reason: 'catches missed migrations', plan: 'test/fixtures/skill-evals/demo/tasks.json', owner: 'GSTA-168', measure_by: '2026-12-01' };
|
|
|
|
describe('the key list', () => {
|
|
test('keys are the skill directories discoverTemplates finds, the root skill as gstack, then the fixed shared keys', () => {
|
|
const keys = inventoryKeys(ROOT);
|
|
const skills = discoverTemplates(ROOT).map(t => path.dirname(t.tmpl)).map(d => (d === '.' ? ROOT_KEY : d));
|
|
expect(keys).toEqual([...skills.sort(), ...SHARED_KEYS]);
|
|
expect(keys).toContain('gstack');
|
|
expect(keys).toContain('gstack-upgrade');
|
|
expect(keys).not.toContain('connect-chrome');
|
|
expect(new Set(keys).size).toBe(keys.length);
|
|
});
|
|
|
|
test('installed names map to keys: gstack-<name> to <name>, gstack-upgrade stays literal', () => {
|
|
expect(keyOf('gstack-ship')).toBe('ship');
|
|
expect(keyOf('ship')).toBe('ship');
|
|
expect(keyOf('gstack-upgrade')).toBe('gstack-upgrade');
|
|
expect(keyOf('gstack')).toBe('gstack');
|
|
});
|
|
});
|
|
|
|
describe('the ledger covers every key and validates', () => {
|
|
const ledger = JSON.parse(fs.readFileSync(path.join(ROOT, LEDGER_PATH), 'utf8'));
|
|
|
|
test('no missing and no extra keys, and every row passes its status schema', () => {
|
|
const keys = inventoryKeys(ROOT);
|
|
expect(Object.keys(ledger.parts).filter(k => !keys.includes(k))).toEqual([]);
|
|
expect(keys.filter(k => !(k in ledger.parts))).toEqual([]);
|
|
expect(validateLedger(ledger, keys)).toEqual([]);
|
|
});
|
|
|
|
test('the initial backlog is grandfathered and the guards are safety rows', () => {
|
|
for (const [key, row] of Object.entries<any>(ledger.parts)) expect([key, row.grandfathered]).toEqual([key, true]);
|
|
for (const key of ['careful', 'freeze', 'guard', 'unfreeze', 'cso']) expect(ledger.parts[key].status).toBe('safety');
|
|
for (const key of ['capy-skill', 'capy-factory']) expect(ledger.parts[key]).toMatchObject({ status: 'untested', plan: 'none' });
|
|
});
|
|
|
|
test('a missing and an extra key are each named', () => {
|
|
const keys = inventoryKeys(ROOT);
|
|
const { ship, ...rest } = ledger.parts;
|
|
const problems = validateLedger({ ...ledger, parts: { ...rest, 'no-such-part': ship } }, keys);
|
|
expect(problems.map(p => `${p.key} ${p.code}`)).toEqual(['ship LEDGER_KEY_MISSING', 'no-such-part LEDGER_KEY_EXTRA']);
|
|
});
|
|
});
|
|
|
|
describe('the per-status schema', () => {
|
|
test('passing rows: a grandfathered untested part with plan none, a new part with a dated plan, a fair verdict, a safety guard', () => {
|
|
expect(codes('a', { status: 'untested', grandfathered: true, plan: 'none' })).toEqual([]);
|
|
expect(codes('a', NEW_UNTESTED)).toEqual([]);
|
|
expect(codes('a', { ...NEW_UNTESTED, plan: 'docs/evals/demo/PREREG.md' })).toEqual([]);
|
|
expect(codes('a', { status: 'helps', ...VERDICT })).toEqual([]);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, models: ['claude-opus-5-5', 'particle-1'] })).toEqual([]);
|
|
expect(codes('a', { status: 'no-effect', ...VERDICT, r48: { verdict: 'CUT-CANDIDATE', ref: 'why-null.md' } })).toEqual([]);
|
|
expect(codes('a', { status: 'safety', reason: 'blocks rm -rf' })).toEqual([]);
|
|
expect(codes('a', { status: 'untested', grandfathered: true, plan: 'none', prior: [{ ref: 'run 7', why: ['stub', 'unmasked'] }] })).toEqual([]);
|
|
});
|
|
|
|
test('a new untested part needs reason, a concrete plan, owner and measure_by; plan none is refused', () => {
|
|
const { measure_by, ...noDate } = NEW_UNTESTED;
|
|
expect(codes('a', noDate)).toEqual(['LEDGER_MEASURE_BY_MISSING']);
|
|
expect(codes('a', { ...NEW_UNTESTED, measure_by: 'soon' })).toEqual(['LEDGER_MEASURE_BY_MISSING']);
|
|
const { owner, ...noOwner } = NEW_UNTESTED;
|
|
expect(codes('a', noOwner)).toEqual(['LEDGER_OWNER_MISSING']);
|
|
const { reason, ...noReason } = NEW_UNTESTED;
|
|
expect(codes('a', noReason)).toEqual(['LEDGER_REASON_MISSING']);
|
|
expect(codes('a', { ...NEW_UNTESTED, plan: 'none' })).toEqual(['LEDGER_PLAN_NONE_NEW']);
|
|
expect(codes('a', { ...NEW_UNTESTED, plan: 'we will test it later' })).toEqual(['LEDGER_PLAN_NOT_CONCRETE']);
|
|
expect(codes('a', { status: 'untested', grandfathered: true })).toEqual(['LEDGER_PLAN_MISSING']);
|
|
});
|
|
|
|
test('helps, no-effect and hurts need sources, frontier models, the preset prompt, a scope and a masked no-gstack arm', () => {
|
|
const { sources, ...noSources } = VERDICT;
|
|
expect(codes('a', { status: 'helps', ...noSources })).toEqual(['LEDGER_SOURCES_MISSING']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, sources: [] })).toEqual(['LEDGER_SOURCES_MISSING']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, models: ['claude-haiku-5-5'] })).toEqual(['LEDGER_MODELS_NOT_FRONTIER']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, models: ['claude-opus-5-5', 'claude-sonnet-5-5'] })).toEqual(['LEDGER_MODELS_NOT_FRONTIER']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, models: ['claude-opus-5-5', 'particle-1', 'deepseek-v4.1-flash'] })).toEqual(['LEDGER_MODELS_NOT_FRONTIER']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, prompt: 'sdk-stub' })).toEqual(['LEDGER_PROMPT_NOT_PRESET']);
|
|
expect(codes('a', { status: 'hurts', ...VERDICT, scope: '', r48: { verdict: 'KEEP', ref: 'x' } })).toEqual(['LEDGER_SCOPE_MISSING']);
|
|
expect(codes('a', { status: 'helps', ...VERDICT, masked: false })).toEqual(['LEDGER_UNMASKED']);
|
|
});
|
|
|
|
test('no-effect and hurts without an R48 verdict and its reference are refused', () => {
|
|
expect(codes('a', { status: 'no-effect', ...VERDICT })).toEqual(['LEDGER_R48_MISSING']);
|
|
expect(codes('a', { status: 'hurts', ...VERDICT, r48: { verdict: 'DROP', ref: 'x' } })).toEqual(['LEDGER_R48_MISSING']);
|
|
expect(codes('a', { status: 'hurts', ...VERDICT, r48: { verdict: 'KEEP' } })).toEqual(['LEDGER_R48_MISSING']);
|
|
});
|
|
|
|
test('safety needs a reason; unknown statuses, unknown fields and bad prior entries are refused', () => {
|
|
expect(codes('a', { status: 'safety' })).toEqual(['LEDGER_REASON_MISSING']);
|
|
expect(codes('a', { status: 'exempt', reason: 'x' })).toEqual(['LEDGER_STATUS_INVALID']);
|
|
expect(codes('a', { status: 'safety', reason: 'x', source: 'typo' })).toEqual(['LEDGER_FIELD_UNKNOWN']);
|
|
expect(codes('a', { status: 'untested', grandfathered: true, plan: 'none', prior: [{ ref: 'run 7', why: ['vibes'] }] })).toEqual(['LEDGER_PRIOR_INVALID']);
|
|
expect(codes('a', { status: 'untested', grandfathered: true, plan: 'none', prior: [{ why: ['stub'] }] })).toEqual(['LEDGER_PRIOR_INVALID']);
|
|
});
|
|
});
|
|
|
|
/** A small gstack-shaped tree: a root skill, two skills, the shared-part files and the cited dispatch sources. */
|
|
function fixtureTree(dir: string, parts: (keys: string[]) => Record<string, unknown>): string {
|
|
const write = (rel: string, text: string) => { fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); fs.writeFileSync(path.join(dir, rel), text); };
|
|
const skill = (rel: string, name: string, tier: number, body: string) => {
|
|
const fm = `---\nname: ${name}\npreamble-tier: ${tier}\ndescription: |\n Does ${name} things.\n---\n`;
|
|
write(path.join(rel, 'SKILL.md.tmpl'), fm + body);
|
|
write(path.join(rel, 'SKILL.md'), fm + '<!-- AUTO-GENERATED from SKILL.md.tmpl -->\n' + body);
|
|
};
|
|
skill('', 'gstack', 1, 'router\n');
|
|
skill('alpha', 'alpha', 2, 'a'.repeat(4000) + '\n');
|
|
skill('beta', 'beta', 2, 'b'.repeat(1000) + '\n');
|
|
write('alpha/sections/preamble.md', 'p'.repeat(800));
|
|
write('beta/sections/preamble.md', 'p'.repeat(800));
|
|
for (const part of SHARED_PARTS) for (const f of part.files) write(f, 'x'.repeat(500) + '\n');
|
|
for (const d of DISPATCHES) if (!fs.existsSync(path.join(dir, d.file))) write(d.file, 'filler\n');
|
|
for (const d of DISPATCHES) fs.appendFileSync(path.join(dir, d.file), `${d.quote}\n`);
|
|
const keys = inventoryKeys(dir);
|
|
write(LEDGER_PATH, JSON.stringify({ schema_version: 1, parts: parts(keys), unmapped: [{ row: 'gstack as a whole', why: 'not a single text part' }] }, null, 2));
|
|
return dir;
|
|
}
|
|
|
|
const grandfathered = (keys: string[]) => Object.fromEntries(keys.map(k => [k, k === 'beta' ? { status: 'safety', grandfathered: true, reason: 'a guard' } : { status: 'untested', grandfathered: true, plan: 'none' }]));
|
|
|
|
describe('bin/gstack-inventory', () => {
|
|
test('determinism: two runs on a fixed tree write the same docs/inventory.md and the same --json', () => {
|
|
const dir = fixtureTree(path.join(tmp, 'fixed'), grandfathered);
|
|
const first = run(['--root', dir]);
|
|
expect(first.status, first.stderr).toBe(0);
|
|
const a = fs.readFileSync(path.join(dir, 'docs/inventory.md'), 'utf8');
|
|
const second = run(['--root', dir]);
|
|
expect(second.status, second.stderr).toBe(0);
|
|
expect(fs.readFileSync(path.join(dir, 'docs/inventory.md'), 'utf8')).toBe(a);
|
|
expect(run(['--root', dir, '--json']).stdout).toBe(run(['--root', dir, '--json']).stdout);
|
|
expect(a).toContain('Measured at commit none');
|
|
expect(a.indexOf('| alpha |')).toBeLessThan(a.indexOf('| beta |'));
|
|
const start = a.indexOf('## Measure next');
|
|
const next = a.slice(start, a.indexOf('\n## ', start + 1));
|
|
expect(next).toContain('alpha');
|
|
expect(next).not.toContain('| beta |');
|
|
expect(a).toContain('gstack as a whole');
|
|
expect(a).toContain('gstack-<name>');
|
|
for (const d of DISPATCHES) expect(a).toContain(`${d.file}:`);
|
|
});
|
|
|
|
test('--check: ok on a complete ledger; a missing key exits 1 with its code', () => {
|
|
const dir = fixtureTree(path.join(tmp, 'check'), grandfathered);
|
|
const ok = run(['--root', dir, '--check']);
|
|
expect(ok.status, ok.stderr).toBe(0);
|
|
expect(ok.stdout).toMatch(/^INVENTORY: ok keys=\d+/m);
|
|
const bad = fixtureTree(path.join(tmp, 'check-bad'), keys => { const p = grandfathered(keys); delete p.alpha; return p; });
|
|
const r = run(['--root', bad, '--check']);
|
|
expect(r.status).toBe(1);
|
|
expect(r.stderr).toContain('alpha: no row in docs/inventory/evidence.json (LEDGER_KEY_MISSING)');
|
|
expect(run(['--root', bad]).status).toBe(1);
|
|
});
|
|
|
|
test('--check: a cited dispatch source that lost its quote is named', () => {
|
|
const dir = fixtureTree(path.join(tmp, 'drift'), grandfathered);
|
|
fs.writeFileSync(path.join(dir, DISPATCHES[0]!.file), 'rewritten\n');
|
|
const r = run(['--root', dir, '--check']);
|
|
expect(r.status).toBe(1);
|
|
expect(r.stderr).toContain('(INVENTORY_SOURCE_MISSING)');
|
|
});
|
|
|
|
test('the real ledger passes --check', () => {
|
|
const r = run(['--check']);
|
|
expect(r.status, r.stderr).toBe(0);
|
|
});
|
|
});
|