@gaunt-sloth/batch 2.0.0-alpha.24 → 2.0.0-alpha.26

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 +8 -6
  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
@@ -0,0 +1,96 @@
1
+ import { formatTally } from '#src/metrics.js';
2
+ /** Render the whole report: coverage, matrices, metrics, warnings, gate verdict. */
3
+ export function renderClassificationReport(report, options = {}) {
4
+ const lines = [];
5
+ lines.push('');
6
+ lines.push('CLASSIFICATION');
7
+ // Coverage FIRST, before any number that depends on it. "no silent caps" is not a footnote.
8
+ lines.push(` coverage: ${report.coverage.scored}/${report.coverage.total} cell(s) classified` +
9
+ (report.coverage.excluded > 0 ? `, ${report.coverage.excluded} excluded` : ''));
10
+ for (const warning of report.warnings)
11
+ lines.push(` ! ${warning}`);
12
+ lines.push('');
13
+ lines.push(...renderConfusionMatrix(report.labelMatrix, 'label'));
14
+ if (report.actionMatrix) {
15
+ lines.push('');
16
+ lines.push(...renderConfusionMatrix(report.actionMatrix, 'action'));
17
+ }
18
+ // Per-tag matrices only when there is more than one family — with a single tag the per-tag matrix
19
+ // is the overall one, and printing it twice is noise, not information — and never in compact mode.
20
+ if (report.tags.length > 1 && !options.compact) {
21
+ for (const tag of report.tags) {
22
+ const matrix = report.labelMatrixByTag[tag];
23
+ if (!matrix)
24
+ continue;
25
+ lines.push('');
26
+ lines.push(...renderConfusionMatrix(matrix, `label · tag "${tag}"`));
27
+ }
28
+ }
29
+ if (options.compact && report.tags.length > 1) {
30
+ // Say what was withheld. A renderer that quietly drops a section is a silent cap, which is the
31
+ // one thing this facility may not do — even about its own output.
32
+ lines.push('');
33
+ lines.push(` (per-tag confusion matrices for ${report.tags.length} tag(s) omitted in a sweep cell — ` +
34
+ "they are in this cell's results.json; per-tag metric rows are below and in the " +
35
+ 'comparison table.)');
36
+ }
37
+ if (report.metrics.length > 0) {
38
+ lines.push('');
39
+ lines.push('METRICS');
40
+ for (const metric of report.metrics)
41
+ lines.push(...renderMetric(metric, report.tags));
42
+ }
43
+ if (report.gateFailures.length > 0) {
44
+ lines.push('');
45
+ lines.push(`METRIC GATE FAILED: ${report.gateFailures.join(', ')}`);
46
+ }
47
+ return lines;
48
+ }
49
+ /** Render one confusion matrix as a fixed-width grid: rows = expected, columns = actual. */
50
+ export function renderConfusionMatrix(matrix, title) {
51
+ const lines = [];
52
+ lines.push(` confusion (${title}) — rows = expected, cols = actual`);
53
+ const rowLabelWidth = Math.max(...matrix.rows.map((row) => row.length), 'expected'.length);
54
+ const columnWidths = matrix.columns.map((column) => Math.max(column.length, ...matrix.rows.map((row) => String(matrix.counts[row][column] ?? 0).length)));
55
+ const header = matrix.columns
56
+ .map((column, index) => column.padStart(columnWidths[index] + 2))
57
+ .join('');
58
+ lines.push(` ${''.padEnd(rowLabelWidth)}${header}`);
59
+ for (const row of matrix.rows) {
60
+ const cells = matrix.columns
61
+ .map((column, index) => String(matrix.counts[row][column] ?? 0).padStart(columnWidths[index] + 2))
62
+ .join('');
63
+ lines.push(` ${row.padEnd(rowLabelWidth)}${cells}`);
64
+ }
65
+ lines.push(` counted ${matrix.counted}` +
66
+ (matrix.excluded > 0 ? `, EXCLUDED ${matrix.excluded} (not classified)` : ''));
67
+ return lines;
68
+ }
69
+ /** Render one metric: headline value, gate verdict, per-tag sub-scores, then its warnings. */
70
+ export function renderMetric(metric, tags) {
71
+ const lines = [];
72
+ // The threshold's UNIT is always on the line, passing or failing. `[gate ok]` alone left a reader
73
+ // unable to tell whether the bar was two cases or 2% — and the count form exists precisely
74
+ // because those are different rules that drift apart as the corpus grows.
75
+ const gateSuffix = metric.gate
76
+ ? metric.gate.passed
77
+ ? ` [gate ok: ${metric.gate.summary}]`
78
+ : ` [GATE ${metric.gate.mode === 'fail' ? 'FAILED' : 'breached (report-only)'}: ${metric.gate.reason ?? `threshold ${metric.gate.summary} breached`}]`
79
+ : '';
80
+ lines.push(` ${metric.name}: ${formatTally(metric.overall)}${gateSuffix}`);
81
+ if (metric.description)
82
+ lines.push(` ${metric.description}`);
83
+ // Per-tag ALWAYS, not only when interesting: the 48.9%-overall / 0-of-3-on-injection result is
84
+ // exactly the one a "print it if it looks bad" rule would have hidden, since it looked fine.
85
+ for (const tag of tags) {
86
+ const tally = metric.byTag[tag];
87
+ if (!tally)
88
+ continue;
89
+ lines.push(` ${tag}: ${formatTally(tally)}`);
90
+ }
91
+ lines.push(` coverage: denominator ${metric.coverage.denominator}/${metric.coverage.total} case(s)`);
92
+ for (const warning of metric.warnings)
93
+ lines.push(` ! ${warning}`);
94
+ return lines;
95
+ }
96
+ //# sourceMappingURL=classificationRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classificationRender.js","sourceRoot":"","sources":["../src/classificationRender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ClassifiedCell, EvalClassificationReport, EvalClassificationSpec, EvalMetricSpec } from '#src/classificationTypes.js';
2
+ /**
3
+ * BATCH-25 — assemble the suite-level classification report: the confusion matrices (overall and
4
+ * per tag), every declared metric (overall and per tag), the corpus-wide coverage, and the list of
5
+ * `gate: fail` metrics that were breached.
6
+ *
7
+ * Kept separate from both the extractor ({@link ./classification.js}) and the metric engine
8
+ * ({@link ./metrics.js}) so neither imports the other, and so the whole aggregation is exercisable
9
+ * from a plain array of {@link ClassifiedCell}s with no runner, no I/O, and no model.
10
+ */
11
+ export declare function buildClassificationReport(spec: EvalClassificationSpec, metricSpecs: EvalMetricSpec[], cells: ClassifiedCell[]): EvalClassificationReport;
@@ -0,0 +1,60 @@
1
+ import { buildConfusionMatrix, collectTags } from '#src/classification.js';
2
+ import { computeMetric } from '#src/metrics.js';
3
+ import { UNRECOGNIZED_LABEL } from '#src/classificationTypes.js';
4
+ /**
5
+ * BATCH-25 — assemble the suite-level classification report: the confusion matrices (overall and
6
+ * per tag), every declared metric (overall and per tag), the corpus-wide coverage, and the list of
7
+ * `gate: fail` metrics that were breached.
8
+ *
9
+ * Kept separate from both the extractor ({@link ./classification.js}) and the metric engine
10
+ * ({@link ./metrics.js}) so neither imports the other, and so the whole aggregation is exercisable
11
+ * from a plain array of {@link ClassifiedCell}s with no runner, no I/O, and no model.
12
+ */
13
+ export function buildClassificationReport(spec, metricSpecs, cells) {
14
+ const tags = collectTags(cells);
15
+ const total = cells.length;
16
+ const scored = cells.filter((cell) => cell.scored);
17
+ const excluded = total - scored.length;
18
+ const labelMatrix = buildConfusionMatrix(cells, 'label', spec.labels);
19
+ const actionMatrix = spec.actions.length > 0 ? buildConfusionMatrix(cells, 'action', spec.actions) : undefined;
20
+ // Per-tag matrices are built from the tag's OWN cells, so a family's matrix reads on its own —
21
+ // the 0/3-on-prompt-injection result that a 48.9% aggregate hid.
22
+ const labelMatrixByTag = {};
23
+ for (const tag of tags) {
24
+ labelMatrixByTag[tag] = buildConfusionMatrix(cells.filter((cell) => cell.tags.includes(tag)), 'label', spec.labels);
25
+ }
26
+ const metrics = metricSpecs.map((metricSpec) => computeMetric(metricSpec, cells, tags));
27
+ // Report-level warnings: everything that bounds what these numbers cover, stated once at the top
28
+ // rather than left for a reader to infer from a denominator. "No silent caps" is not a per-metric
29
+ // courtesy — it is the report's contract.
30
+ const warnings = [];
31
+ if (excluded > 0) {
32
+ warnings.push(`${excluded}/${total} cell(s) produced no classification (the SUT did not run, or the ` +
33
+ 'classifier failed) and are excluded from every matrix and metric below. Coverage is ' +
34
+ `${scored.length}/${total}, NOT ${total}/${total}.`);
35
+ }
36
+ const unrecognized = scored.filter((cell) => cell.actualLabel === UNRECOGNIZED_LABEL || cell.actualAction === UNRECOGNIZED_LABEL);
37
+ if (unrecognized.length > 0) {
38
+ warnings.push(`${unrecognized.length} cell(s) produced output matching no declared value and are counted ` +
39
+ `under "${UNRECOGNIZED_LABEL}" (e.g. ${unrecognized
40
+ .slice(0, 3)
41
+ .map((cell) => cell.id)
42
+ .join(', ')}). They are scored, not dropped — an uninterpretable verdict is a finding.`);
43
+ }
44
+ const gateFailures = metrics
45
+ .filter((metric) => metric.gate && metric.gate.mode === 'fail' && !metric.gate.passed)
46
+ .map((metric) => metric.name);
47
+ return {
48
+ labels: spec.labels,
49
+ actions: spec.actions,
50
+ tags,
51
+ coverage: { total, scored: scored.length, excluded },
52
+ labelMatrix,
53
+ actionMatrix,
54
+ labelMatrixByTag,
55
+ metrics,
56
+ warnings,
57
+ gateFailures,
58
+ };
59
+ }
60
+ //# sourceMappingURL=classificationReport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classificationReport.js","sourceRoot":"","sources":["../src/classificationReport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CACvC,IAA4B,EAC5B,WAA6B,EAC7B,KAAuB;IAEvB,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC;IAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAEvC,MAAM,WAAW,GAAG,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACtE,MAAM,YAAY,GAChB,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE5F,+FAA+F;IAC/F,iEAAiE;IACjE,MAAM,gBAAgB,GAA4D,EAAE,CAAC;IACrF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,gBAAgB,CAAC,GAAG,CAAC,GAAG,oBAAoB,CAC1C,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAC/C,OAAO,EACP,IAAI,CAAC,MAAM,CACZ,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IAExF,iGAAiG;IACjG,kGAAkG;IAClG,0CAA0C;IAC1C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,QAAQ,CAAC,IAAI,CACX,GAAG,QAAQ,IAAI,KAAK,mEAAmE;YACrF,sFAAsF;YACtF,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,SAAS,KAAK,IAAI,KAAK,GAAG,CACtD,CAAC;IACJ,CAAC;IACD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAChC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,kBAAkB,IAAI,IAAI,CAAC,YAAY,KAAK,kBAAkB,CAC9F,CAAC;IACF,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CACX,GAAG,YAAY,CAAC,MAAM,sEAAsE;YAC1F,UAAU,kBAAkB,WAAW,YAAY;iBAChD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;iBACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;iBACtB,IAAI,CAAC,IAAI,CAAC,4EAA4E,CAC5F,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,OAAO;SACzB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACrF,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEhC,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI;QACJ,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE;QACpD,WAAW;QACX,YAAY;QACZ,gBAAgB;QAChB,OAAO;QACP,QAAQ;QACR,YAAY;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * BATCH-25 — the shapes for a CLASSIFIER eval: a suite-declared label/action enum, the extractors
4
+ * that turn a SUT answer into a label, the confusion matrix, and declared aggregate metrics.
5
+ *
6
+ * Deliberately its own file rather than more weight in {@link ./evalTypes.js}: eval's per-case
7
+ * PASS/FAIL shapes and a classifier's corpus-wide *distribution* shapes answer different questions,
8
+ * and only the latter needs the anti-blind-metric machinery below.
9
+ *
10
+ * ## The lesson this file is shaped by
11
+ *
12
+ * The QA-5 throwaway harness declared an `over-rejection` metric whose denominator was only the
13
+ * `safe`-labelled cases. It read a clean **0/10 at both strictness settings** while that setting was
14
+ * simultaneously rejecting seven routine commands back to the model. A perfect score, reported for a
15
+ * setting that had made behaviour materially worse, and then trusted.
16
+ *
17
+ * So, structurally, in this file:
18
+ * - a metric's denominator is **the whole scored corpus unless the author says otherwise**
19
+ * ({@link EvalMetricSpec.over} is optional and its absence means "everything");
20
+ * - every metric carries {@link EvalMetricResult.coverage}, and any shortfall — a declared subset
21
+ * denominator, cases excluded because the SUT failed, an empty denominator, or numerator cases
22
+ * sitting OUTSIDE the denominator — becomes a {@link EvalMetricResult.warnings} entry;
23
+ * - an unrecognized label is a real, visible bucket ({@link UNRECOGNIZED_LABEL}) rather than a
24
+ * dropped row, so the confusion matrix always accounts for every scored cell.
25
+ *
26
+ * A blind metric is worse than no metric, because it is trusted.
27
+ */
28
+ /** The bucket a classification lands in when the SUT produced output but it matched no declared
29
+ * label/action. It is a REAL row/column of the confusion matrix, never a dropped case — an
30
+ * unparseable verdict is a finding, and silently discarding it is how a matrix comes to read
31
+ * "covered everything" when it didn't. Parenthesized so it can never collide with a declared enum
32
+ * value (which must match `/^[\w.-]+$/`). */
33
+ export declare const UNRECOGNIZED_LABEL = "(unrecognized)";
34
+ /** The axis entry for "this case declares no expectation on this dimension" — e.g. a corpus case
35
+ * that asserts an action but no label. Like {@link UNRECOGNIZED_LABEL} it is a visible bucket.
36
+ * Parenthesized for the same collision-proof reason. */
37
+ export declare const NO_EXPECTATION = "(none)";
38
+ /**
39
+ * How a label/action is read out of a cell's answer. Deliberately tiny and TOTAL — every extractor
40
+ * either yields a declared enum value or {@link UNRECOGNIZED_LABEL}; none of them guesses.
41
+ *
42
+ * - `answer` (default) — the trimmed answer, compared case-insensitively against the declared enum.
43
+ * Suited to a suite whose prompt says "reply with exactly one of: …".
44
+ * - `json_path` — resolve a minimal dot/`[index]` path (the same resolver `json_path` assertions
45
+ * use) against the answer parsed as JSON, then match its stringified value against the enum.
46
+ * Suited to a structured-output classifier.
47
+ *
48
+ * There is deliberately NO fuzzy/substring mode. A heuristic that finds `safe` inside "this is not
49
+ * safe" is precisely the class of silent misreading this facility exists to eliminate.
50
+ */
51
+ export type ClassificationExtractor = {
52
+ kind: 'answer';
53
+ } | {
54
+ kind: 'json_path';
55
+ path: string;
56
+ };
57
+ /**
58
+ * A suite's `classification:` declaration — the enum that gives the confusion matrix its axes, plus
59
+ * how to read a value out of the SUT's answer.
60
+ *
61
+ * `actions` is empty when the suite classifies labels only. The two dimensions are separate because
62
+ * the QA-5 corpus asserts two different things about the same case: **the label the model returned**
63
+ * (diagnostic) and **the action the gate took** (what a user actually experiences). They diverge by
64
+ * design — deterministic preflights rewrite the verdict independently of the model — so scoring
65
+ * labels alone overstates the model and understates the gate.
66
+ */
67
+ export interface EvalClassificationSpec {
68
+ /** The declared label enum (≥1 value). Gives the matrix its axes and validates every
69
+ * `expect_label` and every metric literal at parse time. */
70
+ labels: string[];
71
+ /** The declared action enum, or `[]` when the suite asserts labels only. */
72
+ actions: string[];
73
+ /** How the ACTUAL label is read from a cell's answer. */
74
+ labelFrom: ClassificationExtractor;
75
+ /** How the ACTUAL action is read. `undefined` = the suite declares no action dimension; an
76
+ * `expect_action` assertion is then a parse error rather than a silently ungradeable one. */
77
+ actionFrom?: ClassificationExtractor;
78
+ }
79
+ /** One field a metric predicate can read. `expected.*` is what the CORPUS declares; `actual.*` is
80
+ * what the SUT produced. The prose in a corpus plan says "label" for the corpus label and "action"
81
+ * for the actual action taken, and that ambiguity is exactly what produces a silently-wrong metric —
82
+ * so the surface forces the distinction to be written down. */
83
+ export type MetricField = 'expected.label' | 'expected.action' | 'actual.label' | 'actual.action';
84
+ /** The literal `none` in a predicate — matches a cell where that field is absent. */
85
+ export declare const METRIC_NONE_LITERAL = "none";
86
+ /**
87
+ * One comparison inside a metric's predicate list. Predicates in a list are **ANDed**; there is no
88
+ * `or`, no nesting and no parentheses.
89
+ *
90
+ * That is a deliberate limit, not an oversight. Every metric the approvals corpus plan declares —
91
+ * `false_approve`, `false_halt`, `over_escalation`, `outcome_accuracy`, `exfil_recall`,
92
+ * `floor_recall` — is a conjunction of at most two comparisons. A general expression grammar would
93
+ * add a parser to the ONE component where a bug yields wrong numbers that look right, which is the
94
+ * failure this whole file is built to prevent. If a corpus ever needs disjunction, that is a new
95
+ * node, not a silent generalisation.
96
+ */
97
+ export type MetricPredicate =
98
+ /** `<field> == <value>` / `<field> != <value>`, where the right-hand side is a literal. */
99
+ {
100
+ kind: 'compare';
101
+ field: MetricField;
102
+ negated: boolean;
103
+ value: string;
104
+ }
105
+ /** `<field> == <field>` / `<field> != <field>` — how `outcome_accuracy` is written
106
+ * (`actual.label == expected.label`). */
107
+ | {
108
+ kind: 'compareField';
109
+ field: MetricField;
110
+ negated: boolean;
111
+ other: MetricField;
112
+ }
113
+ /** `<field> in [a, b]` / `<field> not in [a, b]`. */
114
+ | {
115
+ kind: 'in';
116
+ field: MetricField;
117
+ negated: boolean;
118
+ values: string[];
119
+ }
120
+ /** `has_tag(x)` / `not has_tag(x)`. */
121
+ | {
122
+ kind: 'tag';
123
+ negated: boolean;
124
+ tag: string;
125
+ };
126
+ /**
127
+ * A suite-declared aggregate metric over the corpus.
128
+ *
129
+ * `over` (the denominator) is OPTIONAL and its absence means **the whole scored corpus**. That
130
+ * default is the load-bearing part of this type: an author who does not think about the denominator
131
+ * gets the corpus-wide one, and an author who narrows it gets a warning saying how much of the
132
+ * corpus their number can no longer see.
133
+ */
134
+ export interface EvalMetricSpec {
135
+ name: string;
136
+ /** The numerator predicate list (ANDed). Evaluated only over cells already in the denominator. */
137
+ where: MetricPredicate[];
138
+ /** The denominator predicate list (ANDed). Absent = every scored cell — corpus-wide by
139
+ * construction. */
140
+ over?: MetricPredicate[];
141
+ /** FRACTION gate: fail when the value is strictly GREATER than this (0..1). */
142
+ max?: number;
143
+ /** FRACTION gate: fail when the value is strictly LESS than this (0..1) — or when the denominator
144
+ * is empty (a recall metric that measured nothing has not met its floor; it has measured
145
+ * nothing). */
146
+ min?: number;
147
+ /**
148
+ * COUNT gate: fail when the NUMERATOR exceeds this many cases.
149
+ *
150
+ * Not sugar for {@link max}. A target like "at most 2 of the 22 cases in this family" is an
151
+ * absolute count against an honest denominator, and expressing it as a fraction has two failures
152
+ * the fraction form cannot avoid:
153
+ *
154
+ * - the author must compute `2/22 = 0.0909` by hand from the current corpus size, and
155
+ * - **it silently drifts as the corpus grows.** Add ten cases and the gate quietly tightens or
156
+ * loosens — no edit, no warning, the number still plausible while its meaning has moved.
157
+ *
158
+ * That second one is the same species as the blind denominator the rest of this file guards
159
+ * against: a number that stays believable while what it measures changes underneath it. A count
160
+ * gate is invariant to corpus size by construction.
161
+ *
162
+ * A count gate also reads more honestly wherever the target genuinely IS zero cases —
163
+ * `max_count: 0` says "not one case may do this", where `max: 0` says it in a unit that happens
164
+ * to coincide at zero.
165
+ */
166
+ maxCount?: number;
167
+ /** COUNT gate: fail when the NUMERATOR is below this many cases. Unlike {@link min}, this needs
168
+ * no empty-denominator special case — an empty denominator yields a numerator of 0, which is
169
+ * simply below any positive floor. */
170
+ minCount?: number;
171
+ /** `fail` (default when a threshold is declared) — a tripped gate fails the run (exit 1).
172
+ * `report` — the threshold is computed and printed but never changes the exit code. */
173
+ gate: 'fail' | 'report';
174
+ /** Optional human note carried into the output, for a metric whose meaning is not its name. */
175
+ description?: string;
176
+ }
177
+ /** One metric's numerator/denominator/value. `value` is `null` — never `0` — when the denominator is
178
+ * empty, because "0% of nothing" and "0% of the corpus" are opposite findings and a metric that
179
+ * silently reports the former as the latter is the exact bug this facility exists to catch. */
180
+ export interface EvalMetricTally {
181
+ numerator: number;
182
+ denominator: number;
183
+ value: number | null;
184
+ }
185
+ /** How much of the corpus a metric could actually see. Always emitted, never conditional — point 9
186
+ * ("no silent caps") and the anti-blind-metric rule are the same requirement, so they are the same
187
+ * field. */
188
+ export interface EvalMetricCoverage {
189
+ /** Every cell in the suite. */
190
+ total: number;
191
+ /** Cells that produced a gradeable classification (the SUT ran). */
192
+ scored: number;
193
+ /** `total - scored` — cells that could not be classified at all. */
194
+ excluded: number;
195
+ /** How many scored cells this metric's own denominator selected. */
196
+ denominator: number;
197
+ }
198
+ /** One metric's computed result, overall and per tag. */
199
+ export interface EvalMetricResult {
200
+ name: string;
201
+ description?: string;
202
+ overall: EvalMetricTally;
203
+ /** Per-family sub-scores. An aggregate hides adversarial collapse: the first QA-5 baseline scored
204
+ * 48.9% overall while scoring 0/3 on prompt injection, and a single blended number would have
205
+ * shipped that. Keyed by tag, in the suite's tag order. */
206
+ byTag: Record<string, EvalMetricTally>;
207
+ coverage: EvalMetricCoverage;
208
+ /** Every way this metric's denominator falls short of the whole corpus. Empty is the good case. */
209
+ warnings: string[];
210
+ /** Present when the metric declares any threshold. */
211
+ gate?: {
212
+ /** Which unit the thresholds are in. A consumer must never have to infer whether `2` meant two
213
+ * cases or 200% — the two forms are mutually exclusive per metric, and this names which. */
214
+ kind: 'fraction' | 'count';
215
+ max?: number;
216
+ min?: number;
217
+ maxCount?: number;
218
+ minCount?: number;
219
+ mode: 'fail' | 'report';
220
+ /** `false` = the threshold was breached. A breached `fail` gate fails the run. */
221
+ passed: boolean;
222
+ /** Why it failed; empty when it passed. Always states the unit. */
223
+ reason?: string;
224
+ /** The threshold as a human string, unit included (`≤ 2 case(s)`, `≤ 5.0%`). Rendered whether
225
+ * the gate passed or failed, so a passing gate is as legible as a failing one. */
226
+ summary: string;
227
+ };
228
+ }
229
+ /**
230
+ * A confusion matrix: rows = what the corpus expected, columns = what the SUT actually produced.
231
+ *
232
+ * The primary artifact of a classifier eval, because **which way it is wrong is the whole signal**:
233
+ * on the approvals scale (`safe` · `destructive` · `catastrophic` · `attack`) an `attack` graded
234
+ * `destructive` means a prompt instead of a halt, while a `destructive` graded `safe` is a security
235
+ * incident. A single accuracy percentage cannot tell those apart.
236
+ */
237
+ export interface EvalConfusionMatrix {
238
+ /** Which dimension this matrix is over. */
239
+ dimension: 'label' | 'action';
240
+ /** Row axis (expected): the declared enum, plus {@link NO_EXPECTATION} when some cell declares
241
+ * none. */
242
+ rows: string[];
243
+ /** Column axis (actual): the declared enum, plus {@link UNRECOGNIZED_LABEL} when some cell
244
+ * produced an unmatched value, plus {@link NO_EXPECTATION} when some cell produced nothing. */
245
+ columns: string[];
246
+ /** `counts[expectedRow][actualColumn]`. Every row/column key is present (zeros included) so a
247
+ * renderer never has to guess an axis. */
248
+ counts: Record<string, Record<string, number>>;
249
+ /** Cells counted here. `counted + excluded === ` the suite's cell total — asserted in the unit
250
+ * suite, because a matrix that silently drops errored cells reads as "covered everything". */
251
+ counted: number;
252
+ /** Cells NOT counted: the SUT did not run, so there is nothing to place. */
253
+ excluded: number;
254
+ }
255
+ /**
256
+ * The suite-level classification report — the classifier eval's answer, attached to
257
+ * {@link ./evalTypes.js EvalSuiteSummary}. Absent entirely for a suite that declares no
258
+ * `classification:` block, so a #405-era suite's `results.json` is unchanged.
259
+ */
260
+ export interface EvalClassificationReport {
261
+ labels: string[];
262
+ actions: string[];
263
+ /** Every tag any case declared, sorted — the per-tag sub-score axis. */
264
+ tags: string[];
265
+ /** Corpus-wide coverage. `excluded > 0` means some cells could not be classified at all. */
266
+ coverage: {
267
+ total: number;
268
+ scored: number;
269
+ excluded: number;
270
+ };
271
+ labelMatrix: EvalConfusionMatrix;
272
+ /** Present only when the suite declares an action dimension. */
273
+ actionMatrix?: EvalConfusionMatrix;
274
+ /** Per-tag label matrices, keyed by tag — an adversarial family's matrix read on its own. */
275
+ labelMatrixByTag: Record<string, EvalConfusionMatrix>;
276
+ metrics: EvalMetricResult[];
277
+ /** Report-level warnings (as opposed to per-metric ones): excluded cells, unrecognized outputs,
278
+ * and anything else that bounds what these numbers cover. */
279
+ warnings: string[];
280
+ /** Names of metrics whose `gate: fail` threshold was breached. Non-empty ⇒ the run fails. */
281
+ gateFailures: string[];
282
+ }
283
+ /**
284
+ * One graded cell reduced to what the matrices and the metrics need. Built by
285
+ * {@link ../evalRunner.js} from the graded results; its own tiny shape so both consumers read the
286
+ * same thing and the aggregation is unit-testable without constructing whole `EvalCaseResult`s.
287
+ */
288
+ export interface ClassifiedCell {
289
+ id: string;
290
+ tags: string[];
291
+ expectedLabel?: string;
292
+ expectedAction?: string;
293
+ actualLabel?: string;
294
+ actualAction?: string;
295
+ /** `false` = the SUT did not run / the classifier failed, so this cell has no place in any matrix
296
+ * or denominator. It is counted as `excluded` and reported, never as a wrong answer. */
297
+ scored: boolean;
298
+ }
299
+ /** One cell's classification, as recorded on its {@link ./evalTypes.js EvalCaseResult}. */
300
+ export interface EvalCaseClassification {
301
+ expectedLabel?: string;
302
+ expectedAction?: string;
303
+ actualLabel?: string;
304
+ actualAction?: string;
305
+ /** The raw text the extractors read, kept so an `(unrecognized)` result is diagnosable without
306
+ * re-running. Omitted when it is identical to the cell's `answer`. */
307
+ raw?: string;
308
+ /** How many model calls this classification cost. Present only on the injected-classifier
309
+ * (Half B) path; the answer-extraction path always costs the cell's own single run. */
310
+ modelCalls?: number;
311
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * BATCH-25 — the shapes for a CLASSIFIER eval: a suite-declared label/action enum, the extractors
4
+ * that turn a SUT answer into a label, the confusion matrix, and declared aggregate metrics.
5
+ *
6
+ * Deliberately its own file rather than more weight in {@link ./evalTypes.js}: eval's per-case
7
+ * PASS/FAIL shapes and a classifier's corpus-wide *distribution* shapes answer different questions,
8
+ * and only the latter needs the anti-blind-metric machinery below.
9
+ *
10
+ * ## The lesson this file is shaped by
11
+ *
12
+ * The QA-5 throwaway harness declared an `over-rejection` metric whose denominator was only the
13
+ * `safe`-labelled cases. It read a clean **0/10 at both strictness settings** while that setting was
14
+ * simultaneously rejecting seven routine commands back to the model. A perfect score, reported for a
15
+ * setting that had made behaviour materially worse, and then trusted.
16
+ *
17
+ * So, structurally, in this file:
18
+ * - a metric's denominator is **the whole scored corpus unless the author says otherwise**
19
+ * ({@link EvalMetricSpec.over} is optional and its absence means "everything");
20
+ * - every metric carries {@link EvalMetricResult.coverage}, and any shortfall — a declared subset
21
+ * denominator, cases excluded because the SUT failed, an empty denominator, or numerator cases
22
+ * sitting OUTSIDE the denominator — becomes a {@link EvalMetricResult.warnings} entry;
23
+ * - an unrecognized label is a real, visible bucket ({@link UNRECOGNIZED_LABEL}) rather than a
24
+ * dropped row, so the confusion matrix always accounts for every scored cell.
25
+ *
26
+ * A blind metric is worse than no metric, because it is trusted.
27
+ */
28
+ /** The bucket a classification lands in when the SUT produced output but it matched no declared
29
+ * label/action. It is a REAL row/column of the confusion matrix, never a dropped case — an
30
+ * unparseable verdict is a finding, and silently discarding it is how a matrix comes to read
31
+ * "covered everything" when it didn't. Parenthesized so it can never collide with a declared enum
32
+ * value (which must match `/^[\w.-]+$/`). */
33
+ export const UNRECOGNIZED_LABEL = '(unrecognized)';
34
+ /** The axis entry for "this case declares no expectation on this dimension" — e.g. a corpus case
35
+ * that asserts an action but no label. Like {@link UNRECOGNIZED_LABEL} it is a visible bucket.
36
+ * Parenthesized for the same collision-proof reason. */
37
+ export const NO_EXPECTATION = '(none)';
38
+ /** The literal `none` in a predicate — matches a cell where that field is absent. */
39
+ export const METRIC_NONE_LITERAL = 'none';
40
+ //# sourceMappingURL=classificationTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classificationTypes.js","sourceRoot":"","sources":["../src/classificationTypes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;6CAI6C;AAC7C,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAEnD;;wDAEwD;AACxD,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AA8CvC,qFAAqF;AACrF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC"}
@@ -0,0 +1,108 @@
1
+ import type { EvalSuite, EvalSuiteSummary, EvalSweep } from '#src/evalTypes.js';
2
+ /**
3
+ * BATCH-25 — the comparison layer: run the same corpus across a sweep of configurations and emit
4
+ * ONE comparison table, and diff a run against a previous one.
5
+ *
6
+ * ## Why a sweep is an OUTER loop and not a runner concept
7
+ *
8
+ * A sweep is "the same suite, a different config" — structurally identical to BATCH-19's
9
+ * multi-suite loop, and unrelated to grading. Threading it into `runEvalSuite` would have meant
10
+ * generalising the (case × identity) unit to (case × identity × sweep cell) and rewriting the
11
+ * `RunCellFn` resolution that #405's identity matrix depends on. Instead the command runs the suite
12
+ * once per cell and this module folds the N summaries into one table. The identity matrix is
13
+ * untouched, and the run-over-run diff falls out of the same code for free.
14
+ *
15
+ * ## Why one table and not N reports
16
+ *
17
+ * The decisive QA-5 result came from running one corpus at two settings and diffing them; N
18
+ * separate reports is the form in which that result is invisible. So the artifact is a table whose
19
+ * rows are metrics and whose columns are cells, plus the same for overall accuracy.
20
+ */
21
+ /** One sweep cell: a name and the config overrides that produce it. */
22
+ export interface SweepCell {
23
+ /** `axis=value` joined by ` · ` across axes — stable, and derived from the declared names. */
24
+ name: string;
25
+ /** A filename-safe form of {@link name} (`rung-auto-safe__model-flash`), used as an output-dir
26
+ * component. Built from parse-time-validated path-safe tokens, so it can neither traverse nor
27
+ * escape the output root. */
28
+ dirName: string;
29
+ /** The `model` override for this cell, when any axis value sets one. */
30
+ model?: string;
31
+ /** The merged plain-data config overrides for this cell. */
32
+ config: Record<string, unknown>;
33
+ }
34
+ /**
35
+ * Expand a {@link EvalSweep} into its cartesian product of cells, in declared axis order.
36
+ *
37
+ * A later axis wins on a conflicting key, and that is documented rather than defended against: two
38
+ * axes that set the same config key are describing the same knob twice, which the author should see
39
+ * in the cell name.
40
+ */
41
+ export declare function expandSweep(sweep: EvalSweep): SweepCell[];
42
+ /**
43
+ * Deep-merge plain data (the sweep's `config:` overrides) into a target.
44
+ *
45
+ * Objects merge recursively; arrays and scalars REPLACE. Replacing an array is the right default
46
+ * for config: an override that appended to `mcpServers` or `allowedTools` would silently keep the
47
+ * base value the author meant to displace, which is the harder bug to see.
48
+ *
49
+ * Prototype-polluting keys are skipped — a suite file is only semi-trusted input.
50
+ */
51
+ export declare function deepMerge<T extends Record<string, unknown>>(base: T, overrides: Record<string, unknown>): T;
52
+ /** One column of the comparison table: a cell name and the summary it produced. */
53
+ export interface ComparisonColumn {
54
+ name: string;
55
+ summary: EvalSuiteSummary;
56
+ }
57
+ /**
58
+ * Render the cross-cell comparison table.
59
+ *
60
+ * Rows are the metrics the suite declares (plus pass rate), columns are the sweep cells. A metric
61
+ * that a cell could not compute renders `n/a`, never a blank and never a zero — the same rule the
62
+ * metric engine follows, for the same reason.
63
+ *
64
+ * Per-tag sub-scores get their own rows under each metric, because the whole point of running a
65
+ * sweep is to see which setting moved which family, and a blended per-cell number cannot show that.
66
+ */
67
+ export declare function renderComparison(columns: ComparisonColumn[]): string[];
68
+ /** One case whose verdict or classification moved between two runs. */
69
+ export interface RunDiffEntry {
70
+ id: string;
71
+ before: string;
72
+ after: string;
73
+ }
74
+ /** The run-over-run diff. */
75
+ export interface RunDiff {
76
+ /** Cases in both runs. */
77
+ compared: number;
78
+ /** Cases that went PASS → FAIL. The regression list. */
79
+ regressed: RunDiffEntry[];
80
+ /** Cases that went FAIL → PASS. */
81
+ fixed: RunDiffEntry[];
82
+ /** Cases whose actual label/action changed, verdict aside — a rating-prompt edit's real signal. */
83
+ reclassified: RunDiffEntry[];
84
+ /** Metric deltas, `after - before`, for metrics both runs computed. */
85
+ metricDeltas: {
86
+ name: string;
87
+ before: number | null;
88
+ after: number | null;
89
+ delta: number | null;
90
+ }[];
91
+ /** Ids in only one of the two runs — reported, so a shrunken corpus cannot read as "no change". */
92
+ onlyInBefore: string[];
93
+ onlyInAfter: string[];
94
+ warnings: string[];
95
+ }
96
+ /**
97
+ * Diff two runs of the same suite, so a rating-prompt edit produces a signal rather than a vibe.
98
+ *
99
+ * Three separate lists, because they answer different questions: verdict regressions are what a CI
100
+ * gate reads, verdict fixes are what a change claims to have done, and RECLASSIFICATIONS are what
101
+ * a prompt edit actually moved — a case can keep its verdict while the label underneath it changes,
102
+ * and that is exactly the drift a pass-rate comparison cannot see.
103
+ */
104
+ export declare function diffRuns(before: EvalSuiteSummary, after: EvalSuiteSummary): RunDiff;
105
+ /** Render a {@link RunDiff} as plain lines. */
106
+ export declare function renderRunDiff(diff: RunDiff): string[];
107
+ /** Does this suite declare a sweep? Small helper so the command reads declaratively. */
108
+ export declare function suiteSweep(suite: EvalSuite): EvalSweep | undefined;