@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.
- 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
|
@@ -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":"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"}
|
|
@@ -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;
|