Files
gstack/test/eval-plan-results.test.ts
T
Garry Tanandcapy-ai[bot] ade35bc813 v1.94.29.0 R56: everything in gstack earns its place (principle, evidence gates, inventory) (#3837)
* eval-plan: a no-gstack arm needs masking, install and audit lines

prereg --check now refuses a loaded comparison whose ## No-gstack arm
section lacks masking:, install: or audit:, or says none for one of them
(PREREG_NO_GSTACK_ARM). On Capy, run agents inherit the instruction to load
the user-drive gstack skill, so the arm hides both Capy drives, checks that
no gstack is installed, and audits drive reads; an arm that read a gstack
skill file is contaminated and gets zero credit (TESTING_INTERNALS.md
#no-gstack-arm). The prereg template, /eval-plan, troubleshooting and the
two passing prereg fixtures carry the new lines. eval-plan's paragraph was
dieted to hold its context-budget ceiling.

Evidence: safety: keeps the no-gstack control uncontaminated (Captain order, FOUN-32 via GGST-6)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: the R56 evidence ledger, gstack-inventory and skill-bill EVIDENCE

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>

* ETHOS 9, Earn Its Place: factory, /write-skill and /autoplan carry it

Garry, 2026-11-02: "Also work this principle of only include it if it is
beneficial and good into gstack". ETHOS.md principle 9 states the rule, the
fair test (claude_code preset, frontier models per R54, masked no-gstack
arm), the R48 Why-null cycle, the safety exception, and measure biggest
first, linking docs/inventory.md. factory.md item 8 cites it, trimmed
elsewhere to 3,298 of 3,300 bytes with no rule dropped. /write-skill's edit
ladder step 2 is the evidence rule and section 4 adds the Evidence: commit
line. /autoplan's CEO phase lists each part a plan adds to gstack with its
evidence or owned, dated plan; two redundant lines went to keep autoplan
under its size pins (pins lowered by 17). CONTRIBUTING.md "Adding a new
skill" and docs/skills.md point at the ledger.

Skill-Size: 3273→3666 bytes (write-skill/SKILL.md.tmpl)
Skill-Size: 12738→12721 bytes (autoplan/sections/ceo-phase.md)
Evidence: plan none (owner ruling R56, GST-914); write-skill and autoplan rows stay grandfathered untested
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fixup! inventory: the R56 evidence ledger, gstack-inventory and skill-bill EVIDENCE

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: list the evidence table's other names and its unmapped rows

Ledger rows carry aliases (the name GEVA-2's table uses where it differs from the key, such as upgrade for gstack-upgrade), and docs/inventory.md lists them beside the installed-name rule. Unmapped rows are labeled as evidence-table rows, and the ranking notes that a skill's on-demand files can also be a shared row's.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the first inventory snapshot and the R56 changelog fragment

docs/inventory.md is generated by bin/gstack-inventory at the previous commit: 71 parts ranked by load per run, 0 helps, 0 no-effect, 0 hurts, 16 untested with a plan (4 fair tests under way, 12 proposed), 50 untested with plan none, 5 safety.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: investigate's step procedure is a fair-test CUT-CANDIDATE (GST-814)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the inventory snapshot after investigate's no-effect row

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: only the seeded backlog may be grandfathered, so a new part's row is checked (review fix)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: devex-review and retro name their GEVA-2 registry rows (R56-11, R56-12)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the inventory snapshot after the review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: a concrete plan is a path, gstack-inventory never writes into a gstack install, and the ledger checks aliases, r48 and prior.why (review fixes)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* inventory: investigate's no-effect row notes that only Opus 5.5 was measured

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the inventory snapshot after the review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: R56 counts after investigate's row; grandfathering and the install guard

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: register inventory-grandfathered.json as a free fixture

free-suite (1) at e56d3312c failed FREE_FIXTURES inventory: the pinned grandfather fixture from d0b3028b5 had no paid touchfile and no FREE_FIXTURES entry. Its only reader is test/inventory.test.ts.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-03 17:08:30 +00:00

131 lines
7.8 KiB
TypeScript

/**
* GST-471: `gstack-eval-plan results --check`. A loaded preregistration's
* results file must hold one markdown table with a filled row for every Arms
* identity, so each no-gstack row sits beside its variants.
*/
import { afterEach, 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 { checkResults } from '../lib/eval-plan-report';
const BIN = path.resolve(import.meta.dir, '..', 'bin', 'gstack-eval-plan');
const dirs: string[] = [];
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); });
const BASE = ['Bars', 'Decoys', 'Stop rules', 'Held-out exposure'].map(s => `## ${s}\n- filled in\n`).join('\n');
const PREREG = BASE + `
## Arms
- claude-opus-5-5#review-g default settings
- claude-opus-5-5#no-gstack default settings
## Comparison
stack: loaded
## No-gstack arm
arms: claude-opus-5-5#no-gstack
inputs: the same rows
prompt: the task only
panel: rule, 3 trials, same judge
report: results.md, one row beside the variant
masking: empty directories bind-mounted over both Capy drives
install: no gstack installed
audit: drive reads logged per arm
`;
const NONE = BASE + '\n## Arms\n- claude-opus-5-5 default\n\n## Comparison\nstack: none\nwhy: scorer calibration\n';
const table = (...rows: string[]) => ['| arm | passed | cost |', '|---|---|---|', ...rows].join('\n') + '\n';
describe('checkResults', () => {
test('one table with a filled row per arm passes; backticks and the header row are handled', () => {
const r = checkResults(PREREG, '# Results\n\n' + table('| `claude-opus-5-5#review-g` | 7/9 | $1.20 |', '| claude-opus-5-5#no-gstack | 5/9 | $0.80 |'));
expect(r).toMatchObject({ ok: true, no_gstack: ['claude-opus-5-5#no-gstack'], missing: [] });
});
test('a missing no-gstack row fails naming it', () => {
const r = checkResults(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |'));
expect(r.ok).toBe(false);
expect(r.problems).toEqual(['no filled row for claude-opus-5-5#no-gstack in the table that holds the other arms']);
});
test('placeholder cells do not count as a filled row', () => {
for (const cell of ['', '-', '—', 'n/a', 'TBD']) {
const r = checkResults(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |', `| claude-opus-5-5#no-gstack | ${cell} | ${cell} |`));
expect(r.missing, JSON.stringify(cell)).toEqual(['claude-opus-5-5#no-gstack']);
}
});
test('the no-gstack row must sit in the same table as its variants; prose mentions never count', () => {
const split = table('| claude-opus-5-5#review-g | 7/9 | $1.20 |') + '\nThe claude-opus-5-5#no-gstack arm passed 5/9.\n\n' + table('| claude-opus-5-5#no-gstack | 5/9 | $0.80 |');
expect(checkResults(PREREG, split).ok).toBe(false);
expect(checkResults(PREREG, 'claude-opus-5-5#review-g 7/9, claude-opus-5-5#no-gstack 5/9\n').problems).toEqual(['no markdown table in the results file']);
});
test('identities match whole: a prefix model does not stand in for the arm', () => {
const r = checkResults(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |', '| claude-opus-5#no-gstack | 5/9 | $0.80 |'));
expect(r.missing).toEqual(['claude-opus-5-5#no-gstack']);
});
// Generated by /ship Step 7 coverage audit (PR #3708).
// Value: protects=checkResults judges the table holding the most arms, so a complete table after a partial one passes and an aligned separator is no row; fails_when=the best-table reduce keeps the first table or alignment colons stop separator detection; why_new=existing cases use one table or two partial tables and plain --- separators; seam=none; no_claim=none beyond protects
test('a complete table after a partial one passes; an aligned separator row is never an arm row', () => {
const aligned = (...rows: string[]) => ['| arm | passed | cost |', '|:---|:---:|---:|', ...rows].join('\n') + '\n';
const r = checkResults(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |') + '\nNotes.\n\n' + aligned('| claude-opus-5-5#review-g | 7/9 | $1.20 |', '| claude-opus-5-5#no-gstack | 5/9 | $0.80 |'));
expect(r).toMatchObject({ ok: true, found: ['claude-opus-5-5#review-g', 'claude-opus-5-5#no-gstack'], missing: [] });
});
test('stack: none needs no row; a preregistration that fails prereg --check fails here too', () => {
expect(checkResults(NONE, 'no table at all\n').ok).toBe(true);
const broken = PREREG.replace(/## No-gstack arm[\s\S]*$/, '');
const r = checkResults(broken, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |', '| claude-opus-5-5#no-gstack | 5/9 | $0.80 |'));
expect(r.problems).toEqual(['the preregistration does not pass prereg --check']);
});
});
describe('gstack-eval-plan results --check', () => {
const run = (prereg: string, results: string, extra: string[] = []) => {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-results-check-'));
dirs.push(d);
fs.writeFileSync(path.join(d, 'prereg.md'), prereg);
fs.writeFileSync(path.join(d, 'results.md'), results);
const r = spawnSync(BIN, ['results', '--check', 'prereg.md', 'results.md', ...extra], { cwd: d, encoding: 'utf8', timeout: 60_000 });
return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '' };
};
test('missing row: RESULTS: incomplete, EVAL_REPORT_NO_GSTACK_ROW, exit 1', () => {
const r = run(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |'));
expect(r.code).toBe(1);
expect(r.out).toBe('RESULTS: incomplete no_gstack_rows=0/1 rows=1/2 file=results.md\n');
expect(r.err).toContain('(EVAL_REPORT_NO_GSTACK_ROW)');
});
test('complete: RESULTS: ok, exit 0', () => {
const r = run(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |', '| claude-opus-5-5#no-gstack | 5/9 | $0.80 |'));
expect(r.code, r.err).toBe(0);
expect(r.out).toBe('RESULTS: ok no_gstack_rows=1/1 rows=2/2 file=results.md\n');
});
// Generated by /ship Step 7 coverage audit (PR #3708).
// Value: protects=--json prints one parseable object with the paths and the check fields, and keeps exit 1 plus the result code when a row is missing; fails_when=--json drops a field, prints the text line instead, or exits 0 on an incomplete file; why_new=the CLI tests cover only the text output; seam=none; no_claim=none beyond protects
test('--json: one object with the paths and the check, exit 1 and the result code when a row is missing', () => {
const r = run(PREREG, table('| claude-opus-5-5#review-g | 7/9 | $1.20 |'), ['--json']);
expect(r.code).toBe(1);
expect(JSON.parse(r.out)).toMatchObject({ prereg: 'prereg.md', results: 'results.md', ok: false, no_gstack: ['claude-opus-5-5#no-gstack'], found: ['claude-opus-5-5#review-g'], missing: ['claude-opus-5-5#no-gstack'] });
expect(r.err).toContain('(EVAL_REPORT_NO_GSTACK_ROW)');
});
// Generated by /ship Step 7 coverage audit (PR #3708).
// Value: protects=results with a missing results file or no --check is a usage error (exit 2) that checks nothing; fails_when=the existence guard is removed so a missing file throws or reads as an empty table; why_new=no test reaches the usage path of results; seam=none; no_claim=none beyond protects
test('a missing results file or a missing --check is a usage error, exit 2, nothing on stdout', () => {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-results-check-'));
dirs.push(d);
fs.writeFileSync(path.join(d, 'prereg.md'), PREREG);
for (const args of [['results', '--check', 'prereg.md', 'nope.md'], ['results', 'prereg.md']]) {
const r = spawnSync(BIN, args, { cwd: d, encoding: 'utf8', timeout: 60_000 });
expect(r.status, args.join(' ')).toBe(2);
expect(r.stdout).toBe('');
expect(r.stderr).toContain('results needs --check <existing prereg> <existing results file>');
}
});
});