modelrig 0.6.0 → 0.8.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 +123 -0
- package/README.md +65 -1
- package/dist/artifacts/run-context.d.ts +10 -0
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +12 -0
- package/dist/bakeoff/disagreement.d.ts +93 -0
- package/dist/bakeoff/disagreement.d.ts.map +1 -0
- package/dist/bakeoff/disagreement.js +140 -0
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +58 -2
- package/dist/bakeoff/pairwise.d.ts +102 -0
- package/dist/bakeoff/pairwise.d.ts.map +1 -0
- package/dist/bakeoff/pairwise.js +214 -0
- package/dist/bakeoff/replay.d.ts +6 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +6 -2
- package/dist/bakeoff/report.d.ts +7 -0
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +46 -0
- package/dist/bakeoff/types.d.ts +24 -0
- package/dist/bakeoff/types.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -9
- package/dist/mask/apply.d.ts +80 -0
- package/dist/mask/apply.d.ts.map +1 -0
- package/dist/mask/apply.js +228 -0
- package/dist/mask/detect.d.ts +38 -0
- package/dist/mask/detect.d.ts.map +1 -0
- package/dist/mask/detect.js +167 -0
- package/dist/mask/index.d.ts +21 -0
- package/dist/mask/index.d.ts.map +1 -0
- package/dist/mask/index.js +54 -0
- package/dist/mask/mask-policy-cli.d.ts +11 -0
- package/dist/mask/mask-policy-cli.d.ts.map +1 -0
- package/dist/mask/mask-policy-cli.js +33 -0
- package/dist/mask/policy.d.ts +38 -0
- package/dist/mask/policy.d.ts.map +1 -0
- package/dist/mask/policy.js +187 -0
- package/dist/mask/proof.d.ts +54 -0
- package/dist/mask/proof.d.ts.map +1 -0
- package/dist/mask/proof.js +120 -0
- package/dist/mask/receipt.d.ts +30 -0
- package/dist/mask/receipt.d.ts.map +1 -0
- package/dist/mask/receipt.js +49 -0
- package/dist/mask/seam.d.ts +53 -0
- package/dist/mask/seam.d.ts.map +1 -0
- package/dist/mask/seam.js +120 -0
- package/dist/mask/types.d.ts +81 -0
- package/dist/mask/types.d.ts.map +1 -0
- package/dist/mask/types.js +10 -0
- package/dist/mask/vault.d.ts +50 -0
- package/dist/mask/vault.d.ts.map +1 -0
- package/dist/mask/vault.js +81 -0
- package/dist/receipts/types.d.ts +65 -0
- package/dist/receipts/types.d.ts.map +1 -0
- package/dist/receipts/types.js +134 -0
- package/dist/routes/loader-warnings.d.ts +8 -0
- package/dist/routes/loader-warnings.d.ts.map +1 -1
- package/dist/routes/loader-warnings.js +23 -0
- package/dist/routes/loader.d.ts +1 -1
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +24 -25
- package/dist/routes/mask-loader.d.ts +18 -0
- package/dist/routes/mask-loader.d.ts.map +1 -0
- package/dist/routes/mask-loader.js +39 -0
- package/dist/routes/objective-loader.d.ts.map +1 -1
- package/dist/routes/objective-loader.js +51 -17
- package/dist/routes/screen-loader.d.ts +23 -0
- package/dist/routes/screen-loader.d.ts.map +1 -0
- package/dist/routes/screen-loader.js +79 -0
- package/dist/routes/template.d.ts +10 -0
- package/dist/routes/template.d.ts.map +1 -1
- package/dist/routes/template.js +10 -3
- package/dist/run/accept.d.ts +4 -0
- package/dist/run/accept.d.ts.map +1 -1
- package/dist/run/accept.js +2 -0
- package/dist/run/dispatch.d.ts +35 -0
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +33 -0
- package/dist/run/grounding.d.ts +47 -2
- package/dist/run/grounding.d.ts.map +1 -1
- package/dist/run/grounding.js +113 -6
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +10 -3
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +12 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +25 -19
- package/dist/screen/provenance.d.ts +59 -0
- package/dist/screen/provenance.d.ts.map +1 -0
- package/dist/screen/provenance.js +104 -0
- package/dist/screen/types.d.ts +71 -0
- package/dist/screen/types.d.ts.map +1 -0
- package/dist/screen/types.js +9 -0
- package/dist/status-cli.d.ts +11 -0
- package/dist/status-cli.d.ts.map +1 -0
- package/dist/status-cli.js +21 -0
- package/dist/status-runs.d.ts +38 -0
- package/dist/status-runs.d.ts.map +1 -0
- package/dist/status-runs.js +80 -0
- package/dist/status.d.ts +7 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -2
- package/dist/telemetry/ingest-protocol.d.ts +8 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +11 -0
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +12 -3
- package/dist/telemetry/supabase-exporter.d.ts +64 -0
- package/dist/telemetry/supabase-exporter.d.ts.map +1 -0
- package/dist/telemetry/supabase-exporter.js +211 -0
- package/dist/types/provider.d.ts +11 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +9 -0
- package/dist/types/route.d.ts +55 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +7 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +13 -0
- package/dist/verify/types.d.ts +6 -0
- package/dist/verify/types.d.ts.map +1 -1
- package/docs/agent-skill/SKILL.md +131 -0
- package/docs/how-it-fits.md +34 -0
- package/docs/instrumentation.md +221 -0
- package/docs/llms.txt +276 -0
- package/docs/migration-playbook.md +512 -0
- package/docs/quickstart.md +333 -0
- package/package.json +18 -2
- package/receipts.d.ts +12 -0
package/dist/bakeoff/engine.js
CHANGED
|
@@ -22,6 +22,8 @@ const loader_1 = require("../routes/loader");
|
|
|
22
22
|
const replay_1 = require("./replay");
|
|
23
23
|
const stats_1 = require("./stats");
|
|
24
24
|
const judge_1 = require("./judge");
|
|
25
|
+
const pairwise_1 = require("./pairwise");
|
|
26
|
+
const disagreement_1 = require("./disagreement");
|
|
25
27
|
const reference_1 = require("./reference");
|
|
26
28
|
const samples_1 = require("./samples");
|
|
27
29
|
/** The arm name carrying the reference (ceiling) metrics. Reserved — a user
|
|
@@ -75,6 +77,10 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
75
77
|
const keepOutputs = req.keepOutputs !== undefined && samplesSink !== undefined;
|
|
76
78
|
const bakeoffId = (0, node_crypto_1.randomUUID)();
|
|
77
79
|
const judgeCfg = route.bakeoff?.judge;
|
|
80
|
+
// N5: a declared pairwise judge needs each sample's output retained (arm-vs-
|
|
81
|
+
// reference comparison in the engine) — WITHOUT the keepOutputs sink persist.
|
|
82
|
+
const pairwiseJudgeCfg = route.bakeoff?.pairwise?.judge;
|
|
83
|
+
const retainForPairwise = pairwiseJudgeCfg !== undefined;
|
|
78
84
|
// Replay every requested arm. Raw samples are kept ALWAYS (paired regret is
|
|
79
85
|
// computed from per-sample quality); the keepOutputs sink is separate. A fresh
|
|
80
86
|
// judge scorer per arm keeps `max_samples` per-arm (spec §4.3).
|
|
@@ -88,6 +94,7 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
88
94
|
lastN: req.replayLastN,
|
|
89
95
|
envelopeUsd: req.envelopeUsdPerVariant,
|
|
90
96
|
...(keepOutputs ? { keepOutputs: true } : {}),
|
|
97
|
+
...(retainForPairwise ? { retainOutputForPairwise: true } : {}),
|
|
91
98
|
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
92
99
|
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
93
100
|
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
@@ -101,6 +108,10 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
101
108
|
// arm's paired regret vs it. Partial-result honesty is preserved.
|
|
102
109
|
const referenceInfo = resolveReference(route, req);
|
|
103
110
|
let reference = null;
|
|
111
|
+
// N6: pairwise verdicts keyed `${arm} ${captureId}` (a mining input) + the
|
|
112
|
+
// reference samples, hoisted so disagreement mining sees them after the block.
|
|
113
|
+
const pairwiseOutcomes = new Map();
|
|
114
|
+
let refSamplesForMining = [];
|
|
104
115
|
if (referenceInfo !== null) {
|
|
105
116
|
const { deps: refDeps } = withReferenceArm(deps, route, referenceInfo.candidate);
|
|
106
117
|
const refEnvelope = route.bakeoff?.referenceEnvelopeUsd ?? req.envelopeUsdPerVariant;
|
|
@@ -109,16 +120,26 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
109
120
|
variant: exports.REFERENCE_ARM,
|
|
110
121
|
lastN: req.replayLastN,
|
|
111
122
|
envelopeUsd: refEnvelope,
|
|
123
|
+
...(retainForPairwise ? { retainOutputForPairwise: true } : {}),
|
|
112
124
|
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
113
125
|
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
114
126
|
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
115
127
|
});
|
|
116
128
|
const partial = refSamples.length < replayN || refSamples.some((s) => s.failureClass === "budget_exhausted");
|
|
129
|
+
refSamplesForMining = refSamples;
|
|
117
130
|
for (let i = 0; i < metrics.length; i += 1) {
|
|
118
131
|
const raw = rawByArm.get(metrics[i].variant) ?? [];
|
|
132
|
+
// N5: pairwise standing vs the reference — deterministic where both sides
|
|
133
|
+
// carry value accuracy, else the opt-in capped judge (fresh per arm, so
|
|
134
|
+
// maxSamples is per-arm). A REPORTED signal beside regret.
|
|
135
|
+
const pwJudge = pairwiseJudgeCfg !== undefined ? (0, pairwise_1.makePairwiseJudge)(deps, route, pairwiseJudgeCfg) : null;
|
|
136
|
+
const { stats: pairwise, outcomes } = await (0, pairwise_1.scoreArmPairwise)(raw, refSamples, pwJudge);
|
|
137
|
+
for (const [captureId, outcome] of outcomes)
|
|
138
|
+
pairwiseOutcomes.set((0, disagreement_1.pairwiseKey)(metrics[i].variant, captureId), outcome);
|
|
119
139
|
metrics[i] = {
|
|
120
140
|
...metrics[i],
|
|
121
141
|
regret: (0, stats_1.pairedRegret)(raw, refSamples),
|
|
142
|
+
...(pairwise !== null ? { pairwise } : {}),
|
|
122
143
|
...(partial ? { referencePartial: true } : {}),
|
|
123
144
|
};
|
|
124
145
|
}
|
|
@@ -181,6 +202,15 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
181
202
|
"re-run with --keep-outputs to retry the samples.");
|
|
182
203
|
}
|
|
183
204
|
}
|
|
205
|
+
// N6: mine the replay/verdict set for disagreements → ranked eval-case
|
|
206
|
+
// candidates (never auto-promoted). Pure over the per-arm raw samples + the
|
|
207
|
+
// reference + pairwise verdicts. Rides the LOCAL artifact only — captureIds are
|
|
208
|
+
// pointers to already-captured inputs and are never mirrored.
|
|
209
|
+
const disagreement = (0, disagreement_1.mineDisagreements)({
|
|
210
|
+
arms: req.variants.map((variant) => ({ arm: variant, samples: rawByArm.get(variant) ?? [] })),
|
|
211
|
+
...(refSamplesForMining.length > 0 ? { reference: refSamplesForMining } : {}),
|
|
212
|
+
...(pairwiseOutcomes.size > 0 ? { pairwise: pairwiseOutcomes } : {}),
|
|
213
|
+
});
|
|
184
214
|
return {
|
|
185
215
|
id: bakeoffId,
|
|
186
216
|
route: route.route,
|
|
@@ -192,6 +222,7 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
192
222
|
objective,
|
|
193
223
|
reference,
|
|
194
224
|
qualityMetric,
|
|
225
|
+
disagreement,
|
|
195
226
|
};
|
|
196
227
|
}
|
|
197
228
|
function isDeclared(route, candidate) {
|
|
@@ -224,6 +255,7 @@ function resolveObjective(route, req) {
|
|
|
224
255
|
maxRegret: route.objective?.maxRegret ?? null,
|
|
225
256
|
priceCeiling: route.objective?.priceCeiling ?? null,
|
|
226
257
|
lambda: route.objective?.lambda ?? null,
|
|
258
|
+
...(route.objective?.pairwiseAware === true ? { pairwiseAware: true } : {}),
|
|
227
259
|
};
|
|
228
260
|
}
|
|
229
261
|
const EPS = 1e-9;
|
|
@@ -233,6 +265,16 @@ function savingsPct(incumbent, winner) {
|
|
|
233
265
|
? 0
|
|
234
266
|
: ((inc - winner.effectiveUsdPer1kConformant) / inc) * 100;
|
|
235
267
|
}
|
|
268
|
+
/** N5 pairwise-aware gate clause (default off). When `objective.pairwiseAware` is
|
|
269
|
+
* set, an eligible arm must NOT be pairwise-worse than the reference within CI —
|
|
270
|
+
* its pairwise win-rate lower bound ≥ 0.5. Fail-closed: an arm with NO pairwise
|
|
271
|
+
* stats cannot be confirmed not-worse, so it is ineligible under the knob. When
|
|
272
|
+
* the knob is off this returns true for every arm ⇒ RB-1 behaviour byte-identical. */
|
|
273
|
+
function pairwiseEligible(m, objective) {
|
|
274
|
+
if (objective.pairwiseAware !== true)
|
|
275
|
+
return true;
|
|
276
|
+
return m.pairwise != null && m.pairwise.winRateCi95[0] >= 0.5 - EPS;
|
|
277
|
+
}
|
|
236
278
|
/** A price ceiling test: usd_per_1k on the effective-$/1k axis, usd_per_run on
|
|
237
279
|
* mean cost. Null ceiling ⇒ no cost constraint (regret alone gates). */
|
|
238
280
|
function withinCeiling(m, ceiling) {
|
|
@@ -298,7 +340,8 @@ function computeProposal(requested, metrics, ctx) {
|
|
|
298
340
|
const eligible = pool.filter((m) => m.regret != null &&
|
|
299
341
|
m.regret.nPaired >= 10 &&
|
|
300
342
|
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
301
|
-
withinCeiling(m, objective.priceCeiling)
|
|
343
|
+
withinCeiling(m, objective.priceCeiling) &&
|
|
344
|
+
pairwiseEligible(m, objective));
|
|
302
345
|
if (eligible.length === 0)
|
|
303
346
|
return null;
|
|
304
347
|
// Lowest regret, tie → lowest cost.
|
|
@@ -333,7 +376,8 @@ function computeProposal(requested, metrics, ctx) {
|
|
|
333
376
|
const eligible = pool.filter((m) => m.regret != null &&
|
|
334
377
|
m.regret.nPaired >= 10 &&
|
|
335
378
|
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
336
|
-
withinCeiling(m, objective.priceCeiling)
|
|
379
|
+
withinCeiling(m, objective.priceCeiling) &&
|
|
380
|
+
pairwiseEligible(m, objective));
|
|
337
381
|
if (eligible.length === 0)
|
|
338
382
|
return null;
|
|
339
383
|
const winner = [...eligible].sort((a, b) => blended(a) - blended(b))[0];
|
|
@@ -395,6 +439,17 @@ function bakeoffEvidence(artifact, spentUsd) {
|
|
|
395
439
|
reference: artifact.reference ?? null,
|
|
396
440
|
regret: artifact.proposal?.regret ?? null,
|
|
397
441
|
quality_metric: artifact.qualityMetric ?? null,
|
|
442
|
+
// N6: disagreement COUNTS only (numbers — meta-safe) ride the evidence; the
|
|
443
|
+
// captureId candidate list stays on the LOCAL artifact and is never sent to
|
|
444
|
+
// the control plane (content custody).
|
|
445
|
+
disagreement: artifact.disagreement === undefined
|
|
446
|
+
? null
|
|
447
|
+
: {
|
|
448
|
+
examined: artifact.disagreement.examined,
|
|
449
|
+
found: artifact.disagreement.found,
|
|
450
|
+
capped_at: artifact.disagreement.cappedAt,
|
|
451
|
+
counts: artifact.disagreement.counts,
|
|
452
|
+
},
|
|
398
453
|
matrix: artifact.variants.map((m) => ({
|
|
399
454
|
variant: m.variant,
|
|
400
455
|
samples: m.samples,
|
|
@@ -408,6 +463,7 @@ function bakeoffEvidence(artifact, spentUsd) {
|
|
|
408
463
|
quality_mean: m.qualityMean,
|
|
409
464
|
quality_metric: m.qualityMetric,
|
|
410
465
|
regret: m.regret ?? null,
|
|
466
|
+
pairwise: m.pairwise ?? null,
|
|
411
467
|
})),
|
|
412
468
|
};
|
|
413
469
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N5 — pairwise preference vs the reference arm (near-optimal brief §3.2 N5).
|
|
3
|
+
*
|
|
4
|
+
* A second, complementary read on regret: for each replayed sample, is the SERVED
|
|
5
|
+
* arm's output BETTER than, tied with, or WORSE than the REFERENCE arm's on the
|
|
6
|
+
* same captured input? Aggregated to a `pairwise_win_rate` (win=1, tie=0.5,
|
|
7
|
+
* loss=0) with a seeded bootstrap CI — the RigIndex pairwise idea, scoped to one
|
|
8
|
+
* bake-off's arm-vs-reference comparison.
|
|
9
|
+
*
|
|
10
|
+
* DETERMINISTIC-FIRST (F5), exactly like the regret quality tiers:
|
|
11
|
+
* 1. When BOTH sides carry a per-sample value-accuracy score (eval case /
|
|
12
|
+
* value_checks / Q1 gate), the verdict is the sign of their difference —
|
|
13
|
+
* no model call. This is the free, always-preferred tier.
|
|
14
|
+
* 2. Only when a sample cannot be decided deterministically does an OPT-IN,
|
|
15
|
+
* CAPPED, METERED pairwise judge (`bakeoff.pairwise.judge`) compare the two
|
|
16
|
+
* outputs. Judge model ∈ declared candidates (#223 F3, enforced at load).
|
|
17
|
+
*
|
|
18
|
+
* The aggregation math is PURE and unit-tested directly; the judge reuses the
|
|
19
|
+
* real `run()` loop (no new dispatch path, no run.ts edit) exactly as judge.ts.
|
|
20
|
+
*/
|
|
21
|
+
import type { RunDeps } from "../run/run";
|
|
22
|
+
import type { CandidateSpec, RouteBundle } from "../types/route";
|
|
23
|
+
import type { ReplaySampleResult } from "./replay";
|
|
24
|
+
/** The served arm's standing vs the reference on one sample. `win` = the arm's
|
|
25
|
+
* output is better than the reference's; `loss` = worse; `tie` = indistinguishable. */
|
|
26
|
+
export type PairwiseOutcome = "win" | "tie" | "loss";
|
|
27
|
+
/** How a single verdict was decided (F5): a free deterministic comparison of two
|
|
28
|
+
* value-accuracy scores, or a billed pairwise judge call. */
|
|
29
|
+
export type PairwiseSource = "deterministic" | "judge";
|
|
30
|
+
/** Aggregate pairwise standing of an arm vs the reference (N5). `winRate` counts
|
|
31
|
+
* a win as 1 and a tie as 0.5; the CI is a seeded bootstrap of the per-sample
|
|
32
|
+
* win values, so it is reproducible given the same sample set. */
|
|
33
|
+
export interface PairwiseStats {
|
|
34
|
+
readonly winRate: number;
|
|
35
|
+
readonly winRateCi95: readonly [number, number];
|
|
36
|
+
/** Samples with a verdict (deterministic + judge); the CI's n. */
|
|
37
|
+
readonly nDecided: number;
|
|
38
|
+
readonly wins: number;
|
|
39
|
+
readonly ties: number;
|
|
40
|
+
readonly losses: number;
|
|
41
|
+
/** Paired samples that could NOT be decided (no deterministic score and no
|
|
42
|
+
* judge, or the judge was capped/failed) — excluded from the rate, counted. */
|
|
43
|
+
readonly nUndecided: number;
|
|
44
|
+
/** Where the decided verdicts came from — "deterministic", "judge", or "mixed". */
|
|
45
|
+
readonly source: "deterministic" | "judge" | "mixed";
|
|
46
|
+
}
|
|
47
|
+
/** Seeded so a bake-off's pairwise CI is reproducible (like the regret CI). A
|
|
48
|
+
* distinct seed from the regret bootstrap keeps the two resamplings independent. */
|
|
49
|
+
export declare const PAIRWISE_BOOTSTRAP_REPS = 2000;
|
|
50
|
+
export declare const PAIRWISE_BOOTSTRAP_SEED = 20260824;
|
|
51
|
+
/** The win-value of an outcome (win=1, tie=0.5, loss=0) — the quantity the win
|
|
52
|
+
* rate averages. */
|
|
53
|
+
export declare function winValue(o: PairwiseOutcome): number;
|
|
54
|
+
/**
|
|
55
|
+
* The DETERMINISTIC pairwise verdict (tier 1): the sign of `armQuality −
|
|
56
|
+
* refQuality`. Returns null when EITHER side lacks a value-accuracy score — the
|
|
57
|
+
* caller then falls to the judge. A tie within EPS is a genuine tie.
|
|
58
|
+
*/
|
|
59
|
+
export declare function deterministicPairwise(armQuality: number | null, refQuality: number | null): PairwiseOutcome | null;
|
|
60
|
+
/**
|
|
61
|
+
* Aggregate per-sample verdicts into `PairwiseStats` (N5). `decided` are the
|
|
62
|
+
* verdicts that resolved; `nUndecided` the paired samples that did not. The CI is
|
|
63
|
+
* a seeded bootstrap of the per-sample win values (win=1/tie=0.5/loss=0), so it
|
|
64
|
+
* is deterministic given the verdict list + seed. Pure.
|
|
65
|
+
*/
|
|
66
|
+
export declare function aggregatePairwise(decided: readonly {
|
|
67
|
+
readonly outcome: PairwiseOutcome;
|
|
68
|
+
readonly source: PairwiseSource;
|
|
69
|
+
}[], nUndecided: number, seed?: number, reps?: number): PairwiseStats;
|
|
70
|
+
/**
|
|
71
|
+
* Score ONE arm's pairwise standing vs the reference over the paired replay set
|
|
72
|
+
* (N5). Deterministic-first: each paired sample's verdict is the sign of the two
|
|
73
|
+
* value-accuracy scores; a sample neither side scored falls to the capped judge
|
|
74
|
+
* (when supplied), else it is undecided (counted, excluded from the rate). Pairs
|
|
75
|
+
* by captureId; unpaired samples are ignored. Returns null stats when there are
|
|
76
|
+
* NO paired samples at all. `outcomes` maps captureId → verdict (for N6 mining).
|
|
77
|
+
*/
|
|
78
|
+
export declare function scoreArmPairwise(armSamples: readonly ReplaySampleResult[], refSamples: readonly ReplaySampleResult[], judge: PairwiseJudge | null): Promise<{
|
|
79
|
+
stats: PairwiseStats | null;
|
|
80
|
+
outcomes: Map<string, PairwiseOutcome>;
|
|
81
|
+
}>;
|
|
82
|
+
/** Build the synthetic pairwise-judge route for a base route + judge model. The
|
|
83
|
+
* judge model is declared WITHOUT its trial flag so it serves live here. */
|
|
84
|
+
export declare function buildPairwiseJudgeRoute(baseRoute: RouteBundle, model: CandidateSpec): RouteBundle;
|
|
85
|
+
/** A per-arm pairwise scorer: `compare(armOutput, refOutput, taskInput)` returns
|
|
86
|
+
* the arm's outcome vs the reference, or null when the cap is hit or the judge
|
|
87
|
+
* could not produce a valid verdict. */
|
|
88
|
+
export interface PairwiseJudge {
|
|
89
|
+
compare(armOutput: unknown, refOutput: unknown, taskInput: unknown): Promise<PairwiseOutcome | null>;
|
|
90
|
+
used(): number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build a pairwise judge over the run loop (mirrors makeJudgeScorer). Each arm
|
|
94
|
+
* gets a FRESH judge so `maxSamples` is per arm. The comparison is presented in a
|
|
95
|
+
* FIXED order (A = served arm, B = reference); position bias is a known limit,
|
|
96
|
+
* noted in verification — a fixed order keeps the judge DETERMINISTIC for tests.
|
|
97
|
+
*/
|
|
98
|
+
export declare function makePairwiseJudge(deps: RunDeps, baseRoute: RouteBundle, judge: {
|
|
99
|
+
readonly model: CandidateSpec;
|
|
100
|
+
readonly maxSamples: number;
|
|
101
|
+
}): PairwiseJudge;
|
|
102
|
+
//# sourceMappingURL=pairwise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairwise.d.ts","sourceRoot":"","sources":["../../src/bakeoff/pairwise.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;uFACuF;AACvF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAErD;6DAC6D;AAC7D,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,OAAO,CAAC;AAEvD;;kEAEkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;mFAC+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,GAAG,OAAO,CAAC;CACtD;AAED;oFACoF;AACpF,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAC5C,eAAO,MAAM,uBAAuB,WAAW,CAAC;AAKhD;oBACoB;AACpB,wBAAgB,QAAQ,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,GACxB,eAAe,GAAG,IAAI,CAKxB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS;IAAE,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;CAAE,EAAE,EAC1F,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,MAAgC,EACtC,IAAI,GAAE,MAAgC,GACrC,aAAa,CA2Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,KAAK,EAAE,aAAa,GAAG,IAAI,GAC1B,OAAO,CAAC;IAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAAE,CAAC,CA8BlF;AAmBD;4EAC4E;AAC5E,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,GAAG,WAAW,CAejG;AAED;;wCAEwC;AACxC,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACrG,IAAI,IAAI,MAAM,CAAC;CAChB;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,WAAW,EACtB,KAAK,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACpE,aAAa,CAmCf"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N5 — pairwise preference vs the reference arm (near-optimal brief §3.2 N5).
|
|
4
|
+
*
|
|
5
|
+
* A second, complementary read on regret: for each replayed sample, is the SERVED
|
|
6
|
+
* arm's output BETTER than, tied with, or WORSE than the REFERENCE arm's on the
|
|
7
|
+
* same captured input? Aggregated to a `pairwise_win_rate` (win=1, tie=0.5,
|
|
8
|
+
* loss=0) with a seeded bootstrap CI — the RigIndex pairwise idea, scoped to one
|
|
9
|
+
* bake-off's arm-vs-reference comparison.
|
|
10
|
+
*
|
|
11
|
+
* DETERMINISTIC-FIRST (F5), exactly like the regret quality tiers:
|
|
12
|
+
* 1. When BOTH sides carry a per-sample value-accuracy score (eval case /
|
|
13
|
+
* value_checks / Q1 gate), the verdict is the sign of their difference —
|
|
14
|
+
* no model call. This is the free, always-preferred tier.
|
|
15
|
+
* 2. Only when a sample cannot be decided deterministically does an OPT-IN,
|
|
16
|
+
* CAPPED, METERED pairwise judge (`bakeoff.pairwise.judge`) compare the two
|
|
17
|
+
* outputs. Judge model ∈ declared candidates (#223 F3, enforced at load).
|
|
18
|
+
*
|
|
19
|
+
* The aggregation math is PURE and unit-tested directly; the judge reuses the
|
|
20
|
+
* real `run()` loop (no new dispatch path, no run.ts edit) exactly as judge.ts.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.PAIRWISE_BOOTSTRAP_SEED = exports.PAIRWISE_BOOTSTRAP_REPS = void 0;
|
|
24
|
+
exports.winValue = winValue;
|
|
25
|
+
exports.deterministicPairwise = deterministicPairwise;
|
|
26
|
+
exports.aggregatePairwise = aggregatePairwise;
|
|
27
|
+
exports.scoreArmPairwise = scoreArmPairwise;
|
|
28
|
+
exports.buildPairwiseJudgeRoute = buildPairwiseJudgeRoute;
|
|
29
|
+
exports.makePairwiseJudge = makePairwiseJudge;
|
|
30
|
+
const node_crypto_1 = require("node:crypto");
|
|
31
|
+
const bootstrap_1 = require("../rigindex/bootstrap");
|
|
32
|
+
const run_1 = require("../run/run");
|
|
33
|
+
const failures_1 = require("../types/failures");
|
|
34
|
+
/** Seeded so a bake-off's pairwise CI is reproducible (like the regret CI). A
|
|
35
|
+
* distinct seed from the regret bootstrap keeps the two resamplings independent. */
|
|
36
|
+
exports.PAIRWISE_BOOTSTRAP_REPS = 2000;
|
|
37
|
+
exports.PAIRWISE_BOOTSTRAP_SEED = 20260824;
|
|
38
|
+
/** Tie band: value-accuracy scores within EPS are a tie, not a hair-split win. */
|
|
39
|
+
const PAIRWISE_EPS = 1e-9;
|
|
40
|
+
/** The win-value of an outcome (win=1, tie=0.5, loss=0) — the quantity the win
|
|
41
|
+
* rate averages. */
|
|
42
|
+
function winValue(o) {
|
|
43
|
+
return o === "win" ? 1 : o === "tie" ? 0.5 : 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The DETERMINISTIC pairwise verdict (tier 1): the sign of `armQuality −
|
|
47
|
+
* refQuality`. Returns null when EITHER side lacks a value-accuracy score — the
|
|
48
|
+
* caller then falls to the judge. A tie within EPS is a genuine tie.
|
|
49
|
+
*/
|
|
50
|
+
function deterministicPairwise(armQuality, refQuality) {
|
|
51
|
+
if (armQuality === null || refQuality === null)
|
|
52
|
+
return null;
|
|
53
|
+
const diff = armQuality - refQuality;
|
|
54
|
+
if (Math.abs(diff) <= PAIRWISE_EPS)
|
|
55
|
+
return "tie";
|
|
56
|
+
return diff > 0 ? "win" : "loss";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Aggregate per-sample verdicts into `PairwiseStats` (N5). `decided` are the
|
|
60
|
+
* verdicts that resolved; `nUndecided` the paired samples that did not. The CI is
|
|
61
|
+
* a seeded bootstrap of the per-sample win values (win=1/tie=0.5/loss=0), so it
|
|
62
|
+
* is deterministic given the verdict list + seed. Pure.
|
|
63
|
+
*/
|
|
64
|
+
function aggregatePairwise(decided, nUndecided, seed = exports.PAIRWISE_BOOTSTRAP_SEED, reps = exports.PAIRWISE_BOOTSTRAP_REPS) {
|
|
65
|
+
const wins = decided.filter((d) => d.outcome === "win").length;
|
|
66
|
+
const ties = decided.filter((d) => d.outcome === "tie").length;
|
|
67
|
+
const losses = decided.filter((d) => d.outcome === "loss").length;
|
|
68
|
+
const nDecided = decided.length;
|
|
69
|
+
const hasDet = decided.some((d) => d.source === "deterministic");
|
|
70
|
+
const hasJudge = decided.some((d) => d.source === "judge");
|
|
71
|
+
const source = hasDet && hasJudge ? "mixed" : hasJudge ? "judge" : "deterministic";
|
|
72
|
+
if (nDecided === 0) {
|
|
73
|
+
return { winRate: 0, winRateCi95: [0, 0], nDecided: 0, wins: 0, ties: 0, losses: 0, nUndecided, source };
|
|
74
|
+
}
|
|
75
|
+
const values = decided.map((d) => winValue(d.outcome));
|
|
76
|
+
const winRate = values.reduce((a, b) => a + b, 0) / nDecided;
|
|
77
|
+
// Bootstrap the per-sample win values (the pairing is already fixed), take each
|
|
78
|
+
// resample's mean, then the 2.5/97.5 percentiles.
|
|
79
|
+
const rng = (0, bootstrap_1.mulberry32)(seed);
|
|
80
|
+
const means = [];
|
|
81
|
+
for (let b = 0; b < reps; b += 1) {
|
|
82
|
+
let sum = 0;
|
|
83
|
+
for (let i = 0; i < nDecided; i += 1)
|
|
84
|
+
sum += values[Math.floor(rng() * nDecided)];
|
|
85
|
+
means.push(sum / nDecided);
|
|
86
|
+
}
|
|
87
|
+
means.sort((a, b) => a - b);
|
|
88
|
+
const lo = means[Math.floor(0.025 * reps)];
|
|
89
|
+
const hi = means[Math.min(reps - 1, Math.floor(0.975 * reps))];
|
|
90
|
+
return { winRate, winRateCi95: [lo, hi], nDecided, wins, ties, losses, nUndecided, source };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Score ONE arm's pairwise standing vs the reference over the paired replay set
|
|
94
|
+
* (N5). Deterministic-first: each paired sample's verdict is the sign of the two
|
|
95
|
+
* value-accuracy scores; a sample neither side scored falls to the capped judge
|
|
96
|
+
* (when supplied), else it is undecided (counted, excluded from the rate). Pairs
|
|
97
|
+
* by captureId; unpaired samples are ignored. Returns null stats when there are
|
|
98
|
+
* NO paired samples at all. `outcomes` maps captureId → verdict (for N6 mining).
|
|
99
|
+
*/
|
|
100
|
+
async function scoreArmPairwise(armSamples, refSamples, judge) {
|
|
101
|
+
const refByCapture = new Map(refSamples.map((s) => [s.captureId, s]));
|
|
102
|
+
const decided = [];
|
|
103
|
+
const outcomes = new Map();
|
|
104
|
+
let paired = 0;
|
|
105
|
+
let undecided = 0;
|
|
106
|
+
for (const arm of armSamples) {
|
|
107
|
+
const ref = refByCapture.get(arm.captureId);
|
|
108
|
+
if (ref === undefined)
|
|
109
|
+
continue;
|
|
110
|
+
paired += 1;
|
|
111
|
+
const det = deterministicPairwise(arm.valueAccuracy, ref.valueAccuracy);
|
|
112
|
+
if (det !== null) {
|
|
113
|
+
decided.push({ outcome: det, source: "deterministic" });
|
|
114
|
+
outcomes.set(arm.captureId, det);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Deterministic can't decide (a side lacks value accuracy). Only a PRODUCED
|
|
118
|
+
// pair can be judged (both need an output); the judge is capped internally.
|
|
119
|
+
if (judge !== null && arm.failureClass === null && ref.failureClass === null) {
|
|
120
|
+
const verdict = await judge.compare(arm.outputText ?? null, ref.outputText ?? null, arm.inputText ?? null);
|
|
121
|
+
if (verdict !== null) {
|
|
122
|
+
decided.push({ outcome: verdict, source: "judge" });
|
|
123
|
+
outcomes.set(arm.captureId, verdict);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
undecided += 1;
|
|
128
|
+
}
|
|
129
|
+
if (paired === 0)
|
|
130
|
+
return { stats: null, outcomes };
|
|
131
|
+
return { stats: aggregatePairwise(decided, undecided), outcomes };
|
|
132
|
+
}
|
|
133
|
+
// ── The opt-in, capped, metered pairwise judge (tier 2) ──────────────────────
|
|
134
|
+
const PAIRWISE_SCHEMA = {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: { verdict: { type: "string", enum: ["A", "B", "tie"] }, reason: { type: "string" } },
|
|
137
|
+
required: ["verdict"],
|
|
138
|
+
additionalProperties: true,
|
|
139
|
+
};
|
|
140
|
+
const PAIRWISE_RUBRIC = "You are judging PAIRWISE PREFERENCE between two candidate outputs for ONE task — " +
|
|
141
|
+
"which is the better answer for VALUE (correctness and completeness), not merely which " +
|
|
142
|
+
"is better-formed. A is one candidate; B is the other.\n\nTASK INPUT:\n{{task_input}}\n\n" +
|
|
143
|
+
"OUTPUT A:\n{{output_a}}\n\nOUTPUT B:\n{{output_b}}\n\n" +
|
|
144
|
+
'Respond with ONLY compact JSON: {"verdict": "A" | "B" | "tie", "reason": "<one sentence>"}. ' +
|
|
145
|
+
'Answer "A" if A is better, "B" if B is better, "tie" if they are equally good.';
|
|
146
|
+
/** Build the synthetic pairwise-judge route for a base route + judge model. The
|
|
147
|
+
* judge model is declared WITHOUT its trial flag so it serves live here. */
|
|
148
|
+
function buildPairwiseJudgeRoute(baseRoute, model) {
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
route: `__bakeoff_pairwise__:${baseRoute.route}`,
|
|
151
|
+
version: baseRoute.version,
|
|
152
|
+
outputSchema: Object.freeze({ ...PAIRWISE_SCHEMA }),
|
|
153
|
+
candidates: Object.freeze([{ provider: model.provider, model: model.model }]),
|
|
154
|
+
require: Object.freeze([]),
|
|
155
|
+
prefer: Object.freeze([]),
|
|
156
|
+
systemTemplate: PAIRWISE_RUBRIC,
|
|
157
|
+
variables: Object.freeze(["output_a", "output_b", "task_input"]),
|
|
158
|
+
policy: Object.freeze({ retries: Object.freeze({}), timeoutMs: baseRoute.policy.timeoutMs, tier: "standard", json: "json_mode" }),
|
|
159
|
+
variants: Object.freeze([]),
|
|
160
|
+
capture: false,
|
|
161
|
+
fingerprintCell: null,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function asText(v) {
|
|
165
|
+
return typeof v === "string" ? v : JSON.stringify(v ?? null);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Build a pairwise judge over the run loop (mirrors makeJudgeScorer). Each arm
|
|
169
|
+
* gets a FRESH judge so `maxSamples` is per arm. The comparison is presented in a
|
|
170
|
+
* FIXED order (A = served arm, B = reference); position bias is a known limit,
|
|
171
|
+
* noted in verification — a fixed order keeps the judge DETERMINISTIC for tests.
|
|
172
|
+
*/
|
|
173
|
+
function makePairwiseJudge(deps, baseRoute, judge) {
|
|
174
|
+
const judgeRoute = buildPairwiseJudgeRoute(baseRoute, judge.model);
|
|
175
|
+
const routes = {
|
|
176
|
+
get: (r) => (r === judgeRoute.route ? judgeRoute : deps.routes.get(r)),
|
|
177
|
+
list: () => Object.freeze([...deps.routes.list(), judgeRoute]),
|
|
178
|
+
};
|
|
179
|
+
const judgeDeps = { ...deps, routes, captures: null, mode: "live" };
|
|
180
|
+
const envelope = `bakeoff-pairwise:${(0, node_crypto_1.randomUUID)()}`;
|
|
181
|
+
let used = 0;
|
|
182
|
+
return {
|
|
183
|
+
used: () => used,
|
|
184
|
+
async compare(armOutput, refOutput, taskInput) {
|
|
185
|
+
if (used >= judge.maxSamples)
|
|
186
|
+
return null;
|
|
187
|
+
used += 1;
|
|
188
|
+
try {
|
|
189
|
+
const result = await (0, run_1.run)(judgeDeps, judgeRoute.route, {
|
|
190
|
+
input: { output_a: asText(armOutput), output_b: asText(refOutput), task_input: asText(taskInput) },
|
|
191
|
+
// `bakeoff_pairwise` marks the row a pairwise-judge inference (the typed
|
|
192
|
+
// `rung` column is recorder-owned, so a distinct tag key is used); `lane:
|
|
193
|
+
// trial` keeps judge spend off the route's live rollups/odometer.
|
|
194
|
+
tags: { bakeoff_pairwise: "1", lane: "trial" },
|
|
195
|
+
budget: { envelope },
|
|
196
|
+
});
|
|
197
|
+
const verdict = result.output?.verdict;
|
|
198
|
+
if (verdict === "A")
|
|
199
|
+
return "win";
|
|
200
|
+
if (verdict === "B")
|
|
201
|
+
return "loss";
|
|
202
|
+
if (verdict === "tie")
|
|
203
|
+
return "tie";
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (!(err instanceof failures_1.RigFailureError))
|
|
208
|
+
throw err;
|
|
209
|
+
// The metered attempt row(s) still stand; this sample gets no verdict.
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
package/dist/bakeoff/replay.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export interface ReplayRequest {
|
|
|
30
30
|
* sample for the human value review. Off by default (bounds memory + keeps
|
|
31
31
|
* the pure-metrics path clean). */
|
|
32
32
|
readonly keepOutputs?: boolean;
|
|
33
|
+
/** N5: retain `inputText` + `outputText` on each sample for the pairwise judge
|
|
34
|
+
* (arm-vs-reference comparison in the engine), WITHOUT the keepOutputs sink
|
|
35
|
+
* persistence. Set by the engine only when `bakeoff.pairwise.judge` is declared.
|
|
36
|
+
* A superset trigger of `keepOutputs` for retention; the sink stays gated on
|
|
37
|
+
* `keepOutputs` alone so pairwise retention never persists review samples. */
|
|
38
|
+
readonly retainOutputForPairwise?: boolean;
|
|
33
39
|
/** RB-1: corrected/golden expected outputs keyed by captureId (eval-case path).
|
|
34
40
|
* Presence in the map ⇒ this sample has an expected value; `value` may be any
|
|
35
41
|
* JSON (including null). Quality tier 1 (eval case) when present. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/bakeoff/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;uCAEmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;yEAEqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACrE;sFACkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B;;;;;iFAK6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;wEACoE;IACpE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;;;oCAGgC;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;yDACqD;IACrD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;sDACkD;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/bakeoff/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;uCAEmC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;kFAI8E;IAC9E,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;yEAEqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACrE;sFACkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B;;;;;iFAK6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;wEACoE;IACpE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;;;oCAGgC;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;yDACqD;IACrD,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;sDACkD;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,YAAY,EACtB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAyIxC"}
|
package/dist/bakeoff/replay.js
CHANGED
|
@@ -49,6 +49,10 @@ async function replayVariant(deps, captures, req) {
|
|
|
49
49
|
envelopeBudgetUsd: req.envelopeUsd,
|
|
50
50
|
mode,
|
|
51
51
|
};
|
|
52
|
+
// Retain per-sample input/output text when the caller asked (keepOutputs sink,
|
|
53
|
+
// or N5 pairwise judge). Same fields either way; the sink persist stays gated
|
|
54
|
+
// on keepOutputs alone (engine.ts).
|
|
55
|
+
const retainText = req.keepOutputs === true || req.retainOutputForPairwise === true;
|
|
52
56
|
const results = [];
|
|
53
57
|
for (const capture of rows) {
|
|
54
58
|
const sampleTag = `${replayId}:${capture.inferenceId}`;
|
|
@@ -81,7 +85,7 @@ async function replayVariant(deps, captures, req) {
|
|
|
81
85
|
repaired = result.meta.repairedBy !== null;
|
|
82
86
|
outputValue = result.output;
|
|
83
87
|
produced = true;
|
|
84
|
-
if (
|
|
88
|
+
if (retainText)
|
|
85
89
|
outputText = JSON.stringify(result.output, null, 2);
|
|
86
90
|
if (req.qualityGate !== undefined) {
|
|
87
91
|
gated = { output: result.output, provider: result.meta.provider, model: result.meta.model, servedVariant: result.meta.servedVariant };
|
|
@@ -145,7 +149,7 @@ async function replayVariant(deps, captures, req) {
|
|
|
145
149
|
pricingMissing,
|
|
146
150
|
latencyMs,
|
|
147
151
|
failureClass,
|
|
148
|
-
...(
|
|
152
|
+
...(retainText
|
|
149
153
|
? { inputText: capture.variablesJson, outputText }
|
|
150
154
|
: {}),
|
|
151
155
|
});
|
package/dist/bakeoff/report.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export interface CacheAdjustedRow {
|
|
|
36
36
|
* Empty input ⇒ no lines (the section is omitted entirely — back-compat).
|
|
37
37
|
*/
|
|
38
38
|
export declare function formatCacheAdjustedSection(rows: readonly CacheAdjustedRow[]): string[];
|
|
39
|
+
/**
|
|
40
|
+
* N6 disagreement summary lines: the count of mined eval-case candidates (with
|
|
41
|
+
* the honest "of N found" when the list was capped), the per-reason breakdown,
|
|
42
|
+
* and the top candidates as captureId pointers an operator may promote. Pure
|
|
43
|
+
* (returns strings); empty when nothing disagreed. Exported for direct testing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatDisagreementSummary(artifact: BakeoffArtifact): string[];
|
|
39
46
|
export declare function printMatrix(artifact: BakeoffArtifact, cacheAdjusted?: readonly CacheAdjustedRow[]): void;
|
|
40
47
|
/**
|
|
41
48
|
* `modelrig bakeoff --from-eval-cases <task> --variants a,b,c` — replay a task's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/bakeoff/report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/bakeoff/report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AA2CpD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAexE;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAgCtF;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,EAAE,CAiB7E;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,eAAe,EAAE,aAAa,GAAE,SAAS,gBAAgB,EAAO,GAAG,IAAI,CAwD5G;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,MAAM,EAAE,OAAO,EACf,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,MAAM,CAAC,CA+CjB"}
|
package/dist/bakeoff/report.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.bakeoffHeadline = bakeoffHeadline;
|
|
10
10
|
exports.formatCacheAdjustedSection = formatCacheAdjustedSection;
|
|
11
|
+
exports.formatDisagreementSummary = formatDisagreementSummary;
|
|
11
12
|
exports.printMatrix = printMatrix;
|
|
12
13
|
exports.runEvalCaseBakeoffCli = runEvalCaseBakeoffCli;
|
|
13
14
|
const node_fs_1 = require("node:fs");
|
|
@@ -35,6 +36,16 @@ function fmtRegret(m) {
|
|
|
35
36
|
function fmtQuality(m) {
|
|
36
37
|
return m.qualityMean === null ? "—" : `${(m.qualityMean * 100).toFixed(0)}% ${m.qualityMetric}`;
|
|
37
38
|
}
|
|
39
|
+
/** N5: an arm's pairwise-vs-reference cell — "63% [51…74] n=30", "ref", or "—". */
|
|
40
|
+
function fmtPairwise(m) {
|
|
41
|
+
if (m.isReference === true)
|
|
42
|
+
return "ref";
|
|
43
|
+
const p = m.pairwise;
|
|
44
|
+
if (p == null)
|
|
45
|
+
return "—";
|
|
46
|
+
const pct = (v) => (v * 100).toFixed(0);
|
|
47
|
+
return `${pct(p.winRate)}% [${pct(p.winRateCi95[0])}…${pct(p.winRateCi95[1])}] n=${p.nDecided}`;
|
|
48
|
+
}
|
|
38
49
|
/** The reference $/1k or per-run ceiling from the objective, formatted, or null
|
|
39
50
|
* when no ceiling is declared. */
|
|
40
51
|
function fmtCeiling(artifact) {
|
|
@@ -107,14 +118,43 @@ function formatCacheAdjustedSection(rows) {
|
|
|
107
118
|
}
|
|
108
119
|
return lines;
|
|
109
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* N6 disagreement summary lines: the count of mined eval-case candidates (with
|
|
123
|
+
* the honest "of N found" when the list was capped), the per-reason breakdown,
|
|
124
|
+
* and the top candidates as captureId pointers an operator may promote. Pure
|
|
125
|
+
* (returns strings); empty when nothing disagreed. Exported for direct testing.
|
|
126
|
+
*/
|
|
127
|
+
function formatDisagreementSummary(artifact) {
|
|
128
|
+
const d = artifact.disagreement;
|
|
129
|
+
if (d === undefined || d.found === 0)
|
|
130
|
+
return [];
|
|
131
|
+
const reasons = Object.entries(d.counts)
|
|
132
|
+
.filter(([, n]) => n > 0)
|
|
133
|
+
.map(([r, n]) => `${r}=${n}`)
|
|
134
|
+
.join(", ");
|
|
135
|
+
const capNote = d.cappedAt !== null ? ` (showing top ${d.candidates.length} of ${d.found} — cap ${d.cappedAt})` : "";
|
|
136
|
+
const lines = [
|
|
137
|
+
`disagreement: ${d.found} eval-case candidate${d.found === 1 ? "" : "s"} mined from ${d.examined} samples${capNote} — ` +
|
|
138
|
+
"NOT auto-promoted; promote with artifact.promoteToEval.",
|
|
139
|
+
];
|
|
140
|
+
if (reasons !== "")
|
|
141
|
+
lines.push(` reasons: ${reasons}`);
|
|
142
|
+
for (const c of d.candidates.slice(0, 5)) {
|
|
143
|
+
lines.push(` ${c.captureId} [${c.arm}] score ${c.score.toFixed(2)} — ${c.reasons.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
110
147
|
function printMatrix(artifact, cacheAdjusted = []) {
|
|
111
148
|
console.log(`bakeoff ${artifact.id}\nroute ${artifact.route}@${artifact.routeVersion} · replayed ${artifact.replayN} captured inputs · ${artifact.ranAt}`);
|
|
112
149
|
// RB-1: a reference arm is present ⇒ show quality + regret columns and badge
|
|
113
150
|
// the reference row; otherwise the pre-RB-1 matrix stands byte-for-byte.
|
|
114
151
|
const hasReference = artifact.variants.some((m) => m.isReference === true);
|
|
152
|
+
const hasPairwise = artifact.variants.some((m) => m.pairwise != null);
|
|
115
153
|
const header = ["variant", "n", "conform", "ci95", "repair", "repair$", "lat(ms)", "eff-$/1K-conf"];
|
|
116
154
|
if (hasReference)
|
|
117
155
|
header.push("quality", "regret(pts)");
|
|
156
|
+
if (hasPairwise)
|
|
157
|
+
header.push("pairwise-win");
|
|
118
158
|
const rows = artifact.variants.map((m) => {
|
|
119
159
|
const base = [
|
|
120
160
|
m.isReference === true ? `${m.variant} [ref]` : m.variant,
|
|
@@ -128,6 +168,8 @@ function printMatrix(artifact, cacheAdjusted = []) {
|
|
|
128
168
|
];
|
|
129
169
|
if (hasReference)
|
|
130
170
|
base.push(fmtQuality(m), fmtRegret(m));
|
|
171
|
+
if (hasPairwise)
|
|
172
|
+
base.push(fmtPairwise(m));
|
|
131
173
|
return base;
|
|
132
174
|
});
|
|
133
175
|
const widths = header.map((h, c) => Math.max(h.length, ...rows.map((r) => r[c].length)));
|
|
@@ -153,6 +195,10 @@ function printMatrix(artifact, cacheAdjusted = []) {
|
|
|
153
195
|
else {
|
|
154
196
|
console.log("proposal: none (no variant beats the incumbent's gates)");
|
|
155
197
|
}
|
|
198
|
+
// N6 disagreement summary — printed only when candidates were mined; absent ⇒
|
|
199
|
+
// byte-identical to the pre-N6 matrix output.
|
|
200
|
+
for (const l of formatDisagreementSummary(artifact))
|
|
201
|
+
console.log(l);
|
|
156
202
|
// Cache-adjusted section (spec §5.4) — printed only when the caller supplied
|
|
157
203
|
// simulator rows; absent ⇒ the matrix output is byte-identical to pre-WS-B.
|
|
158
204
|
for (const l of formatCacheAdjustedSection(cacheAdjusted))
|