@gaunt-sloth/batch 2.0.0-alpha.19
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 +7 -0
- package/dist/BatchRunner.d.ts +18 -0
- package/dist/BatchRunner.js +100 -0
- package/dist/BatchRunner.js.map +1 -0
- package/dist/deterministicChecks.d.ts +16 -0
- package/dist/deterministicChecks.js +34 -0
- package/dist/deterministicChecks.js.map +1 -0
- package/dist/evalOutput.d.ts +11 -0
- package/dist/evalOutput.js +19 -0
- package/dist/evalOutput.js.map +1 -0
- package/dist/evalRunner.d.ts +25 -0
- package/dist/evalRunner.js +100 -0
- package/dist/evalRunner.js.map +1 -0
- package/dist/evalSuite.d.ts +24 -0
- package/dist/evalSuite.js +129 -0
- package/dist/evalSuite.js.map +1 -0
- package/dist/evalTypes.d.ts +98 -0
- package/dist/evalTypes.js +14 -0
- package/dist/evalTypes.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +16 -0
- package/dist/interpolate.js +48 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/judge.d.ts +62 -0
- package/dist/judge.js +119 -0
- package/dist/judge.js.map +1 -0
- package/dist/matrix.d.ts +11 -0
- package/dist/matrix.js +31 -0
- package/dist/matrix.js.map +1 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.js +24 -0
- package/dist/output.js.map +1 -0
- package/dist/parseOver.d.ts +14 -0
- package/dist/parseOver.js +116 -0
- package/dist/parseOver.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow/runWorkflow.d.ts +79 -0
- package/dist/workflow/runWorkflow.js +142 -0
- package/dist/workflow/runWorkflow.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025-present Andrew Kondratev
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BatchRunnerOptions, type BatchSummary, type CellResult, type MatrixCell } from '#src/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run every cell of the matrix through the injected {@link RunCellFn}, capping in-flight work at
|
|
4
|
+
* `concurrency` and retrying a failed cell up to `retry` times.
|
|
5
|
+
*
|
|
6
|
+
* Isolation: cells are independent by construction — nothing here shares state between them beyond
|
|
7
|
+
* the caller-supplied `runCell` closure, matching the "N isolated model calls" framing batch is
|
|
8
|
+
* scoped around (docs/batch-mechanism-vs-judgment.md). A cell throwing (rather than resolving
|
|
9
|
+
* `{ ok: false }`) is caught and recorded as a failure like any other — one bad cell must never
|
|
10
|
+
* take down the whole batch run or the other in-flight cells (the exit-code contract: `gth batch`
|
|
11
|
+
* exits 0 iff the cells ran at all, regardless of how many of them failed).
|
|
12
|
+
*
|
|
13
|
+
* Returns results in the same order as `cells` (not completion order), so callers can zip them back
|
|
14
|
+
* up with the matrix.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runBatchMatrix(cells: MatrixCell[], options: BatchRunnerOptions): Promise<CellResult[]>;
|
|
17
|
+
/** Build the lightweight aggregate "flake report" (pass/fail counts) from the per-cell results. */
|
|
18
|
+
export declare function buildBatchSummary(results: CellResult[]): BatchSummary;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DEFAULT_CONCURRENCY, } from '#src/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run every cell of the matrix through the injected {@link RunCellFn}, capping in-flight work at
|
|
4
|
+
* `concurrency` and retrying a failed cell up to `retry` times.
|
|
5
|
+
*
|
|
6
|
+
* Isolation: cells are independent by construction — nothing here shares state between them beyond
|
|
7
|
+
* the caller-supplied `runCell` closure, matching the "N isolated model calls" framing batch is
|
|
8
|
+
* scoped around (docs/batch-mechanism-vs-judgment.md). A cell throwing (rather than resolving
|
|
9
|
+
* `{ ok: false }`) is caught and recorded as a failure like any other — one bad cell must never
|
|
10
|
+
* take down the whole batch run or the other in-flight cells (the exit-code contract: `gth batch`
|
|
11
|
+
* exits 0 iff the cells ran at all, regardless of how many of them failed).
|
|
12
|
+
*
|
|
13
|
+
* Returns results in the same order as `cells` (not completion order), so callers can zip them back
|
|
14
|
+
* up with the matrix.
|
|
15
|
+
*/
|
|
16
|
+
export async function runBatchMatrix(cells, options) {
|
|
17
|
+
const concurrency = normalizeConcurrency(options.concurrency);
|
|
18
|
+
const retry = normalizeRetry(options.retry);
|
|
19
|
+
const results = new Array(cells.length);
|
|
20
|
+
let nextIndex = 0;
|
|
21
|
+
const worker = async () => {
|
|
22
|
+
for (;;) {
|
|
23
|
+
const i = nextIndex++;
|
|
24
|
+
if (i >= cells.length)
|
|
25
|
+
return;
|
|
26
|
+
results[i] = await runCellWithRetry(cells[i], options.runCell, retry);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const workerCount = Math.min(concurrency, cells.length);
|
|
30
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
async function runCellWithRetry(cell, runCell, retry) {
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
let attempts = 0;
|
|
36
|
+
// Always at least one attempt; `retry` additional attempts on failure.
|
|
37
|
+
for (;;) {
|
|
38
|
+
attempts++;
|
|
39
|
+
try {
|
|
40
|
+
const outcome = await runCell(cell);
|
|
41
|
+
if (outcome.ok || attempts > retry) {
|
|
42
|
+
return {
|
|
43
|
+
...outcome,
|
|
44
|
+
id: cell.id,
|
|
45
|
+
model: cell.model,
|
|
46
|
+
inputIndex: cell.inputIndex,
|
|
47
|
+
inputRow: cell.inputRow,
|
|
48
|
+
durationMs: Date.now() - startedAt,
|
|
49
|
+
retries: attempts - 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Failed but retries remain: loop around for another attempt.
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (attempts > retry) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: error instanceof Error ? error.message : String(error),
|
|
59
|
+
id: cell.id,
|
|
60
|
+
model: cell.model,
|
|
61
|
+
inputIndex: cell.inputIndex,
|
|
62
|
+
inputRow: cell.inputRow,
|
|
63
|
+
durationMs: Date.now() - startedAt,
|
|
64
|
+
retries: attempts - 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Exception with retries remaining: loop around for another attempt.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function normalizeConcurrency(concurrency) {
|
|
72
|
+
if (concurrency === undefined || !Number.isFinite(concurrency) || concurrency < 1) {
|
|
73
|
+
return DEFAULT_CONCURRENCY;
|
|
74
|
+
}
|
|
75
|
+
return Math.floor(concurrency);
|
|
76
|
+
}
|
|
77
|
+
function normalizeRetry(retry) {
|
|
78
|
+
if (retry === undefined || !Number.isFinite(retry) || retry < 0) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return Math.floor(retry);
|
|
82
|
+
}
|
|
83
|
+
/** Build the lightweight aggregate "flake report" (pass/fail counts) from the per-cell results. */
|
|
84
|
+
export function buildBatchSummary(results) {
|
|
85
|
+
const cells = results.map((r) => ({
|
|
86
|
+
id: r.id,
|
|
87
|
+
model: r.model,
|
|
88
|
+
inputIndex: r.inputIndex,
|
|
89
|
+
ok: r.ok,
|
|
90
|
+
retries: r.retries,
|
|
91
|
+
}));
|
|
92
|
+
const passed = cells.filter((c) => c.ok).length;
|
|
93
|
+
return {
|
|
94
|
+
total: cells.length,
|
|
95
|
+
passed,
|
|
96
|
+
failed: cells.length - passed,
|
|
97
|
+
cells,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=BatchRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BatchRunner.js","sourceRoot":"","sources":["../src/BatchRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DeterministicCheckResult, EvalCase } from '#src/evalTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Case-insensitive substring checks over an SUT answer. Ported, not reinvented, from the field
|
|
4
|
+
* user's proven `deterministic()` function (`docs/batch-eval-user-requirements.md` Appendix A):
|
|
5
|
+
*
|
|
6
|
+
* - `mustContain` — every entry must appear (case-insensitive substring); each miss is reported.
|
|
7
|
+
* - `mustNotContain` — no entry may appear; each hit is reported.
|
|
8
|
+
* - `shouldContainAny` — at least one entry must appear; reported as a single combined failure
|
|
9
|
+
* (not one per missing option — the check is "at least one", so there is only one way to fail
|
|
10
|
+
* it) when none do.
|
|
11
|
+
*
|
|
12
|
+
* A case with all three arrays empty trivially passes (no checks to fail) — the suite parser
|
|
13
|
+
* (`#src/evalSuite.js`) is what enforces that a case has *some* check or a judge rubric; this
|
|
14
|
+
* function itself has no opinion on that.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runDeterministicChecks(answer: string, evalCase: Pick<EvalCase, 'mustContain' | 'mustNotContain' | 'shouldContainAny'>): DeterministicCheckResult;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Case-insensitive substring checks over an SUT answer. Ported, not reinvented, from the field
|
|
3
|
+
* user's proven `deterministic()` function (`docs/batch-eval-user-requirements.md` Appendix A):
|
|
4
|
+
*
|
|
5
|
+
* - `mustContain` — every entry must appear (case-insensitive substring); each miss is reported.
|
|
6
|
+
* - `mustNotContain` — no entry may appear; each hit is reported.
|
|
7
|
+
* - `shouldContainAny` — at least one entry must appear; reported as a single combined failure
|
|
8
|
+
* (not one per missing option — the check is "at least one", so there is only one way to fail
|
|
9
|
+
* it) when none do.
|
|
10
|
+
*
|
|
11
|
+
* A case with all three arrays empty trivially passes (no checks to fail) — the suite parser
|
|
12
|
+
* (`#src/evalSuite.js`) is what enforces that a case has *some* check or a judge rubric; this
|
|
13
|
+
* function itself has no opinion on that.
|
|
14
|
+
*/
|
|
15
|
+
export function runDeterministicChecks(answer, evalCase) {
|
|
16
|
+
const text = answer.toLowerCase();
|
|
17
|
+
const failures = [];
|
|
18
|
+
for (const needle of evalCase.mustContain) {
|
|
19
|
+
if (!text.includes(needle.toLowerCase())) {
|
|
20
|
+
failures.push(`missing "${needle}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const needle of evalCase.mustNotContain) {
|
|
24
|
+
if (text.includes(needle.toLowerCase())) {
|
|
25
|
+
failures.push(`forbidden "${needle}"`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (evalCase.shouldContainAny.length > 0 &&
|
|
29
|
+
!evalCase.shouldContainAny.some((needle) => text.includes(needle.toLowerCase()))) {
|
|
30
|
+
failures.push(`none of [${evalCase.shouldContainAny.join(' | ')}]`);
|
|
31
|
+
}
|
|
32
|
+
return { passed: failures.length === 0, failures };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=deterministicChecks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deterministicChecks.js","sourceRoot":"","sources":["../src/deterministicChecks.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,QAA+E;IAE/E,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACxC,QAAQ,CAAC,IAAI,CAAC,cAAc,MAAM,GAAG,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,IACE,QAAQ,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACpC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,EAChF,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,YAAY,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EvalSuiteSummary } from '#src/evalTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Write one structured JSON record per case (`<id>.json`) plus one aggregate `results.json`
|
|
4
|
+
* (suite totals + every case's verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir`
|
|
5
|
+
* (and any missing parents) if it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/
|
|
6
|
+
* output.js`) — same convention, applied to eval's richer per-case shape.
|
|
7
|
+
*
|
|
8
|
+
* Pure I/O, deliberately separate from {@link ../evalRunner.js}'s `runEvalSuite`: the runner never
|
|
9
|
+
* touches the filesystem, so unit tests can exercise grading logic without a tmp dir.
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeEvalOutput(outputDir: string, summary: EvalSuiteSummary): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Write one structured JSON record per case (`<id>.json`) plus one aggregate `results.json`
|
|
5
|
+
* (suite totals + every case's verdict/checks/judge/reasons) into `outputDir`. Creates `outputDir`
|
|
6
|
+
* (and any missing parents) if it doesn't exist. Mirrors BATCH-1's `writeBatchOutput` (`#src/
|
|
7
|
+
* output.js`) — same convention, applied to eval's richer per-case shape.
|
|
8
|
+
*
|
|
9
|
+
* Pure I/O, deliberately separate from {@link ../evalRunner.js}'s `runEvalSuite`: the runner never
|
|
10
|
+
* touches the filesystem, so unit tests can exercise grading logic without a tmp dir.
|
|
11
|
+
*/
|
|
12
|
+
export function writeEvalOutput(outputDir, summary) {
|
|
13
|
+
mkdirSync(outputDir, { recursive: true });
|
|
14
|
+
for (const result of summary.cases) {
|
|
15
|
+
writeFileSync(join(outputDir, `${result.id}.json`), `${JSON.stringify(result, null, 2)}\n`);
|
|
16
|
+
}
|
|
17
|
+
writeFileSync(join(outputDir, 'results.json'), `${JSON.stringify(summary, null, 2)}\n`);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=evalOutput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evalOutput.js","sourceRoot":"","sources":["../src/evalOutput.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,OAAyB;IAC1E,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1C,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACnC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9F,CAAC;IAED,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC1F,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RunCellFn } from '#src/types.js';
|
|
2
|
+
import type { EvalSuite, EvalSuiteSummary, JudgeFn } from '#src/evalTypes.js';
|
|
3
|
+
/** Options for {@link runEvalSuite}. */
|
|
4
|
+
export interface RunEvalSuiteOptions {
|
|
5
|
+
/** The injectable per-case run function — same seam as BATCH-1's `RunCellFn`, adapted to send
|
|
6
|
+
* each case's `prompt` through the SUT (production wiring: `evalCommand.ts`, reusing
|
|
7
|
+
* `batchCommand.ts`'s `buildProductionRunCell`; tests inject a fake). */
|
|
8
|
+
runCell: RunCellFn;
|
|
9
|
+
/** The injectable judge function. Only consulted for cases that declare a `judgeRubric`;
|
|
10
|
+
* omitted entirely = every judge-rubric case fails with a "no judge configured" reason (this
|
|
11
|
+
* should not happen in production wiring, where `evalCommand.ts` always supplies one, but the
|
|
12
|
+
* runner degrades safely rather than throwing if it's left out). */
|
|
13
|
+
judge?: JudgeFn;
|
|
14
|
+
/** Max in-flight cases — reuses BATCH-1's `runBatchMatrix` pool (`DEFAULT_CONCURRENCY` when
|
|
15
|
+
* omitted); no second concurrency mechanism is introduced for eval. */
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run every case in the suite through the SUT ({@link RunEvalSuiteOptions.runCell}, pooled via
|
|
20
|
+
* BATCH-1's `runBatchMatrix`), then grade each answer with deterministic checks and (when the case
|
|
21
|
+
* declares a rubric) the judge. A case PASSES iff its deterministic checks pass AND (it has no
|
|
22
|
+
* judge rubric OR the judge's rate is at/above the case's `passThreshold`) — ported from the field
|
|
23
|
+
* user's proven harness semantics (`docs/batch-eval-user-requirements.md` Appendix A).
|
|
24
|
+
*/
|
|
25
|
+
export declare function runEvalSuite(suite: EvalSuite, options: RunEvalSuiteOptions): Promise<EvalSuiteSummary>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { runBatchMatrix } from '#src/BatchRunner.js';
|
|
2
|
+
import { runDeterministicChecks } from '#src/deterministicChecks.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build one {@link MatrixCell} per case, `content` = the case's `prompt`. Unlike BATCH-1's
|
|
5
|
+
* `buildMatrix` (a models × rows cross product), eval has exactly one cell per case — so cells are
|
|
6
|
+
* built directly rather than via `buildMatrix`/`bindCellContent`, which would force an artificial
|
|
7
|
+
* single-row/single-model matrix per case for no benefit (no `{{field}}` interpolation applies to
|
|
8
|
+
* an eval case's literal `prompt`).
|
|
9
|
+
*
|
|
10
|
+
* `cell.id` is the case's own `id` (not a `cell-<m>-<r>` index) so per-case JSON output filenames
|
|
11
|
+
* read naturally (`<case-id>.json` — see `#src/evalOutput.js`) and results are trivially traceable
|
|
12
|
+
* back to the authored case. `parseEvalSuite` already rejects duplicate case ids, so this can't
|
|
13
|
+
* collide.
|
|
14
|
+
*/
|
|
15
|
+
function buildEvalCells(cases) {
|
|
16
|
+
return cases.map((evalCase, index) => ({
|
|
17
|
+
id: evalCase.id,
|
|
18
|
+
modelIndex: 0,
|
|
19
|
+
inputIndex: index,
|
|
20
|
+
content: evalCase.prompt,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run every case in the suite through the SUT ({@link RunEvalSuiteOptions.runCell}, pooled via
|
|
25
|
+
* BATCH-1's `runBatchMatrix`), then grade each answer with deterministic checks and (when the case
|
|
26
|
+
* declares a rubric) the judge. A case PASSES iff its deterministic checks pass AND (it has no
|
|
27
|
+
* judge rubric OR the judge's rate is at/above the case's `passThreshold`) — ported from the field
|
|
28
|
+
* user's proven harness semantics (`docs/batch-eval-user-requirements.md` Appendix A).
|
|
29
|
+
*/
|
|
30
|
+
export async function runEvalSuite(suite, options) {
|
|
31
|
+
const cells = buildEvalCells(suite.cases);
|
|
32
|
+
const cellResults = await runBatchMatrix(cells, {
|
|
33
|
+
runCell: options.runCell,
|
|
34
|
+
concurrency: options.concurrency,
|
|
35
|
+
});
|
|
36
|
+
const casesById = new Map(suite.cases.map((evalCase) => [evalCase.id, evalCase]));
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const cellResult of cellResults) {
|
|
39
|
+
const evalCase = casesById.get(cellResult.id);
|
|
40
|
+
/* istanbul ignore next -- cell ids are derived 1:1 from case ids by buildEvalCells above */
|
|
41
|
+
if (!evalCase)
|
|
42
|
+
continue;
|
|
43
|
+
results.push(await gradeCase(evalCase, cellResult, options.judge));
|
|
44
|
+
}
|
|
45
|
+
const passed = results.filter((result) => result.verdict === 'PASS').length;
|
|
46
|
+
return { total: results.length, passed, failed: results.length - passed, cases: results };
|
|
47
|
+
}
|
|
48
|
+
async function gradeCase(evalCase, cellResult, judge) {
|
|
49
|
+
const base = {
|
|
50
|
+
id: evalCase.id,
|
|
51
|
+
passThreshold: evalCase.passThreshold,
|
|
52
|
+
answer: cellResult.answer,
|
|
53
|
+
tokensInput: cellResult.tokensInput,
|
|
54
|
+
tokensOutput: cellResult.tokensOutput,
|
|
55
|
+
tools: cellResult.tools,
|
|
56
|
+
durationMs: cellResult.durationMs,
|
|
57
|
+
};
|
|
58
|
+
if (!cellResult.ok) {
|
|
59
|
+
return {
|
|
60
|
+
...base,
|
|
61
|
+
verdict: 'FAIL',
|
|
62
|
+
sutOk: false,
|
|
63
|
+
reasons: [cellResult.error ? `SUT run failed: ${cellResult.error}` : 'SUT run failed.'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const answer = cellResult.answer ?? '';
|
|
67
|
+
const checks = runDeterministicChecks(answer, evalCase);
|
|
68
|
+
const reasons = [...checks.failures];
|
|
69
|
+
let judgeOutcome;
|
|
70
|
+
let judgePassed = true;
|
|
71
|
+
if (evalCase.judgeRubric) {
|
|
72
|
+
if (!judge) {
|
|
73
|
+
judgeOutcome = { attempted: false, ok: false, error: 'No judge configured for this suite.' };
|
|
74
|
+
judgePassed = false;
|
|
75
|
+
reasons.push('judge: not configured');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
judgeOutcome = await judge(answer, evalCase.judgeRubric);
|
|
79
|
+
if (!judgeOutcome.ok || !judgeOutcome.verdict) {
|
|
80
|
+
judgePassed = false;
|
|
81
|
+
reasons.push(`judge error: ${judgeOutcome.error ?? 'unknown error'}`);
|
|
82
|
+
}
|
|
83
|
+
else if (judgeOutcome.verdict.rate < evalCase.passThreshold) {
|
|
84
|
+
judgePassed = false;
|
|
85
|
+
reasons.push(`judge rate ${judgeOutcome.verdict.rate}/10 below threshold ${evalCase.passThreshold}` +
|
|
86
|
+
(judgeOutcome.verdict.reason ? `: ${judgeOutcome.verdict.reason}` : ''));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const verdict = checks.passed && judgePassed ? 'PASS' : 'FAIL';
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
verdict,
|
|
94
|
+
sutOk: true,
|
|
95
|
+
checks,
|
|
96
|
+
judge: judgeOutcome,
|
|
97
|
+
reasons,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=evalRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evalRunner.js","sourceRoot":"","sources":["../src/evalRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EvalSuite } from '#src/evalTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse and validate an eval suite YAML document into a normalized {@link EvalSuite}.
|
|
4
|
+
*
|
|
5
|
+
* Rejects, with a clear message, at parse time (never silently no-ops or defers to run time):
|
|
6
|
+
* - Malformed YAML.
|
|
7
|
+
* - A suite shape that doesn't match {@link RawSuiteSchema} (missing/wrong-typed fields).
|
|
8
|
+
* - `target.type` other than `"gth-agent"` — pluggable CLI/HTTP targets are out of scope for this
|
|
9
|
+
* task (see the CLI surface doc's future `target: {type: cli|http}` sketch).
|
|
10
|
+
* - `target.profile` set to anything other than `"default"`/absent — a single suite-wide profile
|
|
11
|
+
* switch is the same `--identities` direction the brief scopes out; this task's target is always
|
|
12
|
+
* whatever profile `gth eval` itself was invoked under (`-i/--identity-profile`, if any).
|
|
13
|
+
* - A case `id` containing anything other than alphanumerics, dashes, underscores, or dots (case
|
|
14
|
+
* ids double as output filenames — see `#src/evalOutput.js` — so a path separator or traversal
|
|
15
|
+
* sequence like `../../etc/passwd` is rejected here, not sanitized).
|
|
16
|
+
* - A duplicate case `id` (case ids double as output filenames — see `#src/evalOutput.js`).
|
|
17
|
+
* - A case with **no** deterministic checks (`must_contain`/`must_not_contain`/
|
|
18
|
+
* `should_contain_any` all absent/empty) **and no** `judge` rubric: nothing would ever grade it,
|
|
19
|
+
* which is a suite-authoring bug, not a case that trivially passes.
|
|
20
|
+
*
|
|
21
|
+
* @param yamlText Raw suite file content.
|
|
22
|
+
* @param sourcePath Optional path, only used to make error messages more actionable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseEvalSuite(yamlText: string, sourcePath?: string): EvalSuite;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
import { DEFAULT_EVAL_PASS_THRESHOLD } from '#src/evalTypes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Raw suite-file shape (snake_case, as authored) — see the BATCH-2 Task 2 brief and
|
|
6
|
+
* `docs/batch-eval-user-requirements.md`'s YAML sketch, trimmed to the single-`prompt`,
|
|
7
|
+
* no-identities, no-fixtures subset this task implements:
|
|
8
|
+
*
|
|
9
|
+
* ```yaml
|
|
10
|
+
* target: { type: gth-agent, profile: default }
|
|
11
|
+
* defaults: { pass_threshold: 6 }
|
|
12
|
+
* cases:
|
|
13
|
+
* - id: some-case-id
|
|
14
|
+
* prompt: "the user message to send"
|
|
15
|
+
* must_contain: [ "foo", "bar" ]
|
|
16
|
+
* must_not_contain: [ "baz" ]
|
|
17
|
+
* should_contain_any: [ "x", "y" ]
|
|
18
|
+
* judge: "Answers with a ranked summary and correctly formatted values."
|
|
19
|
+
* pass_threshold: 7
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
const RawCaseSchema = z.object({
|
|
23
|
+
id: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1, 'case id must be a non-empty string')
|
|
26
|
+
.regex(/^[\w.-]+$/, 'case id must be a valid filename (alphanumeric, dashes, underscores, dots) — case ids ' +
|
|
27
|
+
'double as output filenames, so path separators and other special characters are rejected'),
|
|
28
|
+
prompt: z.string().min(1, 'case prompt must be a non-empty string'),
|
|
29
|
+
must_contain: z.array(z.string()).optional(),
|
|
30
|
+
must_not_contain: z.array(z.string()).optional(),
|
|
31
|
+
should_contain_any: z.array(z.string()).optional(),
|
|
32
|
+
judge: z.string().optional(),
|
|
33
|
+
pass_threshold: z.number().min(0).max(10).optional(),
|
|
34
|
+
});
|
|
35
|
+
const RawSuiteSchema = z.object({
|
|
36
|
+
target: z.object({
|
|
37
|
+
type: z.string(),
|
|
38
|
+
profile: z.string().optional(),
|
|
39
|
+
}),
|
|
40
|
+
defaults: z
|
|
41
|
+
.object({
|
|
42
|
+
pass_threshold: z.number().min(0).max(10).optional(),
|
|
43
|
+
})
|
|
44
|
+
.optional(),
|
|
45
|
+
cases: z.array(RawCaseSchema).min(1, 'suite must declare at least one case'),
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Parse and validate an eval suite YAML document into a normalized {@link EvalSuite}.
|
|
49
|
+
*
|
|
50
|
+
* Rejects, with a clear message, at parse time (never silently no-ops or defers to run time):
|
|
51
|
+
* - Malformed YAML.
|
|
52
|
+
* - A suite shape that doesn't match {@link RawSuiteSchema} (missing/wrong-typed fields).
|
|
53
|
+
* - `target.type` other than `"gth-agent"` — pluggable CLI/HTTP targets are out of scope for this
|
|
54
|
+
* task (see the CLI surface doc's future `target: {type: cli|http}` sketch).
|
|
55
|
+
* - `target.profile` set to anything other than `"default"`/absent — a single suite-wide profile
|
|
56
|
+
* switch is the same `--identities` direction the brief scopes out; this task's target is always
|
|
57
|
+
* whatever profile `gth eval` itself was invoked under (`-i/--identity-profile`, if any).
|
|
58
|
+
* - A case `id` containing anything other than alphanumerics, dashes, underscores, or dots (case
|
|
59
|
+
* ids double as output filenames — see `#src/evalOutput.js` — so a path separator or traversal
|
|
60
|
+
* sequence like `../../etc/passwd` is rejected here, not sanitized).
|
|
61
|
+
* - A duplicate case `id` (case ids double as output filenames — see `#src/evalOutput.js`).
|
|
62
|
+
* - A case with **no** deterministic checks (`must_contain`/`must_not_contain`/
|
|
63
|
+
* `should_contain_any` all absent/empty) **and no** `judge` rubric: nothing would ever grade it,
|
|
64
|
+
* which is a suite-authoring bug, not a case that trivially passes.
|
|
65
|
+
*
|
|
66
|
+
* @param yamlText Raw suite file content.
|
|
67
|
+
* @param sourcePath Optional path, only used to make error messages more actionable.
|
|
68
|
+
*/
|
|
69
|
+
export function parseEvalSuite(yamlText, sourcePath) {
|
|
70
|
+
const suffix = sourcePath ? ` (${sourcePath})` : '';
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = parseYaml(yamlText);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`Failed to parse eval suite YAML${suffix}: ` +
|
|
77
|
+
(error instanceof Error ? error.message : String(error)));
|
|
78
|
+
}
|
|
79
|
+
const parsed = RawSuiteSchema.safeParse(raw);
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
const issues = parsed.error.issues
|
|
82
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
83
|
+
.join('; ');
|
|
84
|
+
throw new Error(`Invalid eval suite${suffix}: ${issues}`);
|
|
85
|
+
}
|
|
86
|
+
const data = parsed.data;
|
|
87
|
+
if (data.target.type !== 'gth-agent') {
|
|
88
|
+
throw new Error(`Invalid eval suite${suffix}: unsupported target.type "${data.target.type}" — this version ` +
|
|
89
|
+
'of `gth eval` only supports "gth-agent" (pluggable CLI/HTTP targets are future scope).');
|
|
90
|
+
}
|
|
91
|
+
if (data.target.profile !== undefined && data.target.profile !== 'default') {
|
|
92
|
+
throw new Error(`Invalid eval suite${suffix}: unsupported target.profile "${data.target.profile}" — ` +
|
|
93
|
+
'per-profile/identity targets are not supported yet (see --identities in ' +
|
|
94
|
+
'docs/batch-eval-cli-surface.md); omit target.profile or set it to "default".');
|
|
95
|
+
}
|
|
96
|
+
const suiteDefaultThreshold = data.defaults?.pass_threshold ?? DEFAULT_EVAL_PASS_THRESHOLD;
|
|
97
|
+
const seenIds = new Set();
|
|
98
|
+
const cases = data.cases.map((rawCase, index) => {
|
|
99
|
+
if (seenIds.has(rawCase.id)) {
|
|
100
|
+
throw new Error(`Invalid eval suite${suffix}: duplicate case id "${rawCase.id}".`);
|
|
101
|
+
}
|
|
102
|
+
seenIds.add(rawCase.id);
|
|
103
|
+
const mustContain = rawCase.must_contain ?? [];
|
|
104
|
+
const mustNotContain = rawCase.must_not_contain ?? [];
|
|
105
|
+
const shouldContainAny = rawCase.should_contain_any ?? [];
|
|
106
|
+
const hasChecks = mustContain.length > 0 || mustNotContain.length > 0 || shouldContainAny.length > 0;
|
|
107
|
+
const judgeRubric = rawCase.judge?.trim();
|
|
108
|
+
const hasJudge = !!judgeRubric;
|
|
109
|
+
if (!hasChecks && !hasJudge) {
|
|
110
|
+
throw new Error(`Invalid eval suite${suffix}: case "${rawCase.id}" (index ${index}) has no deterministic ` +
|
|
111
|
+
'checks and no judge rubric — a case must declare at least one of must_contain / ' +
|
|
112
|
+
'must_not_contain / should_contain_any, or a judge rubric.');
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
id: rawCase.id,
|
|
116
|
+
prompt: rawCase.prompt,
|
|
117
|
+
mustContain,
|
|
118
|
+
mustNotContain,
|
|
119
|
+
shouldContainAny,
|
|
120
|
+
judgeRubric: hasJudge ? judgeRubric : undefined,
|
|
121
|
+
passThreshold: rawCase.pass_threshold ?? suiteDefaultThreshold,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
target: { type: 'gth-agent', profile: data.target.profile },
|
|
126
|
+
cases,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=evalSuite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evalSuite.js","sourceRoot":"","sources":["../src/evalSuite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* BATCH-2 — the shapes for `gth eval`: a parsed suite/case, deterministic-check results, the
|
|
4
|
+
* judge's verdict, and per-case/suite outcomes. Deliberately separate from {@link ../types.js}
|
|
5
|
+
* (BATCH-1's cell/outcome shapes), which documents itself as scoped to "cells and outcomes" only
|
|
6
|
+
* — eval's shapes layer on top of (not into) that file.
|
|
7
|
+
*/
|
|
8
|
+
/** The 0-10 judge scale's default pass threshold, matching `review`'s own (unexported)
|
|
9
|
+
* `DEFAULT_PASS_THRESHOLD` in `packages/review/src/middleware/reviewRateMiddleware.ts` — same
|
|
10
|
+
* scale, same default, so a user who knows `review`'s threshold semantics already knows eval's.
|
|
11
|
+
* Kept as our own constant (not imported) since review's is private to that module and coupled to
|
|
12
|
+
* middleware/tool-call/artifact-store plumbing that doesn't fit a plain structured-output call. */
|
|
13
|
+
export declare const DEFAULT_EVAL_PASS_THRESHOLD = 6;
|
|
14
|
+
/** One case's target — this task only supports `gth-agent` against the run's own resolved config
|
|
15
|
+
* (no `--identities`, no pluggable CLI/HTTP targets — see the BATCH-2 Task 2 brief). */
|
|
16
|
+
export interface EvalTarget {
|
|
17
|
+
type: 'gth-agent';
|
|
18
|
+
/** Suite-level profile hint. Only `undefined`/`'default'` is accepted this task (see
|
|
19
|
+
* {@link ../evalSuite.js}'s `parseEvalSuite`) — per-case/per-identity profile switching is
|
|
20
|
+
* `--identities` scope, not this task's. */
|
|
21
|
+
profile?: string;
|
|
22
|
+
}
|
|
23
|
+
/** One case parsed and normalized from suite YAML — snake_case YAML keys become camelCase here,
|
|
24
|
+
* arrays default to `[]` (not `undefined`) so callers never need an existence check, and
|
|
25
|
+
* `passThreshold` is pre-resolved (case override ?? suite `defaults.pass_threshold` ??
|
|
26
|
+
* {@link DEFAULT_EVAL_PASS_THRESHOLD}). */
|
|
27
|
+
export interface EvalCase {
|
|
28
|
+
id: string;
|
|
29
|
+
prompt: string;
|
|
30
|
+
mustContain: string[];
|
|
31
|
+
mustNotContain: string[];
|
|
32
|
+
shouldContainAny: string[];
|
|
33
|
+
/** The judge rubric, when present and non-blank. `undefined` = no judge for this case. */
|
|
34
|
+
judgeRubric?: string;
|
|
35
|
+
passThreshold: number;
|
|
36
|
+
}
|
|
37
|
+
/** A fully parsed and validated suite — see {@link ../evalSuite.js}'s `parseEvalSuite`. */
|
|
38
|
+
export interface EvalSuite {
|
|
39
|
+
target: EvalTarget;
|
|
40
|
+
cases: EvalCase[];
|
|
41
|
+
}
|
|
42
|
+
/** The result of running one case's answer through its deterministic checks. */
|
|
43
|
+
export interface DeterministicCheckResult {
|
|
44
|
+
passed: boolean;
|
|
45
|
+
/** Human-readable failure reasons, e.g. `missing "foo"` / `forbidden "baz"` /
|
|
46
|
+
* `none of [x | y]` — the exact message shapes from the field user's proven harness
|
|
47
|
+
* (`docs/batch-eval-user-requirements.md` Appendix A's `deterministic()`). Empty when passed. */
|
|
48
|
+
failures: string[];
|
|
49
|
+
}
|
|
50
|
+
/** The judge's structured verdict on one case's answer — matches `review`'s `RateSchema` shape
|
|
51
|
+
* (0-10 `rate` + a reason string) for UX consistency, see {@link ../judge.js}. */
|
|
52
|
+
export interface JudgeVerdict {
|
|
53
|
+
rate: number;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
/** The outcome of attempting to grade one case's answer with the judge. */
|
|
57
|
+
export interface JudgeOutcome {
|
|
58
|
+
/** `true` once a judge call was actually made (vs. skipped because no judge was configured). */
|
|
59
|
+
attempted: boolean;
|
|
60
|
+
/** `true` when the judge produced a usable {@link JudgeVerdict}. `false` on any error, timeout,
|
|
61
|
+
* or unparseable output — per the BATCH-2 brief, a judge that can't produce a verdict FAILS the
|
|
62
|
+
* case (there is no human to escalate to here, unlike EXT-10's shell-safety judge). */
|
|
63
|
+
ok: boolean;
|
|
64
|
+
verdict?: JudgeVerdict;
|
|
65
|
+
/** Set when `ok` is `false`: why the judge didn't produce a verdict. */
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Injectable "grade one answer against one rubric" function — the seam that lets
|
|
69
|
+
* {@link ../evalRunner.js}'s `runEvalSuite` be fully unit tested without any real LLM call, mirror
|
|
70
|
+
* of BATCH-1's `RunCellFn`. The production wiring (`evalCommand.ts`) adapts `judgeEvalCase` to
|
|
71
|
+
* this shape; tests inject a fake that resolves/fails as needed. */
|
|
72
|
+
export type JudgeFn = (answer: string, rubric: string) => Promise<JudgeOutcome>;
|
|
73
|
+
/** One case's full graded outcome, as written to `<id>.json` and summarized in `results.json`. */
|
|
74
|
+
export interface EvalCaseResult {
|
|
75
|
+
id: string;
|
|
76
|
+
verdict: 'PASS' | 'FAIL';
|
|
77
|
+
passThreshold: number;
|
|
78
|
+
/** Whether the SUT run itself completed without error (mirrors `CellRunOutcome.ok`). `false`
|
|
79
|
+
* means the case FAILs outright — there is no answer text to check or grade. */
|
|
80
|
+
sutOk: boolean;
|
|
81
|
+
answer?: string;
|
|
82
|
+
tokensInput?: number;
|
|
83
|
+
tokensOutput?: number;
|
|
84
|
+
tools?: string[];
|
|
85
|
+
durationMs: number;
|
|
86
|
+
checks?: DeterministicCheckResult;
|
|
87
|
+
judge?: JudgeOutcome;
|
|
88
|
+
/** Every reason the case FAILed (deterministic check failures, judge-below-threshold, judge
|
|
89
|
+
* error, SUT failure). Empty when `verdict` is `PASS`. */
|
|
90
|
+
reasons: string[];
|
|
91
|
+
}
|
|
92
|
+
/** The suite-level aggregate written to `results.json` — `gth eval` exits 0 iff `failed === 0`. */
|
|
93
|
+
export interface EvalSuiteSummary {
|
|
94
|
+
total: number;
|
|
95
|
+
passed: number;
|
|
96
|
+
failed: number;
|
|
97
|
+
cases: EvalCaseResult[];
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* BATCH-2 — the shapes for `gth eval`: a parsed suite/case, deterministic-check results, the
|
|
4
|
+
* judge's verdict, and per-case/suite outcomes. Deliberately separate from {@link ../types.js}
|
|
5
|
+
* (BATCH-1's cell/outcome shapes), which documents itself as scoped to "cells and outcomes" only
|
|
6
|
+
* — eval's shapes layer on top of (not into) that file.
|
|
7
|
+
*/
|
|
8
|
+
/** The 0-10 judge scale's default pass threshold, matching `review`'s own (unexported)
|
|
9
|
+
* `DEFAULT_PASS_THRESHOLD` in `packages/review/src/middleware/reviewRateMiddleware.ts` — same
|
|
10
|
+
* scale, same default, so a user who knows `review`'s threshold semantics already knows eval's.
|
|
11
|
+
* Kept as our own constant (not imported) since review's is private to that module and coupled to
|
|
12
|
+
* middleware/tool-call/artifact-store plumbing that doesn't fit a plain structured-output call. */
|
|
13
|
+
export const DEFAULT_EVAL_PASS_THRESHOLD = 6;
|
|
14
|
+
//# sourceMappingURL=evalTypes.js.map
|