@gaunt-sloth/batch 2.0.0-alpha.23 → 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.
Files changed (52) hide show
  1. package/README.md +10 -8
  2. package/dist/BatchRunner.d.ts +20 -0
  3. package/dist/BatchRunner.js +28 -2
  4. package/dist/BatchRunner.js.map +1 -1
  5. package/dist/blindExport.d.ts +88 -0
  6. package/dist/blindExport.js +129 -0
  7. package/dist/blindExport.js.map +1 -0
  8. package/dist/classification.d.ts +52 -0
  9. package/dist/classification.js +140 -0
  10. package/dist/classification.js.map +1 -0
  11. package/dist/classificationRender.d.ts +24 -0
  12. package/dist/classificationRender.js +96 -0
  13. package/dist/classificationRender.js.map +1 -0
  14. package/dist/classificationReport.d.ts +11 -0
  15. package/dist/classificationReport.js +60 -0
  16. package/dist/classificationReport.js.map +1 -0
  17. package/dist/classificationTypes.d.ts +311 -0
  18. package/dist/classificationTypes.js +40 -0
  19. package/dist/classificationTypes.js.map +1 -0
  20. package/dist/evalCompare.d.ts +108 -0
  21. package/dist/evalCompare.js +246 -0
  22. package/dist/evalCompare.js.map +1 -0
  23. package/dist/evalRunner.d.ts +34 -3
  24. package/dist/evalRunner.js +259 -9
  25. package/dist/evalRunner.js.map +1 -1
  26. package/dist/evalSuite.d.ts +12 -2
  27. package/dist/evalSuite.js +534 -8
  28. package/dist/evalSuite.js.map +1 -1
  29. package/dist/evalTypes.d.ts +358 -5
  30. package/dist/evalTypes.js +98 -0
  31. package/dist/evalTypes.js.map +1 -1
  32. package/dist/index.d.ts +15 -1
  33. package/dist/index.js +14 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/metrics.d.ts +50 -0
  36. package/dist/metrics.js +433 -0
  37. package/dist/metrics.js.map +1 -0
  38. package/dist/pipelineCli.js +1 -1
  39. package/dist/pipelineCli.js.map +1 -1
  40. package/dist/raterTarget.d.ts +94 -0
  41. package/dist/raterTarget.js +328 -0
  42. package/dist/raterTarget.js.map +1 -0
  43. package/dist/reporters/reporterTypes.d.ts +9 -0
  44. package/dist/reporters/textReporter.js +21 -0
  45. package/dist/reporters/textReporter.js.map +1 -1
  46. package/dist/types.d.ts +14 -2
  47. package/dist/types.js +14 -2
  48. package/dist/types.js.map +1 -1
  49. package/dist/workflow/runWorkflow.d.ts +1 -1
  50. package/dist/workflow/runWorkflow.js +2 -2
  51. package/dist/workflow/runWorkflow.js.map +1 -1
  52. 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. Add `-j <n>` to cap
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, `--retry <n>` retries
63
- a failed cell (default `0`), `-o <dir>` sets the output dir.
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`, `DEFAULT_CONCURRENCY`.
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.
@@ -130,8 +132,8 @@ The corresponding TypeScript types (`MatrixCell`, `BatchSummary`, `CellResult`,
130
132
  - [`@gaunt-sloth/agent`](https://www.npmjs.com/package/@gaunt-sloth/agent) — Agent runtime: built-in
131
133
  tools, filesystem toolkit, middleware registry, API server, AG-UI, MCP, and A2A integration
132
134
  ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/agent))
133
- - [`@gaunt-sloth/review`](https://www.npmjs.com/package/@gaunt-sloth/review) — Review and Q&A modules
134
- with standalone CLI
135
+ - [`@gaunt-sloth/review`](https://www.npmjs.com/package/@gaunt-sloth/review) — Review engine with
136
+ content/requirement sources (GitHub, Jira, file, text) and standalone CLI
135
137
  ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/review))
136
138
  - [`@gaunt-sloth/batch`](https://www.npmjs.com/package/@gaunt-sloth/batch) — Batch / eval / workflow
137
139
  runtime (this package)
@@ -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;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CONCURRENCY, } from '#src/types.js';
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 DEFAULT_CONCURRENCY;
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;
@@ -1 +1 @@
1
- {"version":3,"file":"BatchRunner.js","sourceRoot":"","sources":["../src/BatchRunner.ts"],"names":[],"mappings":"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"}
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[];