Files
gstack/lib/eval-plan-report.ts
T
Garry Tanandcapy-ai[bot] 4eff47c5cf v1.92.2.0 evals: no-gstack arms for the model panel, arm-benchmark, safety and routing evals (#3708)
* feat(eval-plan): results --check refuses a results table without its no-gstack rows

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

* test(model-panel): a <model>#no-gstack arm per model, identity-keyed cells, one campaign, results.md (GST-468)

Each panel model also runs as <model>#no-gstack: the same fixture, ticket,
tools, limits, route and reservation, with no skill folder and no routing
line. The arm identity keys the ledger attempt, test name, row and summary;
candidate folders are named after the fixture project and the seed commit
has a neutral author. summarize reads one campaign (run id and PREREG.md
sha256) and refuses a duplicate cell; suggestionLine reads the skill arms
only, so the preregistered particle-1 line is unchanged. renderResults writes
results.md, which passes gstack-eval-plan results --check against the new
test/fixtures/model-panel/PREREG.md (supersedes GSTA-173 for later runs).

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

* feat(arm-benchmark): no-gstack arm on a pinned model, 3 paired trials, both-order paired judge, spend ledger (GST-469)

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

* test(evals): no-gstack arm for the safety-rule and router evals (GST-470)

GSTACK_SAFETY_ARM and GSTACK_ROUTER_ARM take a third value, none: the same
fixture, tools and user request with no skill excerpt, rule text, step file
or installed skill. Under none every behavioral check is recorded as an
observation (none-arm-observations.jsonl) and never fails the case or writes
contract-violations.jsonl; harness preconditions still assert.

The four safety prompt builders (ship-evidence, codex-boundary,
codex-consult, design-risk) and the router fixtures move into helpers; the
rule, removed, current and before prompts stay byte-identical, proven
against reference copies of main's code. pair-agent and ios-demo refuse
none with the reason (NO_NONE_ARM). GSTACK_ROUTER_ARM now throws on an
unknown value instead of silently running current.

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

* chore: changelog fragment for the no-gstack arms in existing evals

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

* test(arm-benchmark): arm-pair paired-judge calibration run (bar met 6/6, $7.22)

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

* test(paid-shards): keep loop-named panel and arm-benchmark cases selectable by --case

The lanes moved each cell's testName into helpers, so the static detector no
longer saw a computed registration and B4 failed for all eight cases. Same
runtime test names; the paid runs are unaffected.

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

* fix(tests): keep FREE_FIXTURES and the placeholder allowlist current for the no-gstack arms

scripts/judge-calibration.ts is now an arm-benchmark paid touchfile, so it can no
longer be a free consumer of test/fixtures/judge-calibration/** (CI free-suite 8).
The eval-plan Step 5 fence names <prereg-path> and <results-path>; allowlist both
as PATH placeholders (CI free-suite 19).

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

* test(eval-plan): cover a complete table after a partial one, --json output and the results usage error

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

* test(eval-plan): keep the Step 7 coverage test as reviewed

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

* chore: name the changelog fragment after the branch slug

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

* docs: arm-benchmark project folder names and the arm-pair calibration corpus

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

* docs: keep the judge-calibration fixture README as reviewed

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

* docs(skills): describe eval-plan results --check

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-10-31 22:41:36 +00:00

47 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* eval-plan-report: the results check behind `gstack-eval-plan results --check`.
* A preregistration that says `stack: loaded` names its no-gstack arms; its
* results file must hold one markdown table with a filled row for every Arms
* identity, so each no-gstack row sits beside its variants
* (docs/TESTING_INTERNALS.md#no-gstack-arm). Reads text only; executes nothing.
*/
import { armIdentities, checkPrereg } from './eval-plan';
export interface ResultsCheck { ok: boolean; required: string[]; no_gstack: string[]; found: string[]; missing: string[]; problems: string[] }
const PLACEHOLDER = /^(?:|-+|—|–|n\/a|tbd|todo|\?)$/i;
const cellsOf = (line: string) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
const separator = (line: string | undefined) => !!line && /^\s*\|[\s:|-]+\|?\s*$/.test(line) && line.includes('-');
/** The filled-row identities of each markdown table: first cell, backticks stripped, header and separator rows skipped. */
export function resultTables(text: string): Array<Set<string>> {
const lines = text.split('\n');
const tables: Array<Set<string>> = [];
let current: Set<string> | null = null;
lines.forEach((line, i) => {
if (!/^\s*\|/.test(line)) { current = null; return; }
if (!current) { current = new Set(); tables.push(current); }
if (separator(line) || separator(lines[i + 1])) return;
const [first, ...rest] = cellsOf(line);
const id = (first ?? '').replace(/`/g, '').trim();
if (id && rest.some(c => !PLACEHOLDER.test(c.replace(/`/g, '').trim()))) current.add(id);
});
return tables;
}
export function checkResults(preregText: string, resultsText: string): ResultsCheck {
const prereg = checkPrereg(preregText);
if (prereg.comparison.stack === 'none') return { ok: true, required: [], no_gstack: [], found: [], missing: [], problems: [] };
const required = [...new Set(armIdentities(preregText))];
const noGstack = prereg.no_gstack.arms;
const problems: string[] = [];
if (!prereg.ok) problems.push('the preregistration does not pass prereg --check');
const tables = resultTables(resultsText);
const best = tables.reduce<Set<string>>((b, t) => (required.filter(id => t.has(id)).length > required.filter(id => b.has(id)).length ? t : b), new Set());
const found = required.filter(id => best.has(id));
const missing = required.filter(id => !best.has(id));
if (!tables.length) problems.push('no markdown table in the results file');
else for (const id of missing) problems.push(`no filled row for ${id} in the table that holds the other arms`);
return { ok: problems.length === 0, required, no_gstack: noGstack, found, missing, problems };
}