@tangle-network/agent-eval 0.209.1 → 0.210.0
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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/dist/adapters/http.d.ts +1 -1
- package/dist/analyst/index.d.ts +2 -2
- package/dist/analyst/index.js +1 -1
- package/dist/{analyze-runs-0cdYMgob.js → analyze-runs-3xpby3p2.js} +4 -4
- package/dist/analyze-runs-3xpby3p2.js.map +1 -0
- package/dist/{benchmark-command-Cve7mHNC.js → benchmark-command-S8tCtkxh.js} +3 -3
- package/dist/{benchmark-command-Cve7mHNC.js.map → benchmark-command-S8tCtkxh.js.map} +1 -1
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +2 -2
- package/dist/campaign/index.d.ts +2 -2
- package/dist/campaign/index.js +9 -9
- package/dist/{campaign-evidence-B5qsCHEn.js → campaign-evidence-OcUciD-N.js} +2 -2
- package/dist/campaign-evidence-OcUciD-N.js.map +1 -0
- package/dist/{campaign-CvhmiLKa.js → campaign-wvvJA1ps.js} +10 -10
- package/dist/{campaign-CvhmiLKa.js.map → campaign-wvvJA1ps.js.map} +1 -1
- package/dist/claude-jsonl-dZir12m6.js +86 -0
- package/dist/claude-jsonl-dZir12m6.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/contract/index.d.ts +5 -5
- package/dist/contract/index.js +90 -14
- package/dist/contract/index.js.map +1 -1
- package/dist/counterfactual-DtMWvn7D.js.map +1 -1
- package/dist/{define-agent-eval-_nyV5nev.d.ts → define-agent-eval-B_zZY2AZ.d.ts} +3 -3
- package/dist/{define-agent-eval-_nyV5nev.d.ts.map → define-agent-eval-B_zZY2AZ.d.ts.map} +1 -1
- package/dist/{define-agent-eval-Csd47ur_.js → define-agent-eval-CoXXDCMu.js} +4 -4
- package/dist/{define-agent-eval-Csd47ur_.js.map → define-agent-eval-CoXXDCMu.js.map} +1 -1
- package/dist/diagnosis/index.js +1 -1
- package/dist/{eval-campaign-C31Qbv77.js → eval-campaign-4byTtqrf.js} +2 -2
- package/dist/{eval-campaign-C31Qbv77.js.map → eval-campaign-4byTtqrf.js.map} +1 -1
- package/dist/experiment/index.d.ts +1 -1
- package/dist/experiment/index.js +6 -6
- package/dist/feedback-trajectory-CX01700m.d.ts.map +1 -1
- package/dist/{index-H-u7f-w3.d.ts → index-D0_l25We.d.ts} +20 -41
- package/dist/{index-H-u7f-w3.d.ts.map → index-D0_l25We.d.ts.map} +1 -1
- package/dist/{index-BHh9Z9El.d.ts → index-DnZdBxCL.d.ts} +9 -6
- package/dist/{index-BHh9Z9El.d.ts.map → index-DnZdBxCL.d.ts.map} +1 -1
- package/dist/{index-Ddi5M-TQ.d.ts → index-xnhRdqik.d.ts} +3 -14
- package/dist/{index-Ddi5M-TQ.d.ts.map → index-xnhRdqik.d.ts.map} +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/judge-gate-C59znGmR.js +638 -0
- package/dist/judge-gate-C59znGmR.js.map +1 -0
- package/dist/judge-gate-D2hCppem.d.ts +439 -0
- package/dist/judge-gate-D2hCppem.d.ts.map +1 -0
- package/dist/{llm-judge-C0256VT_.js → llm-judge-Tr7MBUnf.js} +6 -6
- package/dist/llm-judge-Tr7MBUnf.js.map +1 -0
- package/dist/meta-eval/index.d.ts +2 -132
- package/dist/meta-eval/index.d.ts.map +1 -1
- package/dist/meta-eval/index.js +2 -275
- package/dist/meta-eval/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/{paired-arms-B5v-X7No.js → paired-arms-DzKn0TfY.js} +2 -2
- package/dist/{paired-arms-B5v-X7No.js.map → paired-arms-DzKn0TfY.js.map} +1 -1
- package/dist/{paired-binary-b2L3WKqr.js → paired-binary-Q36bhn2R.js} +13 -9
- package/dist/paired-binary-Q36bhn2R.js.map +1 -0
- package/dist/{paired-promotion-decision-2deJ7sDX.js → paired-promotion-decision-BN6IQTiu.js} +2 -2
- package/dist/{paired-promotion-decision-2deJ7sDX.js.map → paired-promotion-decision-BN6IQTiu.js.map} +1 -1
- package/dist/{produced-state-CgB9g0VX.js → produced-state-CKybuR9F.js} +3 -3
- package/dist/produced-state-CKybuR9F.js.map +1 -0
- package/dist/{promotion-policy-CCmyEH_c.js → promotion-policy-CWW35UWq.js} +3 -3
- package/dist/{promotion-policy-CCmyEH_c.js.map → promotion-policy-CWW35UWq.js.map} +1 -1
- package/dist/reporting.js +2 -2
- package/dist/rl.js +3 -3
- package/dist/rollout/index.d.ts +2 -2
- package/dist/rollout/index.js +3 -3
- package/dist/{rollout-BZqQc0YY.js → rollout-D9bKKBnZ.js} +29 -164
- package/dist/rollout-D9bKKBnZ.js.map +1 -0
- package/dist/search/index.js +2 -2
- package/dist/{search-kernel-Bfe3kzRk.js → search-kernel-BIXWM7YV.js} +3 -3
- package/dist/search-kernel-BIXWM7YV.js.map +1 -0
- package/dist/{search-summary-DK1bwTAf.js → search-summary-nwrtXxC_.js} +3 -3
- package/dist/{search-summary-DK1bwTAf.js.map → search-summary-nwrtXxC_.js.map} +1 -1
- package/dist/{sequential-BOzBND4t.js → sequential-D-1PPCmM.js} +2 -2
- package/dist/{sequential-BOzBND4t.js.map → sequential-D-1PPCmM.js.map} +1 -1
- package/dist/{skillopt-optimization-method-BCojvoP1.js → skillopt-optimization-method-DujPOiwD.js} +4 -4
- package/dist/{skillopt-optimization-method-BCojvoP1.js.map → skillopt-optimization-method-DujPOiwD.js.map} +1 -1
- package/dist/{statistical-heldout-BP6iEJvR.js → statistical-heldout-1Gbmex7v.js} +2 -2
- package/dist/{statistical-heldout-BP6iEJvR.js.map → statistical-heldout-1Gbmex7v.js.map} +1 -1
- package/dist/{statistical-heldout-D0rX_IDl.d.ts → statistical-heldout-8RGPIeyo.d.ts} +14 -10
- package/dist/statistical-heldout-8RGPIeyo.d.ts.map +1 -0
- package/dist/{summary-report-BsaEFEwA.js → summary-report-DtdRucM0.js} +2 -2
- package/dist/{summary-report-BsaEFEwA.js.map → summary-report-DtdRucM0.js.map} +1 -1
- package/dist/supervisor-run/index.d.ts.map +1 -1
- package/dist/supervisor-run/index.js +34 -69
- package/dist/supervisor-run/index.js.map +1 -1
- package/dist/{tree-CZ78tiPg.js → tree-kc1d80yF.js} +3 -3
- package/dist/{tree-CZ78tiPg.js.map → tree-kc1d80yF.js.map} +1 -1
- package/docs/concepts.md +13 -0
- package/docs/experiment.md +10 -0
- package/docs/trace-analysis.md +3 -0
- package/package.json +4 -3
- package/dist/analyze-runs-0cdYMgob.js.map +0 -1
- package/dist/campaign-evidence-B5qsCHEn.js.map +0 -1
- package/dist/claude-jsonl-CG3JUbTJ.js +0 -211
- package/dist/claude-jsonl-CG3JUbTJ.js.map +0 -1
- package/dist/llm-judge-C0256VT_.js.map +0 -1
- package/dist/paired-binary-b2L3WKqr.js.map +0 -1
- package/dist/produced-state-CgB9g0VX.js.map +0 -1
- package/dist/rollout-BZqQc0YY.js.map +0 -1
- package/dist/search-kernel-Bfe3kzRk.js.map +0 -1
- package/dist/series-convergence-CjO2QdRW.js +0 -43
- package/dist/series-convergence-CjO2QdRW.js.map +0 -1
- package/dist/series-convergence-hEfKdoi-.d.ts +0 -126
- package/dist/series-convergence-hEfKdoi-.d.ts.map +0 -1
- package/dist/statistical-heldout-D0rX_IDl.d.ts.map +0 -1
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { i as JudgeError, s as ValidationError } from "./errors-Dngq5h35.js";
|
|
2
|
+
import { r as computeInterval } from "./ast-DJ6Bc7ab.js";
|
|
3
|
+
//#region src/series-convergence.ts
|
|
4
|
+
function analyzeSeries(values, options = {}) {
|
|
5
|
+
const window = options.window ?? 5;
|
|
6
|
+
const stableCv = options.stableCv ?? .05;
|
|
7
|
+
const driftRun = options.driftRun ?? 3;
|
|
8
|
+
if (values.length < Math.max(2, Math.min(window, 3))) return {
|
|
9
|
+
state: "insufficient-data",
|
|
10
|
+
windowMean: 0,
|
|
11
|
+
windowCv: 0,
|
|
12
|
+
tailRun: 0,
|
|
13
|
+
stable: false
|
|
14
|
+
};
|
|
15
|
+
const tail = values.slice(-window);
|
|
16
|
+
const mean = tail.reduce((a, b) => a + b, 0) / tail.length;
|
|
17
|
+
const variance = tail.reduce((acc, v) => acc + (v - mean) ** 2, 0) / tail.length;
|
|
18
|
+
const cv = Math.sqrt(variance) / (Math.abs(mean) > 1e-9 ? Math.abs(mean) : 1);
|
|
19
|
+
const stable = tail.length >= window && cv <= stableCv;
|
|
20
|
+
let tailRun = 0;
|
|
21
|
+
let direction = 0;
|
|
22
|
+
for (let i = values.length - 1; i > 0; i--) {
|
|
23
|
+
const delta = values[i] - values[i - 1];
|
|
24
|
+
if (delta === 0) break;
|
|
25
|
+
const dir = delta > 0 ? 1 : -1;
|
|
26
|
+
if (direction === 0) direction = dir;
|
|
27
|
+
if (dir !== direction) break;
|
|
28
|
+
tailRun += dir;
|
|
29
|
+
}
|
|
30
|
+
let state;
|
|
31
|
+
if (stable) state = "stabilized";
|
|
32
|
+
else if (Math.abs(tailRun) >= driftRun) state = tailRun > 0 ? "drifting-up" : "drifting-down";
|
|
33
|
+
else state = "noisy";
|
|
34
|
+
return {
|
|
35
|
+
state,
|
|
36
|
+
windowMean: mean,
|
|
37
|
+
windowCv: cv,
|
|
38
|
+
tailRun,
|
|
39
|
+
stable
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/meta-eval/sentinel.ts
|
|
44
|
+
/**
|
|
45
|
+
* Judge sentinel — eval trustworthiness as a continuously measured,
|
|
46
|
+
* alarmed trend.
|
|
47
|
+
*
|
|
48
|
+
* Judges are models; models change underneath us; calibration decays.
|
|
49
|
+
* This module composes three existing instruments into one loop:
|
|
50
|
+
*
|
|
51
|
+
* snapshot — adapters turn real instrument outputs (`calibrateJudge` /
|
|
52
|
+
* `calibrateJudgeContinuous`, `corpusInterRaterAgreement` /
|
|
53
|
+
* `continuousAgreement`, a rerun sentinel golden set) into
|
|
54
|
+
* `SentinelSnapshot`s
|
|
55
|
+
* series — `analyzeSeries` (src/series-convergence) classifies each
|
|
56
|
+
* judge×metric history as stabilized / drifting / noisy
|
|
57
|
+
* alarm — `judgeSentinelReport` turns trends + thresholds into named
|
|
58
|
+
* alarms; `evalHealthStamp` is the tiny object a campaign
|
|
59
|
+
* attaches to its verdicts
|
|
60
|
+
*
|
|
61
|
+
* Alarm conditions:
|
|
62
|
+
* - convergence state `drifting-down` on any tracked metric
|
|
63
|
+
* - irr below `minIrr`
|
|
64
|
+
* - calibrationKappa / sentinelPassRate dropped vs the series baseline
|
|
65
|
+
* beyond `maxKappaDrop` / `maxSentinelDrop`
|
|
66
|
+
* - judge model changed with no post-change golden-grounded snapshot
|
|
67
|
+
* (the silent-upgrade trap: judges agreeing with each other after a
|
|
68
|
+
* model swap proves nothing — only re-measuring against gold does)
|
|
69
|
+
* - newest snapshot older than `staleAfterDays` relative to `asOf`
|
|
70
|
+
*
|
|
71
|
+
* Wire-in: run the snapshot adapters from a post-campaign hook or a
|
|
72
|
+
* nightly job, append to a `SentinelStore`, then compute
|
|
73
|
+
* `judgeSentinelReport(await store.history(), { asOf })` with `asOf` =
|
|
74
|
+
* the campaign timestamp. Attach `evalHealthStamp(report)` to campaign
|
|
75
|
+
* verdicts: an alarmed sentinel means every downstream verdict carries
|
|
76
|
+
* an integrity warning until the judge is recalibrated.
|
|
77
|
+
*
|
|
78
|
+
* The module is clock-free — every timestamp (`at`, `asOf`) is
|
|
79
|
+
* caller-supplied ISO-8601, so reports are reproducible.
|
|
80
|
+
*/
|
|
81
|
+
const SENTINEL_METRIC_NAMES = [
|
|
82
|
+
"irr",
|
|
83
|
+
"calibrationKappa",
|
|
84
|
+
"sentinelPassRate"
|
|
85
|
+
];
|
|
86
|
+
function parseIso$1(value, label) {
|
|
87
|
+
const ms = typeof value === "string" ? Date.parse(value) : NaN;
|
|
88
|
+
if (!Number.isFinite(ms)) throw new ValidationError(`judge-sentinel: ${label} is not a parseable ISO timestamp: ${JSON.stringify(value)}`);
|
|
89
|
+
return ms;
|
|
90
|
+
}
|
|
91
|
+
/** Shape guard for snapshots crossing a boundary (store append, JSONL load, report input). */
|
|
92
|
+
function validateSentinelSnapshot(snapshot, source = "snapshot") {
|
|
93
|
+
parseIso$1(snapshot.at, `${source}.at`);
|
|
94
|
+
if (typeof snapshot.judgeId !== "string" || snapshot.judgeId.length === 0) throw new ValidationError(`judge-sentinel: ${source}.judgeId must be a non-empty string`);
|
|
95
|
+
if (typeof snapshot.judgeModel !== "string" || snapshot.judgeModel.length === 0) throw new ValidationError(`judge-sentinel: ${source}.judgeModel must be a non-empty string`);
|
|
96
|
+
if (snapshot.metrics === null || typeof snapshot.metrics !== "object") throw new ValidationError(`judge-sentinel: ${source}.metrics must be an object (may be empty)`);
|
|
97
|
+
for (const [key, value] of Object.entries(snapshot.metrics)) {
|
|
98
|
+
if (!SENTINEL_METRIC_NAMES.includes(key)) throw new ValidationError(`judge-sentinel: ${source}.metrics carries unknown key "${key}" — known: ${SENTINEL_METRIC_NAMES.join(", ")}`);
|
|
99
|
+
if (value !== void 0 && !Number.isFinite(value)) throw new ValidationError(`judge-sentinel: ${source}.metrics.${key} is not a finite number: ${String(value)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* From `calibrateJudge` / `calibrateJudgeContinuous` output. Prefers the
|
|
104
|
+
* un-rounded continuous κ_w when the report carries one (integer κ
|
|
105
|
+
* discards information for [0,1] judges).
|
|
106
|
+
*/
|
|
107
|
+
function snapshotFromCalibration(report, meta) {
|
|
108
|
+
const kappa = "weightedKappaContinuous" in report ? report.weightedKappaContinuous : report.kappa;
|
|
109
|
+
if (!Number.isFinite(kappa)) throw new ValidationError(`snapshotFromCalibration: κ is not finite (n=${report.n}) — calibrate against ≥2 joined golden items before snapshotting`);
|
|
110
|
+
const snapshot = {
|
|
111
|
+
...meta,
|
|
112
|
+
metrics: { calibrationKappa: kappa }
|
|
113
|
+
};
|
|
114
|
+
validateSentinelSnapshot(snapshot);
|
|
115
|
+
return snapshot;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* From `corpusInterRaterAgreement` (statistics.ts) or `continuousAgreement`
|
|
119
|
+
* (judge-calibration.ts) output. IRR = ICC(2,1) — the reliability
|
|
120
|
+
* coefficient both instruments compute. For ensemble-level agreement,
|
|
121
|
+
* `meta.judgeId` names the ensemble and `meta.judgeModel` pins its
|
|
122
|
+
* composition (e.g. a joined list of member model strings).
|
|
123
|
+
*/
|
|
124
|
+
function snapshotFromAgreement(report, meta) {
|
|
125
|
+
const irr = "overallIcc" in report ? report.overallIcc : report.icc;
|
|
126
|
+
if (!Number.isFinite(irr)) throw new ValidationError("snapshotFromAgreement: ICC is not finite — agreement needs ≥2 raters and ≥2 complete items");
|
|
127
|
+
const snapshot = {
|
|
128
|
+
...meta,
|
|
129
|
+
metrics: { irr }
|
|
130
|
+
};
|
|
131
|
+
validateSentinelSnapshot(snapshot);
|
|
132
|
+
return snapshot;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* From a rerun of a fixed sentinel golden set: the same `GoldenItem`s the
|
|
136
|
+
* judge was originally calibrated on, re-scored periodically. Pass rate =
|
|
137
|
+
* fraction of joined items where the judge stays within `tolerance` of the
|
|
138
|
+
* human score. Items the judge didn't score are excluded from the
|
|
139
|
+
* denominator; zero joined items is an error, not a 0% pass rate.
|
|
140
|
+
*/
|
|
141
|
+
function snapshotFromSentinelSet(scores, golden, meta, options = {}) {
|
|
142
|
+
const tolerance = options.tolerance ?? .1;
|
|
143
|
+
if (!Number.isFinite(tolerance) || tolerance < 0) throw new ValidationError(`snapshotFromSentinelSet: tolerance must be a finite number ≥ 0, got ${tolerance}`);
|
|
144
|
+
const goldenById = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const item of golden) {
|
|
146
|
+
if (!Number.isFinite(item.humanScore)) throw new ValidationError(`snapshotFromSentinelSet: golden item "${item.itemId}" has a non-finite humanScore`);
|
|
147
|
+
if (goldenById.has(item.itemId)) throw new ValidationError(`snapshotFromSentinelSet: duplicate golden itemId "${item.itemId}"`);
|
|
148
|
+
goldenById.set(item.itemId, item.humanScore);
|
|
149
|
+
}
|
|
150
|
+
let joined = 0;
|
|
151
|
+
let passed = 0;
|
|
152
|
+
for (const s of scores) {
|
|
153
|
+
const human = goldenById.get(s.itemId);
|
|
154
|
+
if (human === void 0) continue;
|
|
155
|
+
if (!Number.isFinite(s.score)) throw new ValidationError(`snapshotFromSentinelSet: judge score for item "${s.itemId}" is not finite`);
|
|
156
|
+
joined++;
|
|
157
|
+
if (Math.abs(s.score - human) <= tolerance) passed++;
|
|
158
|
+
}
|
|
159
|
+
if (joined === 0) throw new ValidationError("snapshotFromSentinelSet: no judge scores joined the golden set — itemIds mismatch or empty sentinel run");
|
|
160
|
+
const snapshot = {
|
|
161
|
+
...meta,
|
|
162
|
+
metrics: { sentinelPassRate: passed / joined }
|
|
163
|
+
};
|
|
164
|
+
validateSentinelSnapshot(snapshot);
|
|
165
|
+
return snapshot;
|
|
166
|
+
}
|
|
167
|
+
function inMemorySentinelStore(initial = []) {
|
|
168
|
+
for (const s of initial) validateSentinelSnapshot(s);
|
|
169
|
+
const items = initial.map((s) => ({
|
|
170
|
+
...s,
|
|
171
|
+
metrics: { ...s.metrics }
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
async append(snapshot) {
|
|
175
|
+
validateSentinelSnapshot(snapshot);
|
|
176
|
+
items.push({
|
|
177
|
+
...snapshot,
|
|
178
|
+
metrics: { ...snapshot.metrics }
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
async history(judgeId) {
|
|
182
|
+
const all = items.map((s) => ({
|
|
183
|
+
...s,
|
|
184
|
+
metrics: { ...s.metrics }
|
|
185
|
+
}));
|
|
186
|
+
return judgeId === void 0 ? all : all.filter((s) => s.judgeId === judgeId);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* JSONL store — one snapshot per line, appended atomically per call.
|
|
192
|
+
* A corrupt or shape-invalid line is a loud error naming the file and
|
|
193
|
+
* line number: a sentinel history that silently drops records would
|
|
194
|
+
* defeat the drift detection it exists to provide.
|
|
195
|
+
*/
|
|
196
|
+
function fileSentinelStore(path) {
|
|
197
|
+
return {
|
|
198
|
+
async append(snapshot) {
|
|
199
|
+
validateSentinelSnapshot(snapshot);
|
|
200
|
+
const fs = await import("node:fs/promises");
|
|
201
|
+
const pathMod = await import("node:path");
|
|
202
|
+
await fs.mkdir(pathMod.dirname(path), { recursive: true });
|
|
203
|
+
await fs.appendFile(path, `${JSON.stringify(snapshot)}\n`, "utf8");
|
|
204
|
+
},
|
|
205
|
+
async history(judgeId) {
|
|
206
|
+
const fs = await import("node:fs/promises");
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = await fs.readFile(path, "utf8");
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (err.code === "ENOENT") return [];
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
const lines = raw.split("\n");
|
|
215
|
+
const snapshots = [];
|
|
216
|
+
for (let i = 0; i < lines.length; i++) {
|
|
217
|
+
const line = lines[i];
|
|
218
|
+
if (line.trim().length === 0) continue;
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(line);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
throw new ValidationError(`judge-sentinel: store at ${path} has a corrupt JSONL line ${i + 1}: ${err.message}`);
|
|
224
|
+
}
|
|
225
|
+
const snapshot = parsed;
|
|
226
|
+
validateSentinelSnapshot(snapshot, `${path}:${i + 1}`);
|
|
227
|
+
snapshots.push(snapshot);
|
|
228
|
+
}
|
|
229
|
+
return judgeId === void 0 ? snapshots : snapshots.filter((s) => s.judgeId === judgeId);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const MS_PER_DAY = 864e5;
|
|
234
|
+
function judgeSentinelReport(history, opts) {
|
|
235
|
+
const asOfMs = parseIso$1(opts.asOf, "asOf");
|
|
236
|
+
const minIrr = opts.thresholds?.minIrr ?? .6;
|
|
237
|
+
const maxKappaDrop = opts.thresholds?.maxKappaDrop ?? .15;
|
|
238
|
+
const maxSentinelDrop = opts.thresholds?.maxSentinelDrop ?? .1;
|
|
239
|
+
const staleAfterDays = opts.thresholds?.staleAfterDays ?? 30;
|
|
240
|
+
const byJudge = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const snapshot of history) {
|
|
242
|
+
validateSentinelSnapshot(snapshot);
|
|
243
|
+
const atMs = parseIso$1(snapshot.at, `snapshot.at (judge "${snapshot.judgeId}")`);
|
|
244
|
+
const arr = byJudge.get(snapshot.judgeId) ?? [];
|
|
245
|
+
arr.push({
|
|
246
|
+
...snapshot,
|
|
247
|
+
atMs
|
|
248
|
+
});
|
|
249
|
+
byJudge.set(snapshot.judgeId, arr);
|
|
250
|
+
}
|
|
251
|
+
const perJudge = [];
|
|
252
|
+
const alarms = [];
|
|
253
|
+
const insufficientHistory = [];
|
|
254
|
+
for (const judgeId of [...byJudge.keys()].sort()) {
|
|
255
|
+
const snaps = byJudge.get(judgeId).sort((a, b) => a.atMs - b.atMs);
|
|
256
|
+
const newest = snaps[snaps.length - 1];
|
|
257
|
+
const ageDays = (asOfMs - newest.atMs) / MS_PER_DAY;
|
|
258
|
+
if (ageDays > staleAfterDays) alarms.push(`judge "${judgeId}": stale — newest snapshot ${newest.at} is ${ageDays.toFixed(1)}d old vs asOf (limit ${staleAfterDays}d)`);
|
|
259
|
+
let changeIdx = -1;
|
|
260
|
+
for (let i = 1; i < snaps.length; i++) if (snaps[i].judgeModel !== snaps[i - 1].judgeModel) changeIdx = i;
|
|
261
|
+
if (changeIdx >= 0) {
|
|
262
|
+
if (!snaps.slice(changeIdx).some((s) => Number.isFinite(s.metrics.calibrationKappa) || Number.isFinite(s.metrics.sentinelPassRate))) alarms.push(`judge "${judgeId}": model changed "${snaps[changeIdx - 1].judgeModel}" → "${snaps[changeIdx].judgeModel}" at ${snaps[changeIdx].at} with no post-change calibration snapshot — silent-upgrade trap; rerun calibrateJudge or the sentinel set against the new model`);
|
|
263
|
+
}
|
|
264
|
+
for (const metric of SENTINEL_METRIC_NAMES) {
|
|
265
|
+
const values = snaps.filter((s) => typeof s.metrics[metric] === "number").map((s) => s.metrics[metric]);
|
|
266
|
+
if (values.length === 0) continue;
|
|
267
|
+
const convergence = analyzeSeries(values, opts.convergence);
|
|
268
|
+
if (convergence.state === "insufficient-data") insufficientHistory.push(`${judgeId}:${metric}`);
|
|
269
|
+
const current = values[values.length - 1];
|
|
270
|
+
const baseline = values[0];
|
|
271
|
+
const drift = current - baseline;
|
|
272
|
+
const reasons = [];
|
|
273
|
+
if (convergence.state === "drifting-down") reasons.push(`convergence state drifting-down (tail run ${convergence.tailRun}, window mean ${convergence.windowMean.toFixed(3)})`);
|
|
274
|
+
if (metric === "irr" && current < minIrr) reasons.push(`irr ${current.toFixed(3)} below floor ${minIrr}`);
|
|
275
|
+
if (metric === "calibrationKappa" && baseline - current > maxKappaDrop) reasons.push(`calibrationKappa dropped ${(baseline - current).toFixed(3)} from baseline ${baseline.toFixed(3)} (limit ${maxKappaDrop})`);
|
|
276
|
+
if (metric === "sentinelPassRate" && baseline - current > maxSentinelDrop) reasons.push(`sentinelPassRate dropped ${(baseline - current).toFixed(3)} from baseline ${baseline.toFixed(3)} (limit ${maxSentinelDrop})`);
|
|
277
|
+
const alarmed = reasons.length > 0;
|
|
278
|
+
const trend = {
|
|
279
|
+
judgeId,
|
|
280
|
+
metric,
|
|
281
|
+
state: convergence.state,
|
|
282
|
+
current,
|
|
283
|
+
baseline,
|
|
284
|
+
drift,
|
|
285
|
+
alarmed
|
|
286
|
+
};
|
|
287
|
+
if (alarmed) {
|
|
288
|
+
trend.reason = reasons.join("; ");
|
|
289
|
+
alarms.push(`judge "${judgeId}" ${metric}: ${trend.reason}`);
|
|
290
|
+
}
|
|
291
|
+
perJudge.push(trend);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
perJudge,
|
|
296
|
+
alarms,
|
|
297
|
+
healthy: alarms.length === 0,
|
|
298
|
+
insufficientHistory
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The object a campaign attaches to its verdicts. Compute it from the
|
|
303
|
+
* sentinel report in a post-campaign hook (or a nightly job feeding the
|
|
304
|
+
* next day's campaigns) and store it alongside the verdict payload.
|
|
305
|
+
* `healthy: false` means the judges that produced those verdicts have an
|
|
306
|
+
* unresolved drift / decay / silent-upgrade / staleness alarm — treat the
|
|
307
|
+
* verdicts as carrying an integrity warning until recalibration clears it.
|
|
308
|
+
*/
|
|
309
|
+
function evalHealthStamp(report) {
|
|
310
|
+
return {
|
|
311
|
+
healthy: report.healthy,
|
|
312
|
+
alarms: [...report.alarms]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/meta-eval/judge-gate.ts
|
|
317
|
+
/**
|
|
318
|
+
* Judge calibration gate: a judge may gate only while its measured agreement
|
|
319
|
+
* with its owner's verdicts holds.
|
|
320
|
+
*
|
|
321
|
+
* A judge (a model judge or a deterministic grader) registers a calibration
|
|
322
|
+
* set: items its owner has decided, each verdict with its provenance. A
|
|
323
|
+
* calibration run replays the judge on those items, once or several times, and
|
|
324
|
+
* `measureJudgeAgreement` reports how often it reached the owner's verdict
|
|
325
|
+
* (accuracy, and Cohen's κ for the pass/fail pair), how much its own verdict
|
|
326
|
+
* moves between runs and between samples, and where it disagreed.
|
|
327
|
+
*
|
|
328
|
+
* `judgeGateDecision` turns that report into the one decision a consumer acts
|
|
329
|
+
* on. It refuses when the judge has no measured agreement, too few decided
|
|
330
|
+
* items of either verdict, a report measured on a different model or rubric
|
|
331
|
+
* than the one the judge now runs, a report older than the policy allows,
|
|
332
|
+
* agreement below the policy floors, or an alarmed drift history. A refused
|
|
333
|
+
* judge's verdict is advisory: the consumer reports it but does not let it
|
|
334
|
+
* pass or block anything.
|
|
335
|
+
*
|
|
336
|
+
* Drift is the judge sentinel (`./sentinel`): `snapshotFromJudgeAgreement`
|
|
337
|
+
* records accuracy as `sentinelPassRate` and κ as `calibrationKappa`, so each
|
|
338
|
+
* calibration run appends one snapshot to a `SentinelStore` and the gate reads
|
|
339
|
+
* the alarms `judgeSentinelReport` raises over that history.
|
|
340
|
+
*
|
|
341
|
+
* Every timestamp is caller-supplied ISO-8601, so decisions are reproducible.
|
|
342
|
+
*/
|
|
343
|
+
function parseIso(value, label) {
|
|
344
|
+
const ms = typeof value === "string" ? Date.parse(value) : NaN;
|
|
345
|
+
if (!Number.isFinite(ms)) throw new ValidationError(`judge-gate: ${label} is not a parseable ISO timestamp: ${JSON.stringify(value)}`);
|
|
346
|
+
return ms;
|
|
347
|
+
}
|
|
348
|
+
function requireText(value, label) {
|
|
349
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new ValidationError(`judge-gate: ${label} must be a non-empty string`);
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
function requireVerdict(value, label) {
|
|
353
|
+
if (value !== "pass" && value !== "fail") throw new ValidationError(`judge-gate: ${label} must be 'pass' or 'fail', got ${JSON.stringify(value)}`);
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function checkExample(example, label) {
|
|
357
|
+
if (example === null || typeof example !== "object") throw new ValidationError(`judge-gate: ${label} must be an object`);
|
|
358
|
+
requireText(example.id, `${label}.id`);
|
|
359
|
+
requireVerdict(example.verdict, `${label}.verdict`);
|
|
360
|
+
requireText(example.labeledBy, `${label}.labeledBy`);
|
|
361
|
+
parseIso(example.labeledAt, `${label}.labeledAt`);
|
|
362
|
+
requireText(example.source, `${label}.source`);
|
|
363
|
+
if (example.evidenceRef !== void 0) requireText(example.evidenceRef, `${label}.evidenceRef`);
|
|
364
|
+
const copy = {
|
|
365
|
+
id: example.id,
|
|
366
|
+
verdict: example.verdict,
|
|
367
|
+
labeledBy: example.labeledBy,
|
|
368
|
+
labeledAt: example.labeledAt,
|
|
369
|
+
source: example.source
|
|
370
|
+
};
|
|
371
|
+
if (example.evidenceRef !== void 0) copy.evidenceRef = example.evidenceRef;
|
|
372
|
+
if (example.note !== void 0) copy.note = example.note;
|
|
373
|
+
return copy;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Validate a calibration set and return a normalized copy ordered by example
|
|
377
|
+
* id. Every verdict needs its person, time and source; ids are unique.
|
|
378
|
+
*/
|
|
379
|
+
function registerCalibrationSet(set) {
|
|
380
|
+
if (set === null || typeof set !== "object") throw new ValidationError("judge-gate: calibration set must be an object");
|
|
381
|
+
const judgeId = requireText(set.judgeId, "calibration set judgeId");
|
|
382
|
+
const owner = requireText(set.owner, `calibration set "${judgeId}" owner`);
|
|
383
|
+
if (!Array.isArray(set.examples)) throw new ValidationError(`judge-gate: calibration set "${judgeId}" examples must be an array`);
|
|
384
|
+
const seen = /* @__PURE__ */ new Set();
|
|
385
|
+
const examples = set.examples.map((example, index) => {
|
|
386
|
+
const checked = checkExample(example, `calibration set "${judgeId}" example ${index}`);
|
|
387
|
+
if (seen.has(checked.id)) throw new ValidationError(`judge-gate: calibration set "${judgeId}" has duplicate example id "${checked.id}"`);
|
|
388
|
+
seen.add(checked.id);
|
|
389
|
+
return checked;
|
|
390
|
+
});
|
|
391
|
+
examples.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
392
|
+
return {
|
|
393
|
+
judgeId,
|
|
394
|
+
owner,
|
|
395
|
+
examples
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Join new owner verdicts to a set. A verdict already recorded from the same
|
|
400
|
+
* source is unchanged, so ingestion can rerun. A later verdict on an item
|
|
401
|
+
* replaces an earlier one: the owner changed their mind, and the set keeps
|
|
402
|
+
* what they decided last.
|
|
403
|
+
*/
|
|
404
|
+
function addCalibrationVerdicts(set, verdicts) {
|
|
405
|
+
const base = registerCalibrationSet(set);
|
|
406
|
+
const byId = new Map(base.examples.map((example) => [example.id, example]));
|
|
407
|
+
const added = [];
|
|
408
|
+
const superseded = [];
|
|
409
|
+
const unchanged = [];
|
|
410
|
+
for (const [index, verdict] of verdicts.entries()) {
|
|
411
|
+
const incoming = checkExample(verdict, `incoming verdict ${index} for "${base.judgeId}"`);
|
|
412
|
+
const current = byId.get(incoming.id);
|
|
413
|
+
if (!current) {
|
|
414
|
+
byId.set(incoming.id, incoming);
|
|
415
|
+
added.push(incoming.id);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (current.source === incoming.source && current.verdict === incoming.verdict || parseIso(incoming.labeledAt, "labeledAt") <= parseIso(current.labeledAt, "labeledAt")) {
|
|
419
|
+
unchanged.push(incoming.id);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
byId.set(incoming.id, incoming);
|
|
423
|
+
superseded.push(incoming.id);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
set: registerCalibrationSet({
|
|
427
|
+
...base,
|
|
428
|
+
examples: [...byId.values()]
|
|
429
|
+
}),
|
|
430
|
+
added,
|
|
431
|
+
superseded,
|
|
432
|
+
unchanged
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function bernoulliVariance(passes, n) {
|
|
436
|
+
const p = passes / n;
|
|
437
|
+
return p * (1 - p);
|
|
438
|
+
}
|
|
439
|
+
/** Cohen's κ for two raters over a binary pass/fail table. */
|
|
440
|
+
function binaryKappa(confusion) {
|
|
441
|
+
const n = confusion.truePass + confusion.trueFail + confusion.falsePass + confusion.falseFail;
|
|
442
|
+
if (n === 0) return null;
|
|
443
|
+
const observed = (confusion.truePass + confusion.trueFail) / n;
|
|
444
|
+
const judgePass = (confusion.truePass + confusion.falsePass) / n;
|
|
445
|
+
const ownerPass = (confusion.truePass + confusion.falseFail) / n;
|
|
446
|
+
const expected = judgePass * ownerPass + (1 - judgePass) * (1 - ownerPass);
|
|
447
|
+
if (expected === 1) return null;
|
|
448
|
+
return (observed - expected) / (1 - expected);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Agreement between a judge's runs and the owner's verdicts on one
|
|
452
|
+
* calibration set. A run that names an example outside the set is an error:
|
|
453
|
+
* a silently dropped run would overstate agreement.
|
|
454
|
+
*/
|
|
455
|
+
function measureJudgeAgreement(set, runs, meta) {
|
|
456
|
+
const registered = registerCalibrationSet(set);
|
|
457
|
+
if (meta.judgeId !== registered.judgeId) throw new ValidationError(`judge-gate: runs are for judge "${meta.judgeId}" but the set belongs to "${registered.judgeId}"`);
|
|
458
|
+
requireText(meta.judgeModel, "judgeModel");
|
|
459
|
+
requireText(meta.rubricVersion, "rubricVersion");
|
|
460
|
+
parseIso(meta.measuredAt, "measuredAt");
|
|
461
|
+
const byId = new Map(registered.examples.map((example) => [example.id, example]));
|
|
462
|
+
const runsById = /* @__PURE__ */ new Map();
|
|
463
|
+
const confusion = {
|
|
464
|
+
truePass: 0,
|
|
465
|
+
trueFail: 0,
|
|
466
|
+
falsePass: 0,
|
|
467
|
+
falseFail: 0
|
|
468
|
+
};
|
|
469
|
+
const withinRun = [];
|
|
470
|
+
for (const [index, run] of runs.entries()) {
|
|
471
|
+
const example = byId.get(run?.exampleId);
|
|
472
|
+
if (!example) throw new ValidationError(`judge-gate: run ${index} names example ${JSON.stringify(run?.exampleId)}, which is not in "${registered.judgeId}"'s calibration set`);
|
|
473
|
+
const verdict = requireVerdict(run.verdict, `run ${index} verdict`);
|
|
474
|
+
if (run.samples !== void 0) {
|
|
475
|
+
if (!Array.isArray(run.samples) || run.samples.length === 0) throw new ValidationError(`judge-gate: run ${index} samples must be a non-empty array`);
|
|
476
|
+
const samples = run.samples.map((sample, s) => requireVerdict(sample, `run ${index} sample ${s}`));
|
|
477
|
+
withinRun.push(bernoulliVariance(samples.filter((sample) => sample === "pass").length, samples.length));
|
|
478
|
+
}
|
|
479
|
+
const list = runsById.get(example.id) ?? [];
|
|
480
|
+
list.push({
|
|
481
|
+
...run,
|
|
482
|
+
verdict
|
|
483
|
+
});
|
|
484
|
+
runsById.set(example.id, list);
|
|
485
|
+
if (verdict === "pass") {
|
|
486
|
+
if (example.verdict === "pass") confusion.truePass++;
|
|
487
|
+
else confusion.falsePass++;
|
|
488
|
+
} else if (example.verdict === "fail") confusion.trueFail++;
|
|
489
|
+
else confusion.falseFail++;
|
|
490
|
+
}
|
|
491
|
+
const perExample = [];
|
|
492
|
+
const unjudged = [];
|
|
493
|
+
const judgedByVerdict = {
|
|
494
|
+
pass: 0,
|
|
495
|
+
fail: 0
|
|
496
|
+
};
|
|
497
|
+
for (const example of registered.examples) {
|
|
498
|
+
const list = runsById.get(example.id);
|
|
499
|
+
if (!list) {
|
|
500
|
+
unjudged.push(example.id);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
judgedByVerdict[example.verdict]++;
|
|
504
|
+
const passes = list.filter((run) => run.verdict === "pass").length;
|
|
505
|
+
perExample.push({
|
|
506
|
+
id: example.id,
|
|
507
|
+
verdict: example.verdict,
|
|
508
|
+
runs: list.length,
|
|
509
|
+
passRate: passes / list.length,
|
|
510
|
+
agreement: list.filter((run) => run.verdict === example.verdict).length / list.length
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const total = runs.length;
|
|
514
|
+
const agreedRuns = confusion.truePass + confusion.trueFail;
|
|
515
|
+
const agreedExamples = perExample.filter((example) => example.agreement === 1).length;
|
|
516
|
+
const mean = (values) => values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
517
|
+
return {
|
|
518
|
+
...meta,
|
|
519
|
+
owner: registered.owner,
|
|
520
|
+
examples: registered.examples.length,
|
|
521
|
+
judged: perExample.length,
|
|
522
|
+
judgedByVerdict,
|
|
523
|
+
runs: total,
|
|
524
|
+
accuracy: total === 0 ? null : agreedRuns / total,
|
|
525
|
+
exampleAccuracy: perExample.length === 0 ? null : {
|
|
526
|
+
agreed: agreedExamples,
|
|
527
|
+
judged: perExample.length,
|
|
528
|
+
interval: computeInterval({
|
|
529
|
+
kind: "clopper-pearson",
|
|
530
|
+
level: .95
|
|
531
|
+
}, {
|
|
532
|
+
kind: "binomial",
|
|
533
|
+
successes: agreedExamples,
|
|
534
|
+
trials: perExample.length
|
|
535
|
+
})
|
|
536
|
+
},
|
|
537
|
+
kappa: binaryKappa(confusion),
|
|
538
|
+
confusion,
|
|
539
|
+
variance: {
|
|
540
|
+
betweenRuns: mean(perExample.map((example) => example.passRate * (1 - example.passRate))),
|
|
541
|
+
withinRun: mean(withinRun),
|
|
542
|
+
unstable: perExample.filter((example) => example.passRate > 0 && example.passRate < 1).map((example) => example.id)
|
|
543
|
+
},
|
|
544
|
+
disagreements: perExample.filter((example) => example.agreement < 1).sort((a, b) => a.agreement - b.agreement || (a.id < b.id ? -1 : 1)),
|
|
545
|
+
unjudged
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const DEFAULT_JUDGE_GATE_POLICY = Object.freeze({
|
|
549
|
+
minExamples: 10,
|
|
550
|
+
minPerVerdict: 1,
|
|
551
|
+
minAccuracy: .9,
|
|
552
|
+
minKappa: .6,
|
|
553
|
+
maxAgeDays: 30
|
|
554
|
+
});
|
|
555
|
+
const DAY_MS = 864e5;
|
|
556
|
+
function percent(value) {
|
|
557
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
558
|
+
}
|
|
559
|
+
/** Whether a judge may gate now, and every reason it may not. */
|
|
560
|
+
function judgeGateDecision(input) {
|
|
561
|
+
const judgeId = requireText(input.judgeId, "judgeId");
|
|
562
|
+
const judgeModel = requireText(input.judgeModel, "judgeModel");
|
|
563
|
+
const rubricVersion = requireText(input.rubricVersion, "rubricVersion");
|
|
564
|
+
const asOf = parseIso(input.asOf, "asOf");
|
|
565
|
+
const policy = {
|
|
566
|
+
...DEFAULT_JUDGE_GATE_POLICY,
|
|
567
|
+
...input.policy
|
|
568
|
+
};
|
|
569
|
+
const agreement = input.agreement;
|
|
570
|
+
const decide = (status, reasons) => ({
|
|
571
|
+
judgeId,
|
|
572
|
+
judgeModel,
|
|
573
|
+
rubricVersion,
|
|
574
|
+
mayGate: status === "calibrated",
|
|
575
|
+
status,
|
|
576
|
+
reasons,
|
|
577
|
+
agreement,
|
|
578
|
+
policy
|
|
579
|
+
});
|
|
580
|
+
if (!agreement) return decide("uncalibrated", ["no measured agreement with owner verdicts"]);
|
|
581
|
+
if (agreement.judgeId !== judgeId) throw new ValidationError(`judge-gate: agreement belongs to "${agreement.judgeId}", not "${judgeId}"`);
|
|
582
|
+
const uncalibrated = [];
|
|
583
|
+
if (agreement.judgeModel !== judgeModel) uncalibrated.push(`agreement was measured on model ${agreement.judgeModel}; the judge now runs ${judgeModel}`);
|
|
584
|
+
if (agreement.rubricVersion !== rubricVersion) uncalibrated.push(`agreement was measured on rubric ${agreement.rubricVersion}; the judge now runs ${rubricVersion}`);
|
|
585
|
+
const ageDays = (asOf - parseIso(agreement.measuredAt, "agreement.measuredAt")) / DAY_MS;
|
|
586
|
+
if (ageDays > policy.maxAgeDays) uncalibrated.push(`agreement is ${ageDays.toFixed(1)} days old (limit ${policy.maxAgeDays})`);
|
|
587
|
+
if (agreement.judged < policy.minExamples) uncalibrated.push(`${agreement.judged} judged ${agreement.judged === 1 ? "example" : "examples"}; at least ${policy.minExamples} required`);
|
|
588
|
+
for (const verdict of ["pass", "fail"]) if (agreement.judgedByVerdict[verdict] < policy.minPerVerdict) uncalibrated.push(`${agreement.judgedByVerdict[verdict]} judged examples the owner marked ${verdict}; at least ${policy.minPerVerdict} required`);
|
|
589
|
+
if (uncalibrated.length > 0) return decide("uncalibrated", uncalibrated);
|
|
590
|
+
const below = [];
|
|
591
|
+
if (agreement.accuracy === null || agreement.accuracy < policy.minAccuracy) below.push(`accuracy ${agreement.accuracy === null ? "unmeasured" : percent(agreement.accuracy)} is below ${percent(policy.minAccuracy)}`);
|
|
592
|
+
if (agreement.kappa === null || agreement.kappa < policy.minKappa) below.push(`kappa ${agreement.kappa === null ? "undefined" : agreement.kappa.toFixed(2)} is below ${policy.minKappa}`);
|
|
593
|
+
if (below.length > 0) return decide("below-threshold", below);
|
|
594
|
+
const history = (input.history ?? []).filter((snapshot) => snapshot.judgeId === judgeId);
|
|
595
|
+
if (history.length > 0) {
|
|
596
|
+
const report = judgeSentinelReport([...history], {
|
|
597
|
+
asOf: input.asOf,
|
|
598
|
+
...policy.drift ? { thresholds: policy.drift } : {}
|
|
599
|
+
});
|
|
600
|
+
if (!report.healthy) return decide("drifted", report.alarms);
|
|
601
|
+
}
|
|
602
|
+
return decide("calibrated", []);
|
|
603
|
+
}
|
|
604
|
+
/** Thrown by `assertJudgeMayGate`; carries the refused decision. */
|
|
605
|
+
var JudgeGateRefusedError = class extends JudgeError {
|
|
606
|
+
decision;
|
|
607
|
+
constructor(decision) {
|
|
608
|
+
super(`judge "${decision.judgeId}" may not gate (${decision.status}): ${decision.reasons.join("; ")}`);
|
|
609
|
+
this.decision = decision;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
/** Throw unless the decision lets the judge gate. */
|
|
613
|
+
function assertJudgeMayGate(decision) {
|
|
614
|
+
if (!decision.mayGate) throw new JudgeGateRefusedError(decision);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The sentinel snapshot for one calibration run: accuracy as
|
|
618
|
+
* `sentinelPassRate`, κ as `calibrationKappa` when it is defined. Append it to
|
|
619
|
+
* the judge's `SentinelStore`; the gate reads drift from that history.
|
|
620
|
+
*/
|
|
621
|
+
function snapshotFromJudgeAgreement(agreement) {
|
|
622
|
+
if (agreement.accuracy === null) throw new ValidationError(`snapshotFromJudgeAgreement: judge "${agreement.judgeId}" has no runs to snapshot`);
|
|
623
|
+
const snapshot = {
|
|
624
|
+
at: agreement.measuredAt,
|
|
625
|
+
judgeId: agreement.judgeId,
|
|
626
|
+
judgeModel: agreement.judgeModel,
|
|
627
|
+
metrics: {
|
|
628
|
+
sentinelPassRate: agreement.accuracy,
|
|
629
|
+
...agreement.kappa === null ? {} : { calibrationKappa: agreement.kappa }
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
validateSentinelSnapshot(snapshot);
|
|
633
|
+
return snapshot;
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
export { analyzeSeries as _, judgeGateDecision as a, snapshotFromJudgeAgreement as c, inMemorySentinelStore as d, judgeSentinelReport as f, validateSentinelSnapshot as g, snapshotFromSentinelSet as h, assertJudgeMayGate as i, evalHealthStamp as l, snapshotFromCalibration as m, JudgeGateRefusedError as n, measureJudgeAgreement as o, snapshotFromAgreement as p, addCalibrationVerdicts as r, registerCalibrationSet as s, DEFAULT_JUDGE_GATE_POLICY as t, fileSentinelStore as u };
|
|
637
|
+
|
|
638
|
+
//# sourceMappingURL=judge-gate-C59znGmR.js.map
|