@skill-harness/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/adapters/types.d.ts +29 -0
- package/dist/adapters/types.js +18 -0
- package/dist/discover.d.ts +14 -0
- package/dist/discover.js +35 -0
- package/dist/grade.d.ts +44 -0
- package/dist/grade.js +89 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/journal.d.ts +76 -0
- package/dist/journal.js +32 -0
- package/dist/lint.d.ts +14 -0
- package/dist/lint.js +137 -0
- package/dist/regrade.d.ts +54 -0
- package/dist/regrade.js +107 -0
- package/dist/report.d.ts +80 -0
- package/dist/report.js +106 -0
- package/dist/reps.d.ts +34 -0
- package/dist/reps.js +45 -0
- package/dist/results.d.ts +108 -0
- package/dist/results.js +241 -0
- package/dist/run.d.ts +28 -0
- package/dist/run.js +148 -0
- package/dist/scheduler.d.ts +11 -0
- package/dist/scheduler.js +27 -0
- package/dist/score.d.ts +33 -0
- package/dist/score.js +62 -0
- package/dist/seeded.d.ts +21 -0
- package/dist/seeded.js +45 -0
- package/dist/spec.d.ts +39 -0
- package/dist/spec.js +175 -0
- package/dist/trends.d.ts +54 -0
- package/dist/trends.js +102 -0
- package/dist/util/exec.d.ts +14 -0
- package/dist/util/exec.js +43 -0
- package/dist/workspace.d.ts +17 -0
- package/dist/workspace.js +42 -0
- package/package.json +17 -0
package/dist/regrade.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { buildJudgePrompt, judgeInWorkspace } from "./grade.js";
|
|
4
|
+
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, } from "./results.js";
|
|
5
|
+
import { outcomesToResult } from "./reps.js";
|
|
6
|
+
import { appendJournal } from "./journal.js";
|
|
7
|
+
/** Judge one saved transcript: writes the judge-raw artifact, emits a `judge-verdict` journal event (plus `misfire-flag` when the verdict is suspect), and returns the outcome. */
|
|
8
|
+
export async function judgeOneRep(opts) {
|
|
9
|
+
const { runDir, spec, scenario, transcript, adapter, judge, specDir, mode, rep, now } = opts;
|
|
10
|
+
const prompt = buildJudgePrompt({ skill: spec.skill, persona: spec.judge_persona, scenario, transcript });
|
|
11
|
+
const g = await judgeInWorkspace(adapter, judge, prompt, specDir);
|
|
12
|
+
writeFileSync(judgeRawPath(runDir, scenario.id, mode, rep), g.raw, "utf8");
|
|
13
|
+
const repField = rep === undefined ? {} : { rep };
|
|
14
|
+
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: g.verdict, reason: g.reason, suspect: g.suspect, ...repField });
|
|
15
|
+
if (g.suspect)
|
|
16
|
+
appendJournal(runDir, { event: "misfire-flag", ts: now(), id: scenario.id, reason: g.reason, ...repField });
|
|
17
|
+
return { verdict: g.verdict, reason: g.reason, suspect: g.suspect };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Re-judge a scenario's saved GREEN transcript(s) with `judge` — no harness
|
|
21
|
+
* re-run. Rewrites the judge-raw artifact per rep, emits per-rep judge-verdict
|
|
22
|
+
* (+ misfire-flag) journal events, and returns the aggregated ScenarioResult
|
|
23
|
+
* (override/note empty; the caller merges any prior override + persists).
|
|
24
|
+
*/
|
|
25
|
+
export async function regradeScenario(opts) {
|
|
26
|
+
const now = opts.now ?? (() => new Date().toISOString());
|
|
27
|
+
const files = findTranscriptFiles(opts.runDir, opts.scenario.id, "green");
|
|
28
|
+
if (files.length === 0)
|
|
29
|
+
throw new Error(`no green transcripts for ${opts.scenario.id} in ${opts.runDir}`);
|
|
30
|
+
const repCount = files.length;
|
|
31
|
+
const outcomes = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const rep = repIndexOf(file) ?? undefined;
|
|
34
|
+
const transcript = readFileSync(join(opts.runDir, file), "utf8");
|
|
35
|
+
outcomes.push(await judgeOneRep({
|
|
36
|
+
runDir: opts.runDir, spec: opts.spec, scenario: opts.scenario, transcript,
|
|
37
|
+
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode: "green", rep, now,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return outcomesToResult(opts.scenario.id, outcomes, repCount, opts.threshold);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Re-judge every green-transcript scenario in a run dir with `judge` — no
|
|
44
|
+
* harness re-run. Targets are the run's RECORDED scenarios (falling back to
|
|
45
|
+
* the spec for a run with no prior results.yaml), so re-grading rewrites the
|
|
46
|
+
* whole results.yaml consistently with what the run actually recorded. Each
|
|
47
|
+
* target must still exist in the spec (for its checklist) AND have a green
|
|
48
|
+
* transcript on disk; anything missing fails fast before spending any judge
|
|
49
|
+
* calls. Preserves each prior scenario's override/note, rewrites
|
|
50
|
+
* results.yaml, emits the `score` journal event, and returns the new
|
|
51
|
+
* ResultsFile. Shared by `cmdGrade` and the pi-extension's `judge` command.
|
|
52
|
+
*/
|
|
53
|
+
export async function regradeRun(opts) {
|
|
54
|
+
const { runDir, spec, adapter, judge, specDir } = opts;
|
|
55
|
+
const now = opts.now ?? (() => new Date().toISOString());
|
|
56
|
+
const prev = existsSync(join(runDir, "results.yaml")) ? readResults(runDir) : null;
|
|
57
|
+
const overrides = new Map((prev?.scenarios ?? []).map((s) => [s.id, { override: s.override, note: s.note }]));
|
|
58
|
+
const mode = prev?.mode ?? "green";
|
|
59
|
+
// Re-grading rewrites the WHOLE results.yaml, so re-judge exactly the
|
|
60
|
+
// scenarios the run recorded (falling back to the spec for a run with no
|
|
61
|
+
// prior results). The guard and the loop iterate the SAME `targets` set, so
|
|
62
|
+
// they can't diverge: each target must still exist in the spec (for its
|
|
63
|
+
// checklist) AND have a transcript on disk — only overridden transcripts
|
|
64
|
+
// survive a commit (audit-trail design). Anything missing would silently drop
|
|
65
|
+
// a recorded verdict or shrink the grade denominator. Fail fast, before
|
|
66
|
+
// spending any judge calls.
|
|
67
|
+
const specById = new Map(spec.scenarios.map((s) => [s.id, s]));
|
|
68
|
+
const targets = (prev?.scenarios ?? spec.scenarios).map((s) => s.id);
|
|
69
|
+
const missing = targets.filter((id) => !specById.has(id) || findTranscriptFiles(runDir, id, "green").length === 0);
|
|
70
|
+
if (missing.length === targets.length) {
|
|
71
|
+
throw new Error(`no green transcripts in ${runDir} — nothing to re-grade`);
|
|
72
|
+
}
|
|
73
|
+
if (missing.length > 0) {
|
|
74
|
+
throw new Error(`cannot re-grade ${missing.join(", ")} in ${runDir} (transcript missing or scenario no longer in the spec) — re-run instead of grading`);
|
|
75
|
+
}
|
|
76
|
+
const scenarioResults = [];
|
|
77
|
+
for (const id of targets) {
|
|
78
|
+
const scenario = specById.get(id); // guaranteed present by the guard above
|
|
79
|
+
const prevScenario = prev?.scenarios.find((s) => s.id === id);
|
|
80
|
+
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
81
|
+
const rr = await regradeScenario({
|
|
82
|
+
runDir, spec, scenario, adapter, judge, specDir, threshold, now,
|
|
83
|
+
});
|
|
84
|
+
const carry = overrides.get(id);
|
|
85
|
+
scenarioResults.push({ ...rr, override: carry?.override ?? null, note: carry?.note ?? "" });
|
|
86
|
+
}
|
|
87
|
+
const ctx = mode === "green" ? { shipBar: spec.ship_bar, critical: spec.critical } : null;
|
|
88
|
+
const results = writeResults(runDir, {
|
|
89
|
+
skill: spec.skill,
|
|
90
|
+
harness: prev?.harness ?? "pi",
|
|
91
|
+
model: prev?.model ?? "unknown",
|
|
92
|
+
judge: { provider: judge.provider, model: judge.model },
|
|
93
|
+
timestamp: prev?.timestamp ?? now(),
|
|
94
|
+
label: prev?.label ?? null,
|
|
95
|
+
mode,
|
|
96
|
+
scenarios: scenarioResults,
|
|
97
|
+
}, ctx);
|
|
98
|
+
const g = results.effective_grade;
|
|
99
|
+
if (ctx) {
|
|
100
|
+
appendJournal(runDir, {
|
|
101
|
+
event: "score", ts: now(),
|
|
102
|
+
passed: g.passed, total: g.total, pct: g.pct, letter: g.letter, ship: g.ship, note: g.note,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=regrade.js.map
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ShipBar } from "./spec.js";
|
|
2
|
+
import { type ResultsFile } from "./results.js";
|
|
3
|
+
export interface RunColumn {
|
|
4
|
+
index: number;
|
|
5
|
+
label: string;
|
|
6
|
+
tag: string;
|
|
7
|
+
runDir: string;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
mode: string;
|
|
10
|
+
grade: ResultsFile["effective_grade"];
|
|
11
|
+
judge: ResultsFile["judge"];
|
|
12
|
+
cells: Record<string, {
|
|
13
|
+
judge_verdict: string;
|
|
14
|
+
judge_reason: string;
|
|
15
|
+
suspect: boolean;
|
|
16
|
+
reps?: number;
|
|
17
|
+
passes?: number;
|
|
18
|
+
clean?: number;
|
|
19
|
+
flakiness?: number;
|
|
20
|
+
override: string | null;
|
|
21
|
+
note: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface ReportData {
|
|
25
|
+
skill: string;
|
|
26
|
+
shipBar: ShipBar;
|
|
27
|
+
critical: string[];
|
|
28
|
+
scenarios: {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
critical: boolean;
|
|
32
|
+
}[];
|
|
33
|
+
columns: RunColumn[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Collect the latest run per model-tag under <skillDir>/tests/results/, plus the
|
|
37
|
+
* scenario list from the spec (for titles + order). One column per model.
|
|
38
|
+
*/
|
|
39
|
+
export declare function collectReport(skillDir: string): ReportData;
|
|
40
|
+
/** Client-facing view (no absolute paths leaked). */
|
|
41
|
+
export declare function publicView(data: ReportData): {
|
|
42
|
+
skill: string;
|
|
43
|
+
shipBar: ShipBar;
|
|
44
|
+
critical: string[];
|
|
45
|
+
scenarios: {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
critical: boolean;
|
|
49
|
+
}[];
|
|
50
|
+
columns: {
|
|
51
|
+
index: number;
|
|
52
|
+
label: string;
|
|
53
|
+
tag: string;
|
|
54
|
+
timestamp: string;
|
|
55
|
+
mode: string;
|
|
56
|
+
grade: import("./results.js").GradeSummary;
|
|
57
|
+
judge: {
|
|
58
|
+
provider: string;
|
|
59
|
+
model: string;
|
|
60
|
+
};
|
|
61
|
+
cells: Record<string, {
|
|
62
|
+
judge_verdict: string;
|
|
63
|
+
judge_reason: string;
|
|
64
|
+
suspect: boolean;
|
|
65
|
+
reps?: number;
|
|
66
|
+
passes?: number;
|
|
67
|
+
clean?: number;
|
|
68
|
+
flakiness?: number;
|
|
69
|
+
override: string | null;
|
|
70
|
+
note: string;
|
|
71
|
+
}>;
|
|
72
|
+
}[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Inject the run JSON and the client-scorer module into the template, at the
|
|
76
|
+
* __DATA__ and __GRADE__ placeholders respectively. `gradeScript` is the raw
|
|
77
|
+
* contents of assets/report.grade.js (sibling of the template) — the single,
|
|
78
|
+
* score.ts-parity-tested copy of the client grading logic.
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderReport(template: string, data: ReportData, gradeScript: string): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadSpec } from "./spec.js";
|
|
4
|
+
import { readResults } from "./results.js";
|
|
5
|
+
/** Most-recent run dir (by name, which is an ISO-ish slug) under a model-tag dir. */
|
|
6
|
+
function latestRunDir(tagDir) {
|
|
7
|
+
if (!statSync(tagDir).isDirectory())
|
|
8
|
+
return null;
|
|
9
|
+
const runs = readdirSync(tagDir)
|
|
10
|
+
.map((n) => join(tagDir, n))
|
|
11
|
+
.filter((p) => statSync(p).isDirectory() && existsSync(join(p, "results.yaml")))
|
|
12
|
+
.sort();
|
|
13
|
+
return runs.length ? runs[runs.length - 1] : null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Collect the latest run per model-tag under <skillDir>/tests/results/, plus the
|
|
17
|
+
* scenario list from the spec (for titles + order). One column per model.
|
|
18
|
+
*/
|
|
19
|
+
export function collectReport(skillDir) {
|
|
20
|
+
const specPath = join(skillDir, "tests", "specification.yaml");
|
|
21
|
+
const spec = loadSpec(specPath);
|
|
22
|
+
const scenarios = spec.scenarios.map((s) => ({ id: s.id, title: s.title, critical: s.critical }));
|
|
23
|
+
const resultsRoot = join(skillDir, "tests", "results");
|
|
24
|
+
const columns = [];
|
|
25
|
+
if (existsSync(resultsRoot)) {
|
|
26
|
+
const tags = readdirSync(resultsRoot)
|
|
27
|
+
.map((n) => join(resultsRoot, n))
|
|
28
|
+
.filter((p) => statSync(p).isDirectory())
|
|
29
|
+
.sort();
|
|
30
|
+
for (const tagDir of tags) {
|
|
31
|
+
const runDir = latestRunDir(tagDir);
|
|
32
|
+
if (!runDir)
|
|
33
|
+
continue;
|
|
34
|
+
const r = readResults(runDir);
|
|
35
|
+
const cells = {};
|
|
36
|
+
for (const s of r.scenarios) {
|
|
37
|
+
cells[s.id] = {
|
|
38
|
+
judge_verdict: s.judge_verdict,
|
|
39
|
+
judge_reason: s.judge_reason,
|
|
40
|
+
suspect: s.suspect ?? false, // suspect defaults false for older results that predate the field
|
|
41
|
+
reps: s.reps,
|
|
42
|
+
passes: s.passes,
|
|
43
|
+
clean: s.clean,
|
|
44
|
+
flakiness: s.flakiness,
|
|
45
|
+
override: s.override,
|
|
46
|
+
note: s.note,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
columns.push({
|
|
50
|
+
index: columns.length,
|
|
51
|
+
label: r.model,
|
|
52
|
+
tag: tagDir.split("/").pop(),
|
|
53
|
+
runDir,
|
|
54
|
+
timestamp: r.timestamp,
|
|
55
|
+
mode: r.mode,
|
|
56
|
+
grade: r.effective_grade,
|
|
57
|
+
judge: r.judge,
|
|
58
|
+
cells,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { skill: spec.skill, shipBar: spec.ship_bar, critical: spec.critical, scenarios, columns };
|
|
63
|
+
}
|
|
64
|
+
/** Client-facing view (no absolute paths leaked). */
|
|
65
|
+
export function publicView(data) {
|
|
66
|
+
return {
|
|
67
|
+
skill: data.skill,
|
|
68
|
+
shipBar: data.shipBar,
|
|
69
|
+
critical: data.critical,
|
|
70
|
+
scenarios: data.scenarios,
|
|
71
|
+
columns: data.columns.map((c) => ({
|
|
72
|
+
index: c.index,
|
|
73
|
+
label: c.label,
|
|
74
|
+
tag: c.tag,
|
|
75
|
+
timestamp: c.timestamp,
|
|
76
|
+
mode: c.mode,
|
|
77
|
+
grade: c.grade,
|
|
78
|
+
judge: c.judge,
|
|
79
|
+
cells: c.cells,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A bare inline <script> (no type="module") can't contain an `export`
|
|
85
|
+
* statement, but assets/report.grade.js is written as real ESM so it can also
|
|
86
|
+
* be imported directly (by Node, in the parity test). Strip the `export `
|
|
87
|
+
* keyword off each exported declaration so the leftover plain function
|
|
88
|
+
* declarations splice cleanly into the template's script scope.
|
|
89
|
+
*/
|
|
90
|
+
function stripExports(js) {
|
|
91
|
+
return js.replace(/^export\s+/gm, "");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Inject the run JSON and the client-scorer module into the template, at the
|
|
95
|
+
* __DATA__ and __GRADE__ placeholders respectively. `gradeScript` is the raw
|
|
96
|
+
* contents of assets/report.grade.js (sibling of the template) — the single,
|
|
97
|
+
* score.ts-parity-tested copy of the client grading logic.
|
|
98
|
+
*/
|
|
99
|
+
export function renderReport(template, data, gradeScript) {
|
|
100
|
+
const json = JSON.stringify(publicView(data));
|
|
101
|
+
return template
|
|
102
|
+
.replace("/*__DATA__*/null", json)
|
|
103
|
+
.replace("/*__GRADE__*/", stripExports(gradeScript))
|
|
104
|
+
.replace("__SKILL__", data.skill);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=report.js.map
|
package/dist/reps.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Verdict } from "./score.js";
|
|
2
|
+
import type { ScenarioResult } from "./results.js";
|
|
3
|
+
/** One rep's outcome (subject run + judge). */
|
|
4
|
+
export interface RepOutcome {
|
|
5
|
+
verdict: Verdict;
|
|
6
|
+
reason: string;
|
|
7
|
+
suspect: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A scenario's aggregated result over N reps. */
|
|
10
|
+
export interface RepAggregate {
|
|
11
|
+
verdict: Verdict;
|
|
12
|
+
reason: string;
|
|
13
|
+
passes: number;
|
|
14
|
+
reps: number;
|
|
15
|
+
clean: number;
|
|
16
|
+
flakiness: number;
|
|
17
|
+
suspect: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collapse N rep outcomes into one scenario verdict. A rep is "clean" when its
|
|
21
|
+
* judge did not misfire. If fewer than half the reps are clean the scenario is
|
|
22
|
+
* `suspect` (its verdict is untrustworthy). Otherwise the pass-rate is computed
|
|
23
|
+
* over the clean reps and the scenario PASSes at `pass_rate >= threshold`
|
|
24
|
+
* (default caller threshold 0.5, ties pass). Flakiness = 1 - |2·pass_rate - 1|.
|
|
25
|
+
*/
|
|
26
|
+
export declare function aggregateReps(outcomes: RepOutcome[], threshold: number): RepAggregate;
|
|
27
|
+
/**
|
|
28
|
+
* Collapse a scenario's rep outcomes into a ScenarioResult. N=1 preserves the
|
|
29
|
+
* single judge's verdict/reason with no reps fields (byte-identical to a plain
|
|
30
|
+
* run); N>1 aggregates and persists the effective threshold (so a later
|
|
31
|
+
* re-judge reproduces the same pass-rate). override/note are left empty for the
|
|
32
|
+
* caller to merge.
|
|
33
|
+
*/
|
|
34
|
+
export declare function outcomesToResult(id: string, outcomes: RepOutcome[], repCount: number, threshold: number): ScenarioResult;
|
package/dist/reps.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse N rep outcomes into one scenario verdict. A rep is "clean" when its
|
|
3
|
+
* judge did not misfire. If fewer than half the reps are clean the scenario is
|
|
4
|
+
* `suspect` (its verdict is untrustworthy). Otherwise the pass-rate is computed
|
|
5
|
+
* over the clean reps and the scenario PASSes at `pass_rate >= threshold`
|
|
6
|
+
* (default caller threshold 0.5, ties pass). Flakiness = 1 - |2·pass_rate - 1|.
|
|
7
|
+
*/
|
|
8
|
+
export function aggregateReps(outcomes, threshold) {
|
|
9
|
+
const reps = outcomes.length;
|
|
10
|
+
const clean = outcomes.filter((o) => !o.suspect);
|
|
11
|
+
const passes = clean.filter((o) => o.verdict === "PASS").length;
|
|
12
|
+
if (clean.length * 2 < reps) {
|
|
13
|
+
// majority of reps misfired → untrustworthy
|
|
14
|
+
return { verdict: "FAIL", reason: `${reps - clean.length}/${reps} reps misfired — re-judge`, passes, reps, clean: clean.length, flakiness: 0, suspect: true };
|
|
15
|
+
}
|
|
16
|
+
const errored = clean.filter((o) => o.verdict === "ERROR").length;
|
|
17
|
+
if (clean.length > 0 && errored === clean.length) {
|
|
18
|
+
return { verdict: "ERROR", reason: `${errored}/${reps} reps errored`, passes: 0, reps, clean: clean.length, flakiness: 0, suspect: false };
|
|
19
|
+
}
|
|
20
|
+
const passRate = passes / clean.length;
|
|
21
|
+
const verdict = passRate >= threshold ? "PASS" : "FAIL";
|
|
22
|
+
const flakiness = 1 - Math.abs(2 * passRate - 1);
|
|
23
|
+
const reason = reps === 1 ? outcomes[0].reason : `${passes}/${clean.length} reps passed (flaky ${flakiness.toFixed(2)})`;
|
|
24
|
+
return { verdict, reason, passes, reps, clean: clean.length, flakiness, suspect: false };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Collapse a scenario's rep outcomes into a ScenarioResult. N=1 preserves the
|
|
28
|
+
* single judge's verdict/reason with no reps fields (byte-identical to a plain
|
|
29
|
+
* run); N>1 aggregates and persists the effective threshold (so a later
|
|
30
|
+
* re-judge reproduces the same pass-rate). override/note are left empty for the
|
|
31
|
+
* caller to merge.
|
|
32
|
+
*/
|
|
33
|
+
export function outcomesToResult(id, outcomes, repCount, threshold) {
|
|
34
|
+
if (repCount === 1) {
|
|
35
|
+
const o = outcomes[0];
|
|
36
|
+
return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, override: null, note: "" };
|
|
37
|
+
}
|
|
38
|
+
const agg = aggregateReps(outcomes, threshold);
|
|
39
|
+
return {
|
|
40
|
+
id, judge_verdict: agg.verdict, judge_reason: agg.reason, suspect: agg.suspect,
|
|
41
|
+
reps: agg.reps, passes: agg.passes, clean: agg.clean, flakiness: agg.flakiness,
|
|
42
|
+
pass_threshold: threshold, override: null, note: "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=reps.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type ModelRef } from "./adapters/types.js";
|
|
2
|
+
import { type ScenarioVerdict } from "./score.js";
|
|
3
|
+
import type { Verdict } from "./score.js";
|
|
4
|
+
import type { ShipBar, Scenario } from "./spec.js";
|
|
5
|
+
export interface ScenarioResult {
|
|
6
|
+
id: string;
|
|
7
|
+
judge_verdict: Verdict;
|
|
8
|
+
judge_reason: string;
|
|
9
|
+
suspect: boolean;
|
|
10
|
+
override: Verdict | null;
|
|
11
|
+
note: string;
|
|
12
|
+
reps?: number;
|
|
13
|
+
passes?: number;
|
|
14
|
+
clean?: number;
|
|
15
|
+
flakiness?: number;
|
|
16
|
+
pass_threshold?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GradeSummary {
|
|
19
|
+
passed: number;
|
|
20
|
+
total: number;
|
|
21
|
+
pct: number;
|
|
22
|
+
letter: string;
|
|
23
|
+
ship: boolean;
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ResultsFile {
|
|
27
|
+
schema: 2;
|
|
28
|
+
skill: string;
|
|
29
|
+
harness: string;
|
|
30
|
+
model: string;
|
|
31
|
+
judge: {
|
|
32
|
+
provider: string;
|
|
33
|
+
model: string;
|
|
34
|
+
};
|
|
35
|
+
timestamp: string;
|
|
36
|
+
label: string | null;
|
|
37
|
+
mode: string;
|
|
38
|
+
effective_grade: GradeSummary;
|
|
39
|
+
scenarios: ScenarioResult[];
|
|
40
|
+
}
|
|
41
|
+
/** The pass-threshold a re-grade uses: the run's persisted value, else the spec's per-scenario value, else 0.5. */
|
|
42
|
+
export declare function effectiveThreshold(prevScenario: ScenarioResult | undefined, scenario: Scenario): number;
|
|
43
|
+
/** Everything a caller may set. The grade is computed, never supplied. */
|
|
44
|
+
export type ResultsDraft = Omit<ResultsFile, "schema" | "effective_grade">;
|
|
45
|
+
export interface ScoreContext {
|
|
46
|
+
shipBar: ShipBar;
|
|
47
|
+
critical: string[];
|
|
48
|
+
}
|
|
49
|
+
/** <skillDir>/tests/results/<harness>-<model-slug>/<timestamp-slug>/ */
|
|
50
|
+
export declare function runDirFor(skillDir: string, harness: string, model: ModelRef, timestamp: string): string;
|
|
51
|
+
/** Path of a transcript file within a run dir. A rep index (for --reps N>1) is suffixed. */
|
|
52
|
+
export declare function transcriptPath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
|
|
53
|
+
export declare function reportPath(runDir: string): string;
|
|
54
|
+
export declare function resultsPath(runDir: string): string;
|
|
55
|
+
/** The verdict that counts: author override when present, else the judge's. */
|
|
56
|
+
export declare function effectiveVerdicts(scenarios: ScenarioResult[]): ScenarioVerdict[];
|
|
57
|
+
/**
|
|
58
|
+
* The ONLY place effective_grade is computed. Every writer goes through here,
|
|
59
|
+
* so a persisted grade can never disagree with verdicts + overrides.
|
|
60
|
+
* ctx is null for unscored (red/force) runs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function finalizeResults(draft: ResultsDraft, ctx: ScoreContext | null): ResultsFile;
|
|
63
|
+
/** Finalize + persist results.yaml (creating the run dir). Returns what was written. */
|
|
64
|
+
export declare function writeResults(runDir: string, draft: ResultsDraft, ctx: ScoreContext | null): ResultsFile;
|
|
65
|
+
/** Read-only schema-1 → schema-2 migration. Never rewrites the file on disk. */
|
|
66
|
+
export declare function migrateResults(raw: unknown): ResultsFile;
|
|
67
|
+
/** Read results.yaml from a run dir, migrating schema-1 files in memory. */
|
|
68
|
+
export declare function readResults(runDir: string): ResultsFile;
|
|
69
|
+
/** Pure: return a copy with override + note applied to one scenario. */
|
|
70
|
+
export declare function applyOverride(results: ResultsFile, scenarioId: string, override: Verdict | null, note: string): ResultsFile;
|
|
71
|
+
/**
|
|
72
|
+
* Manage results/.gitignore: transcripts + reports ignored, results.yaml tracked.
|
|
73
|
+
* Rewrites a stale managed body (so new ignore rules roll out) while keeping any
|
|
74
|
+
* `!…` preservation lines added by preserveTranscript.
|
|
75
|
+
*/
|
|
76
|
+
export declare function ensureResultsGitignore(resultsRoot: string): void;
|
|
77
|
+
/** The rep index embedded in a transcript/judge-raw filename (`.rep<k>.`), or null for a plain (non-rep) file. */
|
|
78
|
+
export declare function repIndexOf(filename: string): number | null;
|
|
79
|
+
/**
|
|
80
|
+
* ALL transcript files for a scenario in a run dir, sorted deterministically:
|
|
81
|
+
* a plain `<id>.<mode>.txt` first (if present), then rep-suffixed files
|
|
82
|
+
* (`<id>.<mode>.rep<k>.txt`) in numeric rep order. Empty if the run dir or
|
|
83
|
+
* scenario has no transcripts.
|
|
84
|
+
*
|
|
85
|
+
* With `mode` given, only that mode's transcripts match (`<id>.<mode>.txt` /
|
|
86
|
+
* `<id>.<mode>.rep<k>.txt`) — e.g. to detect a green-only condition without
|
|
87
|
+
* false positives from a red/force transcript of the same scenario. Omitted,
|
|
88
|
+
* behavior is unchanged: any `<id>.*.txt` regardless of mode, excluding this
|
|
89
|
+
* scenario's judge-raw artifacts (`<id>.*.judge.txt` — see judgeRawPath).
|
|
90
|
+
*/
|
|
91
|
+
export declare function findTranscriptFiles(runDir: string, scenarioId: string, mode?: string): string[];
|
|
92
|
+
/** Path of a scenario's raw judge-output artifact within a run dir (rep-suffixed for reps). */
|
|
93
|
+
export declare function judgeRawPath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
|
|
94
|
+
/** A scenario's raw judge-output files, sorted (plain first, then numeric rep). Mode-scoped when given. */
|
|
95
|
+
export declare function findJudgeRawFiles(runDir: string, scenarioId: string, mode?: string): string[];
|
|
96
|
+
/** A single representative transcript file for a scenario in a run dir. Null if none. */
|
|
97
|
+
export declare function findTranscriptFile(runDir: string, scenarioId: string): string | null;
|
|
98
|
+
/**
|
|
99
|
+
* Un-gitignore ALL of a scenario's transcript AND judge-raw artifact files
|
|
100
|
+
* (audit trail for an override — a --reps run has one transcript (and one
|
|
101
|
+
* judge-raw file) per rep, and every rep that drove the verdict must survive
|
|
102
|
+
* a commit, not just an arbitrary one).
|
|
103
|
+
* Appends `!<tag>/<ts>/<id>.<mode>[.rep<k>].txt` (and the matching
|
|
104
|
+
* `.judge.txt`) to results/.gitignore for each, once. The path uses POSIX
|
|
105
|
+
* separators so the negation matches on Windows too (git ignore patterns are
|
|
106
|
+
* always forward-slashed).
|
|
107
|
+
*/
|
|
108
|
+
export declare function preserveTranscript(resultsRoot: string, runDir: string, scenarioId: string): void;
|