@gaunt-sloth/batch 2.0.0-alpha.24 → 2.0.0-alpha.25
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/README.md +8 -6
- package/dist/BatchRunner.d.ts +20 -0
- package/dist/BatchRunner.js +28 -2
- package/dist/BatchRunner.js.map +1 -1
- package/dist/blindExport.d.ts +88 -0
- package/dist/blindExport.js +129 -0
- package/dist/blindExport.js.map +1 -0
- package/dist/classification.d.ts +52 -0
- package/dist/classification.js +140 -0
- package/dist/classification.js.map +1 -0
- package/dist/classificationRender.d.ts +24 -0
- package/dist/classificationRender.js +96 -0
- package/dist/classificationRender.js.map +1 -0
- package/dist/classificationReport.d.ts +11 -0
- package/dist/classificationReport.js +60 -0
- package/dist/classificationReport.js.map +1 -0
- package/dist/classificationTypes.d.ts +311 -0
- package/dist/classificationTypes.js +40 -0
- package/dist/classificationTypes.js.map +1 -0
- package/dist/evalCompare.d.ts +108 -0
- package/dist/evalCompare.js +246 -0
- package/dist/evalCompare.js.map +1 -0
- package/dist/evalRunner.d.ts +34 -3
- package/dist/evalRunner.js +259 -9
- package/dist/evalRunner.js.map +1 -1
- package/dist/evalSuite.d.ts +12 -2
- package/dist/evalSuite.js +534 -8
- package/dist/evalSuite.js.map +1 -1
- package/dist/evalTypes.d.ts +358 -5
- package/dist/evalTypes.js +98 -0
- package/dist/evalTypes.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +50 -0
- package/dist/metrics.js +433 -0
- package/dist/metrics.js.map +1 -0
- package/dist/pipelineCli.js +1 -1
- package/dist/pipelineCli.js.map +1 -1
- package/dist/raterTarget.d.ts +94 -0
- package/dist/raterTarget.js +328 -0
- package/dist/raterTarget.js.map +1 -0
- package/dist/reporters/reporterTypes.d.ts +9 -0
- package/dist/reporters/textReporter.js +21 -0
- package/dist/reporters/textReporter.js.map +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.js +14 -2
- package/dist/types.js.map +1 -1
- package/dist/workflow/runWorkflow.d.ts +1 -1
- package/dist/workflow/runWorkflow.js +2 -2
- package/dist/workflow/runWorkflow.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -42,8 +42,8 @@ A case passes when its deterministic checks hold (`must_contain` / `must_not_con
|
|
|
42
42
|
`should_contain_any`) and, if a `judge` rubric is set, the LLM judge rates the answer at or above
|
|
43
43
|
`pass_threshold` (0–10 scale, suite default `6`). `gth eval` prints a `PASS`/`FAIL` line per case
|
|
44
44
|
plus a suite total, writes structured per-case JSON and a `results.json` summary to a timestamped
|
|
45
|
-
output dir (override with `-o <dir>`), and exits non-zero if any case failed.
|
|
46
|
-
in-flight cases.
|
|
45
|
+
output dir (override with `-o <dir>`), and exits non-zero if any case failed. `-j <n>` caps
|
|
46
|
+
in-flight cases (default `1` — cases run one at a time until you ask for more).
|
|
47
47
|
|
|
48
48
|
### Examples
|
|
49
49
|
|
|
@@ -59,8 +59,9 @@ gth workflow rank-models.mjs --args '{"topic":"robotics"}'
|
|
|
59
59
|
comma-separated; omit to use the configured model) and/or content-bound input rows (`--over
|
|
60
60
|
<file.csv|file.jsonl>` — one cell per row, with `{{field}}` placeholders bound from the row). It
|
|
61
61
|
writes the same structured per-cell output as `eval` but — unlike `eval` — exits `0` as long as the
|
|
62
|
-
cells *ran*: a poor answer is not a harness failure. `-j <n>` caps concurrency
|
|
63
|
-
a failed cell (default `0`), `-o
|
|
62
|
+
cells *ran*: a poor answer is not a harness failure. `-j <n>` caps concurrency (default `1` — cells
|
|
63
|
+
run one at a time until you ask for more), `--retry <n>` retries a failed cell (default `0`), `-o
|
|
64
|
+
<dir>` sets the output dir.
|
|
64
65
|
|
|
65
66
|
`gth workflow <script.mjs>` runs a local ESM script whose default export is `async (ctx) => result`;
|
|
66
67
|
the return value is printed (a string as-is, anything else as pretty JSON), and `--args <json>` is
|
|
@@ -108,10 +109,11 @@ import { runEvalSuite } from '@gaunt-sloth/batch/evalRunner.js';
|
|
|
108
109
|
The public API (see the package's `index.ts`) groups by command:
|
|
109
110
|
|
|
110
111
|
- **Matrix** (`gth batch`): `buildMatrix`, `bindCellContent`, `parseOverFile`, `runBatchMatrix`,
|
|
111
|
-
`buildBatchSummary`, `writeBatchOutput`, `
|
|
112
|
+
`buildBatchSummary`, `writeBatchOutput`, `DEFAULT_CELL_CONCURRENCY`.
|
|
112
113
|
- **Eval** (`gth eval`): `parseEvalSuite`, `runDeterministicChecks`, `judgeEvalCase`, `runEvalSuite`,
|
|
113
114
|
`writeEvalOutput`, `EvalVerdictSchema`, `DEFAULT_EVAL_PASS_THRESHOLD`.
|
|
114
|
-
- **Workflow** (`gth workflow`): `runWorkflow
|
|
115
|
+
- **Workflow** (`gth workflow`): `runWorkflow`, `DEFAULT_WORKFLOW_CONCURRENCY` (the
|
|
116
|
+
`ctx.parallel()` fan-out cap, independent of the cell default).
|
|
115
117
|
|
|
116
118
|
The corresponding TypeScript types (`MatrixCell`, `BatchSummary`, `CellResult`, `EvalSuite`,
|
|
117
119
|
`EvalCaseResult`, `WorkflowContext`, …) are exported alongside them.
|
package/dist/BatchRunner.d.ts
CHANGED
|
@@ -14,5 +14,25 @@ import { type BatchRunnerOptions, type BatchSummary, type CellResult, type Matri
|
|
|
14
14
|
* up with the matrix.
|
|
15
15
|
*/
|
|
16
16
|
export declare function runBatchMatrix(cells: MatrixCell[], options: BatchRunnerOptions): Promise<CellResult[]>;
|
|
17
|
+
/**
|
|
18
|
+
* BATCH-24 — build the end-of-run nudge that makes the serial default discoverable (DL-1: the
|
|
19
|
+
* user should never have to guess why a big matrix took its time). Returns `undefined` when there
|
|
20
|
+
* is nothing worth saying: the user already chose a `-j`, or only one unit ran (nothing to
|
|
21
|
+
* parallelize).
|
|
22
|
+
*
|
|
23
|
+
* Deliberately names **no number**. The right `-j` depends entirely on the backend — a local
|
|
24
|
+
* single-GPU ollama wants exactly this serial default, a cloud key with headroom can take many —
|
|
25
|
+
* so printing "try -j 4" would hand the wrong advice to the very setup this default protects.
|
|
26
|
+
*
|
|
27
|
+
* Kept out of the package root's export list because it is a detail two first-party commands
|
|
28
|
+
* share rather than something the plugin API advertises. Note that is discoverability only, not
|
|
29
|
+
* a boundary: the package publishes a `./*.js` subpath map, so `@gaunt-sloth/batch/BatchRunner.js`
|
|
30
|
+
* — the specifier both commands import it by — is reachable by any consumer.
|
|
31
|
+
*
|
|
32
|
+
* @param unitCount - how many cells/cases the run actually processed
|
|
33
|
+
* @param explicitConcurrency - the user's `-j/--concurrency` value, `undefined` when not supplied
|
|
34
|
+
* @param noun - what the units are called on this surface (`batch` cells vs `eval` cases)
|
|
35
|
+
*/
|
|
36
|
+
export declare function concurrencyHint(unitCount: number, explicitConcurrency: number | undefined, noun?: 'Cells' | 'Cases'): string | undefined;
|
|
17
37
|
/** Build the lightweight aggregate "flake report" (pass/fail counts) from the per-cell results. */
|
|
18
38
|
export declare function buildBatchSummary(results: CellResult[]): BatchSummary;
|
package/dist/BatchRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_CELL_CONCURRENCY, } from '#src/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Run every cell of the matrix through the injected {@link RunCellFn}, capping in-flight work at
|
|
4
4
|
* `concurrency` and retrying a failed cell up to `retry` times.
|
|
@@ -70,10 +70,36 @@ async function runCellWithRetry(cell, runCell, retry) {
|
|
|
70
70
|
}
|
|
71
71
|
function normalizeConcurrency(concurrency) {
|
|
72
72
|
if (concurrency === undefined || !Number.isFinite(concurrency) || concurrency < 1) {
|
|
73
|
-
return
|
|
73
|
+
return DEFAULT_CELL_CONCURRENCY;
|
|
74
74
|
}
|
|
75
75
|
return Math.floor(concurrency);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* BATCH-24 — build the end-of-run nudge that makes the serial default discoverable (DL-1: the
|
|
79
|
+
* user should never have to guess why a big matrix took its time). Returns `undefined` when there
|
|
80
|
+
* is nothing worth saying: the user already chose a `-j`, or only one unit ran (nothing to
|
|
81
|
+
* parallelize).
|
|
82
|
+
*
|
|
83
|
+
* Deliberately names **no number**. The right `-j` depends entirely on the backend — a local
|
|
84
|
+
* single-GPU ollama wants exactly this serial default, a cloud key with headroom can take many —
|
|
85
|
+
* so printing "try -j 4" would hand the wrong advice to the very setup this default protects.
|
|
86
|
+
*
|
|
87
|
+
* Kept out of the package root's export list because it is a detail two first-party commands
|
|
88
|
+
* share rather than something the plugin API advertises. Note that is discoverability only, not
|
|
89
|
+
* a boundary: the package publishes a `./*.js` subpath map, so `@gaunt-sloth/batch/BatchRunner.js`
|
|
90
|
+
* — the specifier both commands import it by — is reachable by any consumer.
|
|
91
|
+
*
|
|
92
|
+
* @param unitCount - how many cells/cases the run actually processed
|
|
93
|
+
* @param explicitConcurrency - the user's `-j/--concurrency` value, `undefined` when not supplied
|
|
94
|
+
* @param noun - what the units are called on this surface (`batch` cells vs `eval` cases)
|
|
95
|
+
*/
|
|
96
|
+
export function concurrencyHint(unitCount, explicitConcurrency, noun = 'Cells') {
|
|
97
|
+
if (explicitConcurrency !== undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (unitCount <= 1)
|
|
100
|
+
return undefined;
|
|
101
|
+
return `${noun} ran one at a time. Pass -j <n> to run them in parallel.`;
|
|
102
|
+
}
|
|
77
103
|
function normalizeRetry(retry) {
|
|
78
104
|
if (retry === undefined || !Number.isFinite(retry) || retry < 0) {
|
|
79
105
|
return 0;
|
package/dist/BatchRunner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BatchRunner.js","sourceRoot":"","sources":["../src/BatchRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"BatchRunner.js","sourceRoot":"","sources":["../src/BatchRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { EvalSuite } from '#src/evalTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* BATCH-25 — the blind export and its round trip.
|
|
4
|
+
*
|
|
5
|
+
* A corpus labelled by one person is a corpus with one person's blind spots in it. The approvals
|
|
6
|
+
* corpus plan requires a **blind relabel by a second person** before any number from it gates a
|
|
7
|
+
* decision, and this module is what makes that structurally possible rather than aspirational: the
|
|
8
|
+
* export carries the case id, the input, and the family tags, and NOTHING else — no expected label,
|
|
9
|
+
* no expected action, no rationale, no judge rubric, no per-identity expectation blocks.
|
|
10
|
+
*
|
|
11
|
+
* The diff then compares the second labeller's file against the corpus BY ID, and — this is the
|
|
12
|
+
* part that is easy to get wrong — reports ids present in only one of the two. A relabel that
|
|
13
|
+
* silently omitted ten cases would otherwise read as 100% agreement on the sixty-eight it did
|
|
14
|
+
* cover, which is the same "a number that looks better than it earned" failure the metric layer
|
|
15
|
+
* exists to prevent.
|
|
16
|
+
*/
|
|
17
|
+
/** One exported case. `inputs` carries every round in order, so a multi-round negotiation case is
|
|
18
|
+
* relabellable rather than silently dropped — dropping it would be a silent cap on the export. */
|
|
19
|
+
export interface BlindExportCase {
|
|
20
|
+
id: string;
|
|
21
|
+
inputs: string[];
|
|
22
|
+
tags: string[];
|
|
23
|
+
}
|
|
24
|
+
/** The blind export document. `note` is written into the file itself so a labeller opening it
|
|
25
|
+
* out of context knows what is being asked and that the labels were deliberately withheld. */
|
|
26
|
+
export interface BlindExport {
|
|
27
|
+
note: string;
|
|
28
|
+
/** The suite's declared label enum — the labeller needs the vocabulary; that is not a leak of the
|
|
29
|
+
* answers. Empty when the suite declares no `classification:` block. */
|
|
30
|
+
labels: string[];
|
|
31
|
+
/** The suite's declared action enum, when it has one. */
|
|
32
|
+
actions: string[];
|
|
33
|
+
cases: BlindExportCase[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the blind export for a suite.
|
|
37
|
+
*
|
|
38
|
+
* Note what is NOT here: `expectLabel`, `expectAction`, `judgeRubric`, every content assertion, and
|
|
39
|
+
* the pass threshold. An expectation block leaks the answer as surely as the label does — a
|
|
40
|
+
* `must_contain: ["refused"]` tells the labeller what was expected — so the export is built from an
|
|
41
|
+
* allow-list of three fields rather than by deleting fields from the case.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildBlindExport(suite: EvalSuite): BlindExport;
|
|
44
|
+
/** One case as returned by the second labeller. */
|
|
45
|
+
export interface RelabelEntry {
|
|
46
|
+
id: string;
|
|
47
|
+
label?: string;
|
|
48
|
+
action?: string;
|
|
49
|
+
note?: string;
|
|
50
|
+
}
|
|
51
|
+
/** One case where the two labellings differ. */
|
|
52
|
+
export interface RelabelDisagreement {
|
|
53
|
+
id: string;
|
|
54
|
+
tags: string[];
|
|
55
|
+
corpusLabel?: string;
|
|
56
|
+
relabelLabel?: string;
|
|
57
|
+
corpusAction?: string;
|
|
58
|
+
relabelAction?: string;
|
|
59
|
+
note?: string;
|
|
60
|
+
}
|
|
61
|
+
/** The result of a blind relabel diff. */
|
|
62
|
+
export interface RelabelDiff {
|
|
63
|
+
/** Cases present in BOTH files — the only ones agreement can be computed over. */
|
|
64
|
+
compared: number;
|
|
65
|
+
agreed: number;
|
|
66
|
+
disagreements: RelabelDisagreement[];
|
|
67
|
+
/** Ids in the corpus that the relabel file never mentions. Reported explicitly: without this a
|
|
68
|
+
* partial relabel reads as full agreement on the part it covered. */
|
|
69
|
+
missingFromRelabel: string[];
|
|
70
|
+
/** Ids in the relabel file that the corpus does not contain (a typo, or a stale export). */
|
|
71
|
+
unknownInRelabel: string[];
|
|
72
|
+
/** Everything that bounds what the agreement figure covers. Empty is the good case. */
|
|
73
|
+
warnings: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Diff a second labeller's file against the corpus, by id.
|
|
77
|
+
*
|
|
78
|
+
* Agreement is computed ONLY over ids present in both files, and the ids present in only one are
|
|
79
|
+
* reported separately and warned about. A relabel that covers half the corpus produces an
|
|
80
|
+
* agreement figure over half the corpus and says so — it never produces a figure that looks like it
|
|
81
|
+
* covers all of it.
|
|
82
|
+
*
|
|
83
|
+
* A relabel entry with no `label` is treated as "not relabelled": it counts as missing rather than
|
|
84
|
+
* as a disagreement with `undefined`, because an unanswered case is not a dissent.
|
|
85
|
+
*/
|
|
86
|
+
export declare function diffRelabel(suite: EvalSuite, entries: RelabelEntry[]): RelabelDiff;
|
|
87
|
+
/** Render a {@link RelabelDiff} as plain lines for the console. */
|
|
88
|
+
export declare function renderRelabelDiff(diff: RelabelDiff): string[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const BLIND_NOTE = 'Blind relabel export. Each case carries only its id, its input(s) and its family tags — the ' +
|
|
2
|
+
'authored expected label/action and every rationale are deliberately withheld. Add a `label` ' +
|
|
3
|
+
'(and, where you have a view, an `action`) to each case and return the file; `gth eval ' +
|
|
4
|
+
'--relabel-diff` compares it to the corpus by id.';
|
|
5
|
+
/**
|
|
6
|
+
* Build the blind export for a suite.
|
|
7
|
+
*
|
|
8
|
+
* Note what is NOT here: `expectLabel`, `expectAction`, `judgeRubric`, every content assertion, and
|
|
9
|
+
* the pass threshold. An expectation block leaks the answer as surely as the label does — a
|
|
10
|
+
* `must_contain: ["refused"]` tells the labeller what was expected — so the export is built from an
|
|
11
|
+
* allow-list of three fields rather than by deleting fields from the case.
|
|
12
|
+
*/
|
|
13
|
+
export function buildBlindExport(suite) {
|
|
14
|
+
return {
|
|
15
|
+
note: BLIND_NOTE,
|
|
16
|
+
labels: suite.classification?.labels ?? [],
|
|
17
|
+
actions: suite.classification?.actions ?? [],
|
|
18
|
+
cases: suite.cases.map((evalCase) => ({
|
|
19
|
+
id: evalCase.id,
|
|
20
|
+
inputs: evalCase.turns.map((turn) => turn.user),
|
|
21
|
+
tags: evalCase.tags ?? [],
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Diff a second labeller's file against the corpus, by id.
|
|
27
|
+
*
|
|
28
|
+
* Agreement is computed ONLY over ids present in both files, and the ids present in only one are
|
|
29
|
+
* reported separately and warned about. A relabel that covers half the corpus produces an
|
|
30
|
+
* agreement figure over half the corpus and says so — it never produces a figure that looks like it
|
|
31
|
+
* covers all of it.
|
|
32
|
+
*
|
|
33
|
+
* A relabel entry with no `label` is treated as "not relabelled": it counts as missing rather than
|
|
34
|
+
* as a disagreement with `undefined`, because an unanswered case is not a dissent.
|
|
35
|
+
*/
|
|
36
|
+
export function diffRelabel(suite, entries) {
|
|
37
|
+
// The corpus's own view of each case: the first expected label/action any expectation declares.
|
|
38
|
+
const corpus = new Map();
|
|
39
|
+
for (const evalCase of suite.cases) {
|
|
40
|
+
const blocks = evalCase.turns.flatMap((turn) => turn.expectations);
|
|
41
|
+
corpus.set(evalCase.id, {
|
|
42
|
+
tags: evalCase.tags ?? [],
|
|
43
|
+
label: blocks.find((block) => block.expectLabel !== undefined)?.expectLabel,
|
|
44
|
+
action: blocks.find((block) => block.expectAction !== undefined)?.expectAction,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const relabelled = new Map();
|
|
48
|
+
const unknownInRelabel = [];
|
|
49
|
+
const duplicateIds = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!corpus.has(entry.id)) {
|
|
52
|
+
unknownInRelabel.push(entry.id);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (relabelled.has(entry.id))
|
|
56
|
+
duplicateIds.push(entry.id);
|
|
57
|
+
// A blank label means "not relabelled" — it belongs in `missingFromRelabel`, not in the
|
|
58
|
+
// agreement denominator.
|
|
59
|
+
if (entry.label === undefined || entry.label.trim().length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
relabelled.set(entry.id, entry);
|
|
62
|
+
}
|
|
63
|
+
const disagreements = [];
|
|
64
|
+
const missingFromRelabel = [];
|
|
65
|
+
let compared = 0;
|
|
66
|
+
let agreed = 0;
|
|
67
|
+
for (const [id, corpusCase] of corpus) {
|
|
68
|
+
const entry = relabelled.get(id);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
missingFromRelabel.push(id);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
compared += 1;
|
|
74
|
+
const labelAgrees = corpusCase.label === entry.label;
|
|
75
|
+
// An action is only compared when the relabeller expressed one — silence is not dissent.
|
|
76
|
+
const actionAgrees = entry.action === undefined || entry.action.trim().length === 0
|
|
77
|
+
? true
|
|
78
|
+
: corpusCase.action === entry.action;
|
|
79
|
+
if (labelAgrees && actionAgrees) {
|
|
80
|
+
agreed += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
disagreements.push({
|
|
84
|
+
id,
|
|
85
|
+
tags: corpusCase.tags,
|
|
86
|
+
corpusLabel: corpusCase.label,
|
|
87
|
+
relabelLabel: entry.label,
|
|
88
|
+
corpusAction: corpusCase.action,
|
|
89
|
+
relabelAction: entry.action,
|
|
90
|
+
note: entry.note,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const warnings = [];
|
|
94
|
+
if (missingFromRelabel.length > 0) {
|
|
95
|
+
warnings.push(`${missingFromRelabel.length}/${corpus.size} corpus case(s) were NOT relabelled, so the ` +
|
|
96
|
+
`agreement figure covers ${compared}/${corpus.size} cases, not the whole corpus ` +
|
|
97
|
+
`(missing: ${missingFromRelabel.join(', ')}).`);
|
|
98
|
+
}
|
|
99
|
+
if (unknownInRelabel.length > 0) {
|
|
100
|
+
warnings.push(`${unknownInRelabel.length} relabel entr(ies) name ids the corpus does not contain — a typo, ` +
|
|
101
|
+
`or a stale export (${unknownInRelabel.join(', ')}).`);
|
|
102
|
+
}
|
|
103
|
+
if (duplicateIds.length > 0) {
|
|
104
|
+
warnings.push(`${duplicateIds.length} id(s) appear more than once in the relabel file; the LAST entry won ` +
|
|
105
|
+
`(${[...new Set(duplicateIds)].join(', ')}).`);
|
|
106
|
+
}
|
|
107
|
+
return { compared, agreed, disagreements, missingFromRelabel, unknownInRelabel, warnings };
|
|
108
|
+
}
|
|
109
|
+
/** Render a {@link RelabelDiff} as plain lines for the console. */
|
|
110
|
+
export function renderRelabelDiff(diff) {
|
|
111
|
+
const lines = ['', 'BLIND RELABEL DIFF'];
|
|
112
|
+
const percent = diff.compared === 0 ? 'n/a (0 cases)' : `${((diff.agreed / diff.compared) * 100).toFixed(1)}%`;
|
|
113
|
+
lines.push(` agreement: ${diff.agreed}/${diff.compared} compared case(s) (${percent})`);
|
|
114
|
+
for (const warning of diff.warnings)
|
|
115
|
+
lines.push(` ! ${warning}`);
|
|
116
|
+
if (diff.disagreements.length > 0) {
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(` disagreements (${diff.disagreements.length}):`);
|
|
119
|
+
for (const item of diff.disagreements) {
|
|
120
|
+
const tags = item.tags.length > 0 ? ` [${item.tags.join(', ')}]` : '';
|
|
121
|
+
lines.push(` ${item.id}${tags}: corpus=${item.corpusLabel ?? '(none)'}` +
|
|
122
|
+
`/${item.corpusAction ?? '(none)'} vs relabel=${item.relabelLabel ?? '(none)'}` +
|
|
123
|
+
`/${item.relabelAction ?? '(none)'}` +
|
|
124
|
+
(item.note ? ` — ${item.note}` : ''));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=blindExport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blindExport.js","sourceRoot":"","sources":["../src/blindExport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClassificationExtractor, ClassifiedCell, EvalClassificationSpec, EvalConfusionMatrix } from '#src/classificationTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* BATCH-25 — read a classification value out of one cell's answer, and build confusion matrices
|
|
4
|
+
* from the graded results.
|
|
5
|
+
*
|
|
6
|
+
* Both halves obey the same rule: **nothing is ever dropped.** An answer that matches no declared
|
|
7
|
+
* value becomes {@link UNRECOGNIZED_LABEL}; a cell that never ran is counted as `excluded` and said
|
|
8
|
+
* out loud. A classifier report that quietly discards the rows it could not interpret reports a
|
|
9
|
+
* cleaner number than it earned, which is the failure mode this whole cluster exists to prevent.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Extract one declared enum value from an answer.
|
|
13
|
+
*
|
|
14
|
+
* TOTAL by construction — the return is always either a member of `allowed` or
|
|
15
|
+
* {@link UNRECOGNIZED_LABEL}. There is deliberately no fuzzy/substring fallback: matching `safe`
|
|
16
|
+
* inside "this is not safe" is exactly the silent misreading a classifier eval must not do.
|
|
17
|
+
*
|
|
18
|
+
* Matching is case-insensitive and whitespace-trimmed (a model that answers `"Safe\n"` meant
|
|
19
|
+
* `safe`), and surrounding quotes/backticks/full stops are stripped, because those are formatting,
|
|
20
|
+
* not content. Anything beyond that is the author's job via `json_path`.
|
|
21
|
+
*
|
|
22
|
+
* @param answer The cell's raw answer text (or the resolved JSON-path value, stringified).
|
|
23
|
+
* @param extractor How to read it.
|
|
24
|
+
* @param allowed The suite's declared enum for this dimension.
|
|
25
|
+
* @returns The matched enum value, or {@link UNRECOGNIZED_LABEL}.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractClassificationValue(answer: string | undefined, extractor: ClassificationExtractor, allowed: string[]): string;
|
|
28
|
+
/** The text an extractor actually reads — kept separate from matching so the per-cell record can
|
|
29
|
+
* carry it for diagnosis (`(unrecognized)` with no trace of what was said is not diagnosable).
|
|
30
|
+
* `undefined` = the extractor found nothing at all (no answer, unparseable JSON, unresolved path). */
|
|
31
|
+
export declare function readRaw(answer: string | undefined, extractor: ClassificationExtractor): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Build one confusion matrix (rows = expected, columns = actual) over the given cells.
|
|
34
|
+
*
|
|
35
|
+
* Axis construction is deliberate: rows are the declared enum in DECLARED order (so two runs of the
|
|
36
|
+
* same suite are diffable line by line) plus {@link NO_EXPECTATION} only when some cell declares no
|
|
37
|
+
* expectation; columns are the declared enum plus {@link UNRECOGNIZED_LABEL} and/or
|
|
38
|
+
* {@link NO_EXPECTATION} only when some cell actually produced one. Extra buckets never appear
|
|
39
|
+
* speculatively, and never fail to appear when they are populated.
|
|
40
|
+
*
|
|
41
|
+
* `counted + excluded === cells.length`, always — asserted in the unit suite, because a matrix that
|
|
42
|
+
* silently drops the cells it could not place reads as "covered everything" when it didn't.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildConfusionMatrix(cells: ClassifiedCell[], dimension: 'label' | 'action', declared: string[]): EvalConfusionMatrix;
|
|
45
|
+
/** Every tag any cell declares, sorted — the per-tag axis for matrices and metrics. */
|
|
46
|
+
export declare function collectTags(cells: ClassifiedCell[]): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Which enum a `classification` spec declares for a dimension. `actions` is `[]` when the suite has
|
|
49
|
+
* no action dimension, which is what makes an `expect_action` assertion a parse error rather than a
|
|
50
|
+
* silently ungradeable one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function declaredValues(spec: EvalClassificationSpec, dimension: 'label' | 'action'): string[];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolveJsonPath } from '#src/deterministicChecks.js';
|
|
2
|
+
import { NO_EXPECTATION, UNRECOGNIZED_LABEL } from '#src/classificationTypes.js';
|
|
3
|
+
/**
|
|
4
|
+
* BATCH-25 — read a classification value out of one cell's answer, and build confusion matrices
|
|
5
|
+
* from the graded results.
|
|
6
|
+
*
|
|
7
|
+
* Both halves obey the same rule: **nothing is ever dropped.** An answer that matches no declared
|
|
8
|
+
* value becomes {@link UNRECOGNIZED_LABEL}; a cell that never ran is counted as `excluded` and said
|
|
9
|
+
* out loud. A classifier report that quietly discards the rows it could not interpret reports a
|
|
10
|
+
* cleaner number than it earned, which is the failure mode this whole cluster exists to prevent.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Extract one declared enum value from an answer.
|
|
14
|
+
*
|
|
15
|
+
* TOTAL by construction — the return is always either a member of `allowed` or
|
|
16
|
+
* {@link UNRECOGNIZED_LABEL}. There is deliberately no fuzzy/substring fallback: matching `safe`
|
|
17
|
+
* inside "this is not safe" is exactly the silent misreading a classifier eval must not do.
|
|
18
|
+
*
|
|
19
|
+
* Matching is case-insensitive and whitespace-trimmed (a model that answers `"Safe\n"` meant
|
|
20
|
+
* `safe`), and surrounding quotes/backticks/full stops are stripped, because those are formatting,
|
|
21
|
+
* not content. Anything beyond that is the author's job via `json_path`.
|
|
22
|
+
*
|
|
23
|
+
* @param answer The cell's raw answer text (or the resolved JSON-path value, stringified).
|
|
24
|
+
* @param extractor How to read it.
|
|
25
|
+
* @param allowed The suite's declared enum for this dimension.
|
|
26
|
+
* @returns The matched enum value, or {@link UNRECOGNIZED_LABEL}.
|
|
27
|
+
*/
|
|
28
|
+
export function extractClassificationValue(answer, extractor, allowed) {
|
|
29
|
+
const raw = readRaw(answer, extractor);
|
|
30
|
+
if (raw === undefined)
|
|
31
|
+
return UNRECOGNIZED_LABEL;
|
|
32
|
+
const normalized = normalizeValue(raw);
|
|
33
|
+
if (normalized.length === 0)
|
|
34
|
+
return UNRECOGNIZED_LABEL;
|
|
35
|
+
const hit = allowed.find((candidate) => candidate.toLowerCase() === normalized);
|
|
36
|
+
return hit ?? UNRECOGNIZED_LABEL;
|
|
37
|
+
}
|
|
38
|
+
/** The text an extractor actually reads — kept separate from matching so the per-cell record can
|
|
39
|
+
* carry it for diagnosis (`(unrecognized)` with no trace of what was said is not diagnosable).
|
|
40
|
+
* `undefined` = the extractor found nothing at all (no answer, unparseable JSON, unresolved path). */
|
|
41
|
+
export function readRaw(answer, extractor) {
|
|
42
|
+
if (answer === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (extractor.kind === 'answer')
|
|
45
|
+
return answer;
|
|
46
|
+
// json_path: parse the answer as JSON and resolve the path with the SAME minimal resolver the
|
|
47
|
+
// `json_path` assertion uses, so an author who knows one knows the other. A non-JSON answer or an
|
|
48
|
+
// unresolved path yields `undefined` → `(unrecognized)`, never a throw mid-run.
|
|
49
|
+
let root;
|
|
50
|
+
try {
|
|
51
|
+
root = JSON.parse(answer);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const { found, value } = resolveJsonPath(root, extractor.path);
|
|
57
|
+
if (!found || value === undefined || value === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
/** Trim, strip wrapping quotes/backticks and a trailing full stop, lowercase. Formatting only —
|
|
62
|
+
* never content. */
|
|
63
|
+
function normalizeValue(raw) {
|
|
64
|
+
let text = raw.trim();
|
|
65
|
+
// Strip one layer of wrapping quotes/backticks, then a single trailing full stop.
|
|
66
|
+
const quotes = ['"', "'", '`'];
|
|
67
|
+
while (text.length >= 2 && quotes.includes(text[0]) && text[text.length - 1] === text[0]) {
|
|
68
|
+
text = text.slice(1, -1).trim();
|
|
69
|
+
}
|
|
70
|
+
if (text.endsWith('.'))
|
|
71
|
+
text = text.slice(0, -1).trim();
|
|
72
|
+
return text.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build one confusion matrix (rows = expected, columns = actual) over the given cells.
|
|
76
|
+
*
|
|
77
|
+
* Axis construction is deliberate: rows are the declared enum in DECLARED order (so two runs of the
|
|
78
|
+
* same suite are diffable line by line) plus {@link NO_EXPECTATION} only when some cell declares no
|
|
79
|
+
* expectation; columns are the declared enum plus {@link UNRECOGNIZED_LABEL} and/or
|
|
80
|
+
* {@link NO_EXPECTATION} only when some cell actually produced one. Extra buckets never appear
|
|
81
|
+
* speculatively, and never fail to appear when they are populated.
|
|
82
|
+
*
|
|
83
|
+
* `counted + excluded === cells.length`, always — asserted in the unit suite, because a matrix that
|
|
84
|
+
* silently drops the cells it could not place reads as "covered everything" when it didn't.
|
|
85
|
+
*/
|
|
86
|
+
export function buildConfusionMatrix(cells, dimension, declared) {
|
|
87
|
+
const expectedOf = (cell) => dimension === 'label' ? cell.expectedLabel : cell.expectedAction;
|
|
88
|
+
const actualOf = (cell) => dimension === 'label' ? cell.actualLabel : cell.actualAction;
|
|
89
|
+
const scored = cells.filter((cell) => cell.scored);
|
|
90
|
+
const excluded = cells.length - scored.length;
|
|
91
|
+
const rowSet = new Set(declared);
|
|
92
|
+
const columnSet = new Set(declared);
|
|
93
|
+
for (const cell of scored) {
|
|
94
|
+
rowSet.add(expectedOf(cell) ?? NO_EXPECTATION);
|
|
95
|
+
columnSet.add(actualOf(cell) ?? NO_EXPECTATION);
|
|
96
|
+
}
|
|
97
|
+
// Preserve DECLARED order first, then any extra bucket in a stable order, so the rendered matrix
|
|
98
|
+
// is diffable across runs.
|
|
99
|
+
const order = (present) => [
|
|
100
|
+
...declared.filter((value) => present.has(value)),
|
|
101
|
+
...[UNRECOGNIZED_LABEL, NO_EXPECTATION].filter((value) => present.has(value)),
|
|
102
|
+
];
|
|
103
|
+
// Declared values always keep their row/column even at zero — an enum value that never appears is
|
|
104
|
+
// itself a finding (the QA-5 "the caution tier was never emitted" result), so it must be visible.
|
|
105
|
+
for (const value of declared) {
|
|
106
|
+
rowSet.add(value);
|
|
107
|
+
columnSet.add(value);
|
|
108
|
+
}
|
|
109
|
+
const rows = order(rowSet);
|
|
110
|
+
const columns = order(columnSet);
|
|
111
|
+
const counts = {};
|
|
112
|
+
for (const row of rows) {
|
|
113
|
+
counts[row] = {};
|
|
114
|
+
for (const column of columns)
|
|
115
|
+
counts[row][column] = 0;
|
|
116
|
+
}
|
|
117
|
+
for (const cell of scored) {
|
|
118
|
+
const row = expectedOf(cell) ?? NO_EXPECTATION;
|
|
119
|
+
const column = actualOf(cell) ?? NO_EXPECTATION;
|
|
120
|
+
counts[row][column] += 1;
|
|
121
|
+
}
|
|
122
|
+
return { dimension, rows, columns, counts, counted: scored.length, excluded };
|
|
123
|
+
}
|
|
124
|
+
/** Every tag any cell declares, sorted — the per-tag axis for matrices and metrics. */
|
|
125
|
+
export function collectTags(cells) {
|
|
126
|
+
const tags = new Set();
|
|
127
|
+
for (const cell of cells)
|
|
128
|
+
for (const tag of cell.tags)
|
|
129
|
+
tags.add(tag);
|
|
130
|
+
return [...tags].sort();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Which enum a `classification` spec declares for a dimension. `actions` is `[]` when the suite has
|
|
134
|
+
* no action dimension, which is what makes an `expect_action` assertion a parse error rather than a
|
|
135
|
+
* silently ungradeable one.
|
|
136
|
+
*/
|
|
137
|
+
export function declaredValues(spec, dimension) {
|
|
138
|
+
return dimension === 'label' ? spec.labels : spec.actions;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=classification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification.js","sourceRoot":"","sources":["../src/classification.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EvalClassificationReport, EvalConfusionMatrix, EvalMetricResult } from '#src/classificationTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* BATCH-25 — render a {@link EvalClassificationReport} as plain lines.
|
|
4
|
+
*
|
|
5
|
+
* A pure string builder, deliberately separate from the reporter that prints it: the console
|
|
6
|
+
* rendering is then unit-testable without mocking `consoleUtils`, and a future reporter (JUnit,
|
|
7
|
+
* TeamCity, a third-party one) can emit the same block without reimplementing the layout.
|
|
8
|
+
*
|
|
9
|
+
* Every function here is total over the report — there is no "…and 12 more" anywhere. A renderer
|
|
10
|
+
* that truncates is a silent cap, and a truncated matrix reads as a complete one.
|
|
11
|
+
*/
|
|
12
|
+
/** Rendering options. */
|
|
13
|
+
export interface RenderClassificationOptions {
|
|
14
|
+
/** Suppress the per-tag confusion matrices (the per-tag METRIC rows always stay). Set for a
|
|
15
|
+
* sweep cell, where N full blocks would bury the comparison table that is the point of sweeping.
|
|
16
|
+
* The suppressed matrices are still written to that cell's `results.json`. */
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Render the whole report: coverage, matrices, metrics, warnings, gate verdict. */
|
|
20
|
+
export declare function renderClassificationReport(report: EvalClassificationReport, options?: RenderClassificationOptions): string[];
|
|
21
|
+
/** Render one confusion matrix as a fixed-width grid: rows = expected, columns = actual. */
|
|
22
|
+
export declare function renderConfusionMatrix(matrix: EvalConfusionMatrix, title: string): string[];
|
|
23
|
+
/** Render one metric: headline value, gate verdict, per-tag sub-scores, then its warnings. */
|
|
24
|
+
export declare function renderMetric(metric: EvalMetricResult, tags: string[]): string[];
|