modelrig 0.5.0 → 0.7.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 +125 -0
- package/README.md +54 -4
- package/dist/adapters/anthropic.d.ts.map +1 -1
- package/dist/adapters/anthropic.js +36 -7
- package/dist/adapters/attempt-meta-map.d.ts +63 -0
- package/dist/adapters/attempt-meta-map.d.ts.map +1 -0
- package/dist/adapters/attempt-meta-map.js +154 -0
- package/dist/adapters/deepseek.d.ts.map +1 -1
- package/dist/adapters/deepseek.js +25 -2
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +29 -2
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +25 -2
- package/dist/adapters/openai-compat.d.ts.map +1 -1
- package/dist/adapters/openai-compat.js +25 -2
- package/dist/adapters/openai-stream.d.ts +2 -0
- package/dist/adapters/openai-stream.d.ts.map +1 -1
- package/dist/adapters/openai-stream.js +23 -3
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +36 -6
- package/dist/artifacts/artifact.d.ts.map +1 -1
- package/dist/artifacts/artifact.js +24 -5
- package/dist/artifacts/classification.d.ts +27 -0
- package/dist/artifacts/classification.d.ts.map +1 -0
- package/dist/artifacts/classification.js +38 -0
- package/dist/artifacts/index.d.ts +3 -1
- package/dist/artifacts/index.d.ts.map +1 -1
- package/dist/artifacts/index.js +10 -1
- package/dist/artifacts/run-context.d.ts +25 -3
- package/dist/artifacts/run-context.d.ts.map +1 -1
- package/dist/artifacts/run-context.js +61 -0
- package/dist/artifacts/scrub.d.ts +26 -0
- package/dist/artifacts/scrub.d.ts.map +1 -1
- package/dist/artifacts/scrub.js +53 -0
- package/dist/artifacts/step.d.ts +9 -0
- package/dist/artifacts/step.d.ts.map +1 -1
- package/dist/artifacts/step.js +9 -0
- package/dist/bakeoff/engine.d.ts +46 -4
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +269 -19
- package/dist/bakeoff/eval-cases.d.ts +5 -0
- package/dist/bakeoff/eval-cases.d.ts.map +1 -1
- package/dist/bakeoff/eval-cases.js +3 -0
- package/dist/bakeoff/judge.d.ts +39 -0
- package/dist/bakeoff/judge.d.ts.map +1 -0
- package/dist/bakeoff/judge.js +102 -0
- package/dist/bakeoff/quality.d.ts +44 -0
- package/dist/bakeoff/quality.d.ts.map +1 -0
- package/dist/bakeoff/quality.js +126 -0
- package/dist/bakeoff/reference.d.ts +34 -0
- package/dist/bakeoff/reference.d.ts.map +1 -0
- package/dist/bakeoff/reference.js +68 -0
- package/dist/bakeoff/replay.d.ts +25 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +48 -1
- package/dist/bakeoff/report.d.ts +8 -1
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +83 -12
- package/dist/bakeoff/samples.d.ts +35 -2
- package/dist/bakeoff/samples.d.ts.map +1 -1
- package/dist/bakeoff/samples.js +67 -6
- package/dist/bakeoff/stats.d.ts +17 -1
- package/dist/bakeoff/stats.d.ts.map +1 -1
- package/dist/bakeoff/stats.js +67 -0
- package/dist/bakeoff/types.d.ts +76 -5
- package/dist/bakeoff/types.d.ts.map +1 -1
- package/dist/bakeoff/types.js +4 -3
- package/dist/cli-query.d.ts +14 -0
- package/dist/cli-query.d.ts.map +1 -0
- package/dist/cli-query.js +59 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -23
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/detect/detector.d.ts +82 -0
- package/dist/detect/detector.d.ts.map +1 -0
- package/dist/detect/detector.js +23 -0
- package/dist/index.d.ts +19 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -11
- package/dist/query/analytics.d.ts +84 -0
- package/dist/query/analytics.d.ts.map +1 -0
- package/dist/query/analytics.js +280 -0
- package/dist/query/columns.d.ts +59 -0
- package/dist/query/columns.d.ts.map +1 -0
- package/dist/query/columns.js +89 -0
- package/dist/query/grammar.d.ts +74 -7
- package/dist/query/grammar.d.ts.map +1 -1
- package/dist/query/grammar.js +179 -71
- package/dist/query/saved.d.ts +26 -0
- package/dist/query/saved.d.ts.map +1 -0
- package/dist/query/saved.js +51 -0
- package/dist/routes/loader.js +15 -10
- package/dist/routes/objective-loader.d.ts +35 -0
- package/dist/routes/objective-loader.d.ts.map +1 -0
- package/dist/routes/objective-loader.js +286 -0
- package/dist/run/accept.d.ts +43 -0
- package/dist/run/accept.d.ts.map +1 -0
- package/dist/run/accept.js +65 -0
- package/dist/run/cost.d.ts +13 -0
- package/dist/run/cost.d.ts.map +1 -1
- package/dist/run/cost.js +27 -0
- package/dist/run/dispatch.d.ts +24 -1
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +19 -0
- package/dist/run/ladder-key.d.ts +4 -0
- package/dist/run/ladder-key.d.ts.map +1 -1
- package/dist/run/ladder-key.js +3 -0
- package/dist/run/quality-gate.d.ts +39 -0
- package/dist/run/quality-gate.d.ts.map +1 -0
- package/dist/run/quality-gate.js +70 -0
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +22 -3
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +22 -1
- package/dist/run/routing-docs.d.ts.map +1 -1
- package/dist/run/routing-docs.js +11 -4
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +46 -39
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +2 -1
- package/dist/settings/task-settings.d.ts +10 -0
- package/dist/settings/task-settings.d.ts.map +1 -1
- package/dist/settings/task-settings.js +17 -0
- package/dist/telemetry/attempt-meta.d.ts +178 -0
- package/dist/telemetry/attempt-meta.d.ts.map +1 -0
- package/dist/telemetry/attempt-meta.js +319 -0
- package/dist/telemetry/exporter.d.ts.map +1 -1
- package/dist/telemetry/exporter.js +6 -0
- package/dist/telemetry/ingest-protocol.d.ts +11 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/ingest-protocol.js +3 -0
- package/dist/telemetry/otel-map.d.ts +10 -0
- package/dist/telemetry/otel-map.d.ts.map +1 -1
- package/dist/telemetry/otel-map.js +64 -0
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +15 -0
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +25 -7
- package/dist/types/failures-teaching.d.ts.map +1 -1
- package/dist/types/failures-teaching.js +20 -0
- package/dist/types/failures.d.ts +1 -1
- package/dist/types/failures.d.ts.map +1 -1
- package/dist/types/failures.js +1 -0
- package/dist/types/provider.d.ts +9 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/quality.d.ts +41 -0
- package/dist/types/quality.d.ts.map +1 -0
- package/dist/types/quality.js +12 -0
- package/dist/types/route.d.ts +89 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +13 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +10 -0
- package/package.json +1 -1
package/dist/bakeoff/engine.js
CHANGED
|
@@ -12,14 +12,22 @@
|
|
|
12
12
|
* the artifact records value_accuracy where the hook supplies it.
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.REFERENCE_ARM = void 0;
|
|
15
16
|
exports.runBakeoff = runBakeoff;
|
|
16
17
|
exports.computeProposal = computeProposal;
|
|
17
18
|
exports.mirrorBakeoff = mirrorBakeoff;
|
|
19
|
+
exports.bakeoffEvidence = bakeoffEvidence;
|
|
18
20
|
const node_crypto_1 = require("node:crypto");
|
|
19
21
|
const loader_1 = require("../routes/loader");
|
|
20
22
|
const replay_1 = require("./replay");
|
|
21
23
|
const stats_1 = require("./stats");
|
|
24
|
+
const judge_1 = require("./judge");
|
|
25
|
+
const reference_1 = require("./reference");
|
|
22
26
|
const samples_1 = require("./samples");
|
|
27
|
+
/** The arm name carrying the reference (ceiling) metrics. Reserved — a user
|
|
28
|
+
* variant may not be named this (they cannot start a name with "__"; and it is
|
|
29
|
+
* never in `req.variants`). */
|
|
30
|
+
exports.REFERENCE_ARM = "__reference__";
|
|
23
31
|
/** Arm metadata for the reviewer: which model/rung/scaffold served it. */
|
|
24
32
|
function deriveArmMeta(route, arm) {
|
|
25
33
|
if (arm === "default") {
|
|
@@ -33,6 +41,25 @@ function deriveArmMeta(route, arm) {
|
|
|
33
41
|
...(variant?.scaffold !== undefined ? { scaffold: true } : {}),
|
|
34
42
|
};
|
|
35
43
|
}
|
|
44
|
+
/** Overlay one synthetic route (reference/judge arm) onto a routes store. */
|
|
45
|
+
function overlayRoute(routes, extra) {
|
|
46
|
+
return {
|
|
47
|
+
get: (r) => (r === extra.route ? extra : routes.get(r)),
|
|
48
|
+
list: () => Object.freeze([...routes.list(), extra]),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Resolve the reference candidate from `route.bakeoff.reference` (auto/explicit),
|
|
52
|
+
* or null when none is declared/resolvable. */
|
|
53
|
+
function resolveReference(route, req) {
|
|
54
|
+
const ref = route.bakeoff?.reference;
|
|
55
|
+
if (ref === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
if (ref === "auto") {
|
|
58
|
+
const candidate = req.resolveReferenceAuto?.(route) ?? null;
|
|
59
|
+
return candidate === null ? null : { candidate, auto: true };
|
|
60
|
+
}
|
|
61
|
+
return { candidate: { provider: ref.provider, model: ref.model }, auto: false };
|
|
62
|
+
}
|
|
36
63
|
async function runBakeoff(deps, captures, req, samplesSink) {
|
|
37
64
|
if (req.variants.length === 0) {
|
|
38
65
|
throw new loader_1.RouteConfigError("bakeoff needs at least one variant (use \"default\" for the base config)");
|
|
@@ -47,6 +74,10 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
47
74
|
}
|
|
48
75
|
const keepOutputs = req.keepOutputs !== undefined && samplesSink !== undefined;
|
|
49
76
|
const bakeoffId = (0, node_crypto_1.randomUUID)();
|
|
77
|
+
const judgeCfg = route.bakeoff?.judge;
|
|
78
|
+
// Replay every requested arm. Raw samples are kept ALWAYS (paired regret is
|
|
79
|
+
// computed from per-sample quality); the keepOutputs sink is separate. A fresh
|
|
80
|
+
// judge scorer per arm keeps `max_samples` per-arm (spec §4.3).
|
|
50
81
|
const metrics = [];
|
|
51
82
|
const rawByArm = new Map();
|
|
52
83
|
let replayN = 0;
|
|
@@ -57,13 +88,70 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
57
88
|
lastN: req.replayLastN,
|
|
58
89
|
envelopeUsd: req.envelopeUsdPerVariant,
|
|
59
90
|
...(keepOutputs ? { keepOutputs: true } : {}),
|
|
91
|
+
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
92
|
+
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
93
|
+
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
60
94
|
});
|
|
61
95
|
replayN = Math.max(replayN, samples.length);
|
|
96
|
+
rawByArm.set(variant, samples);
|
|
62
97
|
metrics.push((0, stats_1.aggregateVariantMetrics)(variant, samples));
|
|
63
|
-
if (keepOutputs)
|
|
64
|
-
rawByArm.set(variant, samples);
|
|
65
98
|
}
|
|
66
|
-
|
|
99
|
+
// RB-1 reference (ceiling) arm — replay the reference model on the SAME set
|
|
100
|
+
// under its own envelope (it is dear by definition), then attach each requested
|
|
101
|
+
// arm's paired regret vs it. Partial-result honesty is preserved.
|
|
102
|
+
const referenceInfo = resolveReference(route, req);
|
|
103
|
+
let reference = null;
|
|
104
|
+
if (referenceInfo !== null) {
|
|
105
|
+
const { deps: refDeps } = withReferenceArm(deps, route, referenceInfo.candidate);
|
|
106
|
+
const refEnvelope = route.bakeoff?.referenceEnvelopeUsd ?? req.envelopeUsdPerVariant;
|
|
107
|
+
const refSamples = await (0, replay_1.replayVariant)(refDeps, captures, {
|
|
108
|
+
route: req.route,
|
|
109
|
+
variant: exports.REFERENCE_ARM,
|
|
110
|
+
lastN: req.replayLastN,
|
|
111
|
+
envelopeUsd: refEnvelope,
|
|
112
|
+
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
113
|
+
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
114
|
+
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
115
|
+
});
|
|
116
|
+
const partial = refSamples.length < replayN || refSamples.some((s) => s.failureClass === "budget_exhausted");
|
|
117
|
+
for (let i = 0; i < metrics.length; i += 1) {
|
|
118
|
+
const raw = rawByArm.get(metrics[i].variant) ?? [];
|
|
119
|
+
metrics[i] = {
|
|
120
|
+
...metrics[i],
|
|
121
|
+
regret: (0, stats_1.pairedRegret)(raw, refSamples),
|
|
122
|
+
...(partial ? { referencePartial: true } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
metrics.push({
|
|
126
|
+
...(0, stats_1.aggregateVariantMetrics)(exports.REFERENCE_ARM, refSamples),
|
|
127
|
+
isReference: true,
|
|
128
|
+
regret: null,
|
|
129
|
+
...(partial ? { referencePartial: true } : {}),
|
|
130
|
+
});
|
|
131
|
+
reference = {
|
|
132
|
+
variant: exports.REFERENCE_ARM,
|
|
133
|
+
model: `${referenceInfo.candidate.provider}/${referenceInfo.candidate.model}`,
|
|
134
|
+
auto: referenceInfo.auto,
|
|
135
|
+
rule: referenceInfo.auto ? reference_1.REFERENCE_AUTO_RULE : null,
|
|
136
|
+
external: !isDeclared(route, referenceInfo.candidate),
|
|
137
|
+
partial,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const objective = resolveObjective(route, req);
|
|
141
|
+
const qualityMetric = metrics.some((m) => m.isReference !== true && m.qualityMetric === "value_accuracy")
|
|
142
|
+
? "value_accuracy"
|
|
143
|
+
: "conformance";
|
|
144
|
+
const proposal = computeProposal(req.variants, metrics, {
|
|
145
|
+
objective,
|
|
146
|
+
...(reference !== null ? { referenceVariant: reference.variant } : {}),
|
|
147
|
+
});
|
|
148
|
+
// Architect B-3: stamp the proposed arm onto the matrix so the mirror/console
|
|
149
|
+
// read the artifact's decision instead of recomputing a mode-blind winner.
|
|
150
|
+
// Every arm carries the flag (false included) so a no-proposal artifact is
|
|
151
|
+
// distinguishable from a pre-RB-1 row — the console never recomputes for it.
|
|
152
|
+
for (let i = 0; i < metrics.length; i += 1) {
|
|
153
|
+
metrics[i] = { ...metrics[i], proposed: proposal !== null && metrics[i].variant === proposal.variant };
|
|
154
|
+
}
|
|
67
155
|
// Persist review samples (A3) — AFTER the winner is known, for the selected
|
|
68
156
|
// arms only. Never touches the artifact (payloads stay off the anon table).
|
|
69
157
|
if (keepOutputs && samplesSink !== undefined) {
|
|
@@ -101,32 +189,160 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
101
189
|
replayN,
|
|
102
190
|
variants: metrics,
|
|
103
191
|
proposal,
|
|
192
|
+
objective,
|
|
193
|
+
reference,
|
|
194
|
+
qualityMetric,
|
|
104
195
|
};
|
|
105
196
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
197
|
+
function isDeclared(route, candidate) {
|
|
198
|
+
return route.candidates.some((c) => c.provider === candidate.provider && c.model === candidate.model);
|
|
199
|
+
}
|
|
200
|
+
/** Build a deps whose route store carries a synthetic `__reference__` variant
|
|
201
|
+
* narrowing to the reference candidate (added to the declared set when external,
|
|
202
|
+
* so the candidate-set guard is satisfied). */
|
|
203
|
+
function withReferenceArm(deps, route, candidate) {
|
|
204
|
+
const refVariant = { name: exports.REFERENCE_ARM, candidates: [{ provider: candidate.provider, model: candidate.model }] };
|
|
205
|
+
const augmented = Object.freeze({
|
|
206
|
+
...route,
|
|
207
|
+
candidates: isDeclared(route, candidate)
|
|
208
|
+
? route.candidates
|
|
209
|
+
: Object.freeze([...route.candidates, { provider: candidate.provider, model: candidate.model }]),
|
|
210
|
+
variants: Object.freeze([...route.variants, refVariant]),
|
|
211
|
+
});
|
|
212
|
+
return { deps: { ...deps, routes: overlayRoute(deps.routes, augmented) } };
|
|
213
|
+
}
|
|
214
|
+
function resolveObjective(route, req) {
|
|
215
|
+
// Architect B-2: a route that declares NEITHER an `objective:` NOR a `bakeoff:`
|
|
216
|
+
// block has not opted into RB-1 — it defaults to `cost-first` (the pre-RB-1
|
|
217
|
+
// gate, byte-identical), so legacy routes and the hosted R3 swap loop keep
|
|
218
|
+
// proposing. A route that DID opt in (either block) defaults to quality-first,
|
|
219
|
+
// where "no bound ⇒ no proposal" (loader-warned) prevents silent actuation.
|
|
220
|
+
const optedIn = route.objective !== undefined || route.bakeoff !== undefined;
|
|
221
|
+
const mode = req.objectiveMode ?? route.objective?.mode ?? (optedIn ? "quality-first" : "cost-first");
|
|
222
|
+
return {
|
|
223
|
+
mode,
|
|
224
|
+
maxRegret: route.objective?.maxRegret ?? null,
|
|
225
|
+
priceCeiling: route.objective?.priceCeiling ?? null,
|
|
226
|
+
lambda: route.objective?.lambda ?? null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const EPS = 1e-9;
|
|
230
|
+
function savingsPct(incumbent, winner) {
|
|
231
|
+
const inc = incumbent.effectiveUsdPer1kConformant;
|
|
232
|
+
return inc === 0 || !Number.isFinite(inc)
|
|
233
|
+
? 0
|
|
234
|
+
: ((inc - winner.effectiveUsdPer1kConformant) / inc) * 100;
|
|
235
|
+
}
|
|
236
|
+
/** A price ceiling test: usd_per_1k on the effective-$/1k axis, usd_per_run on
|
|
237
|
+
* mean cost. Null ceiling ⇒ no cost constraint (regret alone gates). */
|
|
238
|
+
function withinCeiling(m, ceiling) {
|
|
239
|
+
if (ceiling === null)
|
|
240
|
+
return true;
|
|
241
|
+
if (ceiling.usdPer1k !== undefined)
|
|
242
|
+
return m.effectiveUsdPer1kConformant <= ceiling.usdPer1k + EPS;
|
|
243
|
+
if (ceiling.usdPerRun !== undefined)
|
|
244
|
+
return m.meanCostUsd <= ceiling.usdPerRun + EPS;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Winner among gate-passing challengers; null when nobody beats the incumbent.
|
|
249
|
+
* Exported for direct unit testing. `ctx` absent OR `ctx.objective` absent ⇒ the
|
|
250
|
+
* legacy cost-first gate (goldens). Otherwise per the objective's mode.
|
|
251
|
+
*/
|
|
252
|
+
function computeProposal(requested, metrics, ctx) {
|
|
253
|
+
const mode = ctx?.objective?.mode ?? "cost-first";
|
|
109
254
|
const incumbentName = requested.includes("default") ? "default" : requested[0];
|
|
110
255
|
const incumbent = metrics.find((m) => m.variant === incumbentName);
|
|
111
256
|
// A1: unpriced cost columns under-count — an unpriced incumbent can't be
|
|
112
257
|
// compared against, and an unpriced challenger must never look cheap.
|
|
113
258
|
if (incumbent === undefined || incumbent.samples === 0 || incumbent.pricingMissing)
|
|
114
259
|
return null;
|
|
115
|
-
|
|
260
|
+
// Q1 (spec §4.2): when a quality gate ran on the incumbent, the cost-first
|
|
261
|
+
// comparison gains a THIRD clause (challenger's quality-pass CI-lower ≥
|
|
262
|
+
// incumbent's) AND the headline denominator switches from conformant to
|
|
263
|
+
// conformant-AND-passing (effectiveUsdPer1kPassing, reported beside — never
|
|
264
|
+
// redefining — the conformant metric; flag F6). Absent a gate, byte-identical.
|
|
265
|
+
const gated = incumbent.qualityPassCi95 !== null;
|
|
266
|
+
const metricOf = (m) => gated ? m.effectiveUsdPer1kPassing ?? Number.POSITIVE_INFINITY : m.effectiveUsdPer1kConformant;
|
|
267
|
+
// RB-1: the challenger pool is membership + priced; the per-mode gate below
|
|
268
|
+
// decides eligibility (cost-first uses Q1's conformance/quality/cost clauses;
|
|
269
|
+
// quality-first/balanced use regret vs the reference under the ceiling).
|
|
270
|
+
const pool = metrics.filter((m) => m.variant !== incumbentName &&
|
|
271
|
+
m.variant !== ctx?.referenceVariant &&
|
|
272
|
+
requested.includes(m.variant) &&
|
|
116
273
|
m.samples > 0 &&
|
|
117
|
-
!m.pricingMissing
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
274
|
+
!m.pricingMissing);
|
|
275
|
+
if (mode === "cost-first") {
|
|
276
|
+
// Q1 gate: not-worse on conformance CI, (when gated) not-worse on quality-pass
|
|
277
|
+
// CI, AND cheaper on the effective metric (passing when gated, else conformant).
|
|
278
|
+
const challengers = pool.filter((m) => m.conformCi95[0] >= incumbent.conformCi95[0] &&
|
|
279
|
+
(!gated ||
|
|
280
|
+
(m.qualityPassCi95 !== null &&
|
|
281
|
+
incumbent.qualityPassCi95 !== null &&
|
|
282
|
+
m.qualityPassCi95[0] >= incumbent.qualityPassCi95[0])) &&
|
|
283
|
+
metricOf(m) < metricOf(incumbent));
|
|
284
|
+
if (challengers.length === 0)
|
|
285
|
+
return null;
|
|
286
|
+
const winner = [...challengers].sort((a, b) => metricOf(a) - metricOf(b))[0];
|
|
287
|
+
const incumbentMetric = metricOf(incumbent);
|
|
288
|
+
const savings = incumbentMetric === 0 || !Number.isFinite(incumbentMetric)
|
|
289
|
+
? 0
|
|
290
|
+
: ((incumbentMetric - metricOf(winner)) / incumbentMetric) * 100;
|
|
291
|
+
return { variant: winner.variant, vsIncumbentSavingsPct: savings };
|
|
292
|
+
}
|
|
293
|
+
const objective = ctx?.objective;
|
|
294
|
+
if (mode === "quality-first") {
|
|
295
|
+
// No bound declared ⇒ regret is REPORTED but nothing is proposed (spec §3).
|
|
296
|
+
if (objective.maxRegret === null && objective.priceCeiling === null)
|
|
297
|
+
return null;
|
|
298
|
+
const eligible = pool.filter((m) => m.regret != null &&
|
|
299
|
+
m.regret.nPaired >= 10 &&
|
|
300
|
+
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
301
|
+
withinCeiling(m, objective.priceCeiling));
|
|
302
|
+
if (eligible.length === 0)
|
|
303
|
+
return null;
|
|
304
|
+
// Lowest regret, tie → lowest cost.
|
|
305
|
+
const winner = [...eligible].sort((a, b) => a.regret.regretMean - b.regret.regretMean ||
|
|
306
|
+
a.effectiveUsdPer1kConformant - b.effectiveUsdPer1kConformant)[0];
|
|
307
|
+
// Propose iff the arm improves on the incumbent — never a lateral, never a
|
|
308
|
+
// quality regression to buy cost (architect B-1). The conformance clause is
|
|
309
|
+
// kept (a swap must not regress shape), and a COST-motivated proposal must be
|
|
310
|
+
// not-worse on regret: a cheaper-but-worse-quality arm is never proposed under
|
|
311
|
+
// quality-first. A regret win is a quality improvement and proposes on its own.
|
|
312
|
+
const incRegret = incumbent.regret?.regretMean ?? Number.POSITIVE_INFINITY;
|
|
313
|
+
const winRegret = winner.regret.regretMean;
|
|
314
|
+
const notWorseConform = winner.conformCi95[0] >= incumbent.conformCi95[0] - EPS;
|
|
315
|
+
const notWorseRegret = winRegret <= incRegret + EPS;
|
|
316
|
+
const betterRegret = winRegret < incRegret - EPS;
|
|
317
|
+
const betterCost = winner.effectiveUsdPer1kConformant < incumbent.effectiveUsdPer1kConformant - EPS;
|
|
318
|
+
if (!notWorseConform)
|
|
319
|
+
return null;
|
|
320
|
+
if (!(betterRegret || (betterCost && notWorseRegret)))
|
|
321
|
+
return null;
|
|
322
|
+
return { variant: winner.variant, vsIncumbentSavingsPct: savingsPct(incumbent, winner), regret: winner.regret ?? null };
|
|
323
|
+
}
|
|
324
|
+
// balanced: minimize regret_mean + λ·cost_norm, where cost_norm = the arm's
|
|
325
|
+
// effective $/1k divided by the incumbent's (dimensionless; 1.0 = the
|
|
326
|
+
// incumbent's cost). Must beat the incumbent's own blended score, under ceiling.
|
|
327
|
+
const lambda = objective.lambda ?? 0;
|
|
328
|
+
const incCost = incumbent.effectiveUsdPer1kConformant;
|
|
329
|
+
const norm = (m) => Number.isFinite(incCost) && incCost > 0 ? m.effectiveUsdPer1kConformant / incCost : m.effectiveUsdPer1kConformant;
|
|
330
|
+
const blended = (m) => (m.regret?.regretMean ?? Number.POSITIVE_INFINITY) + lambda * norm(m);
|
|
331
|
+
// `max_regret` is a HARD bound in every mode (architect B-1, round 2): λ may
|
|
332
|
+
// trade regret for cost only inside the declared bound, never across it.
|
|
333
|
+
const eligible = pool.filter((m) => m.regret != null &&
|
|
334
|
+
m.regret.nPaired >= 10 &&
|
|
335
|
+
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
336
|
+
withinCeiling(m, objective.priceCeiling));
|
|
337
|
+
if (eligible.length === 0)
|
|
121
338
|
return null;
|
|
122
|
-
const winner = [...
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return { variant: winner.variant, vsIncumbentSavingsPct: savings };
|
|
339
|
+
const winner = [...eligible].sort((a, b) => blended(a) - blended(b))[0];
|
|
340
|
+
if (blended(winner) >= blended(incumbent) - EPS)
|
|
341
|
+
return null;
|
|
342
|
+
// Keep the conformance clause (B-1): a swap must not regress shape.
|
|
343
|
+
if (winner.conformCi95[0] < incumbent.conformCi95[0] - EPS)
|
|
344
|
+
return null;
|
|
345
|
+
return { variant: winner.variant, vsIncumbentSavingsPct: savingsPct(incumbent, winner), regret: winner.regret ?? null };
|
|
130
346
|
}
|
|
131
347
|
/** Upsert the artifact into the `bakeoffs` mirror table (console read path).
|
|
132
348
|
* Same PostgREST transport pattern as the telemetry exporter; captures have
|
|
@@ -161,3 +377,37 @@ async function mirrorBakeoff(artifact, config) {
|
|
|
161
377
|
throw new Error(`bakeoff mirror upsert → ${response.status} ${await response.text().catch(() => "")}`);
|
|
162
378
|
}
|
|
163
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* The proposal-evidence jsonb for a bake-off artifact (near-optimal spec §4.6) —
|
|
382
|
+
* the winning variant, the RB-1 objective/reference/regret/quality_metric, and a
|
|
383
|
+
* per-arm matrix (with per-arm regret). Built here so the CLI stays thin; the PR
|
|
384
|
+
* body reads these fields back off `proposal.evidence`. ZERO DDL — freeform jsonb.
|
|
385
|
+
*/
|
|
386
|
+
function bakeoffEvidence(artifact, spentUsd) {
|
|
387
|
+
return {
|
|
388
|
+
bakeoff_id: artifact.id,
|
|
389
|
+
replay_n: artifact.replayN,
|
|
390
|
+
ran_at: artifact.ranAt,
|
|
391
|
+
spent_usd: spentUsd,
|
|
392
|
+
winning_variant: artifact.proposal?.variant ?? null,
|
|
393
|
+
proposal: artifact.proposal,
|
|
394
|
+
objective: artifact.objective ?? null,
|
|
395
|
+
reference: artifact.reference ?? null,
|
|
396
|
+
regret: artifact.proposal?.regret ?? null,
|
|
397
|
+
quality_metric: artifact.qualityMetric ?? null,
|
|
398
|
+
matrix: artifact.variants.map((m) => ({
|
|
399
|
+
variant: m.variant,
|
|
400
|
+
samples: m.samples,
|
|
401
|
+
conform_rate: m.conformRate,
|
|
402
|
+
conform_ci95: m.conformCi95,
|
|
403
|
+
effective_usd_per_1k_conformant: Number.isFinite(m.effectiveUsdPer1kConformant)
|
|
404
|
+
? m.effectiveUsdPer1kConformant
|
|
405
|
+
: null,
|
|
406
|
+
pricing_missing: m.pricingMissing,
|
|
407
|
+
is_reference: m.isReference ?? false,
|
|
408
|
+
quality_mean: m.qualityMean,
|
|
409
|
+
quality_metric: m.qualityMetric,
|
|
410
|
+
regret: m.regret ?? null,
|
|
411
|
+
})),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
@@ -23,6 +23,8 @@ import type { RunDeps } from "../run/run";
|
|
|
23
23
|
import { type BakeoffSamplesSink } from "./engine";
|
|
24
24
|
import type { KeepOutputsMode } from "./samples";
|
|
25
25
|
import type { BakeoffArtifact } from "./types";
|
|
26
|
+
import type { CandidateSpec, ObjectiveMode, RouteBundle } from "../types/route";
|
|
27
|
+
import type { QualityGate } from "../types/quality";
|
|
26
28
|
/** The narrow store surface the selection reads — kept minimal so this module
|
|
27
29
|
* depends on four reads, not three whole stores. */
|
|
28
30
|
export interface EvalCaseSelectionDeps {
|
|
@@ -92,5 +94,8 @@ export declare function runBakeoffFromEvalCases(deps: RunDeps, stores: EvalCaseS
|
|
|
92
94
|
readonly variants: readonly string[];
|
|
93
95
|
readonly envelopeUsdPerVariant: number;
|
|
94
96
|
readonly keepOutputs?: KeepOutputsMode;
|
|
97
|
+
readonly objectiveMode?: ObjectiveMode;
|
|
98
|
+
readonly resolveReferenceAuto?: (route: RouteBundle) => CandidateSpec | null;
|
|
99
|
+
readonly qualityGate?: QualityGate;
|
|
95
100
|
}, samplesSink?: BakeoffSamplesSink): Promise<EvalCaseBakeoffResult>;
|
|
96
101
|
//# sourceMappingURL=eval-cases.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-cases.d.ts","sourceRoot":"","sources":["../../src/bakeoff/eval-cases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"eval-cases.d.ts","sourceRoot":"","sources":["../../src/bakeoff/eval-cases.ts"],"names":[],"mappings":"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"}
|
|
@@ -100,6 +100,9 @@ async function runBakeoffFromEvalCases(deps, stores, req, samplesSink) {
|
|
|
100
100
|
replayLastN: selection.rehydratable.length, // replay every rehydratable case
|
|
101
101
|
envelopeUsdPerVariant: req.envelopeUsdPerVariant,
|
|
102
102
|
...(req.keepOutputs !== undefined ? { keepOutputs: req.keepOutputs } : {}),
|
|
103
|
+
...(req.objectiveMode !== undefined ? { objectiveMode: req.objectiveMode } : {}),
|
|
104
|
+
...(req.resolveReferenceAuto !== undefined ? { resolveReferenceAuto: req.resolveReferenceAuto } : {}),
|
|
105
|
+
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
103
106
|
}, samplesSink);
|
|
104
107
|
return { selection, artifact };
|
|
105
108
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-side bake-off judge (RB-1, near-optimal spec §4.3, tier 3). When a sample
|
|
3
|
+
* has NO eval-case expected value and NO declared value check, an opt-in judge
|
|
4
|
+
* (`bakeoff.judge: { model, max_samples }`) scores the arm's output on value.
|
|
5
|
+
*
|
|
6
|
+
* How it stays honest and bounded, reusing the real run loop (no new dispatch
|
|
7
|
+
* path, no run.ts edit):
|
|
8
|
+
* - The judge is a normal `run()` against a synthetic single-candidate route
|
|
9
|
+
* whose only candidate is the judge model — so the call is METERED exactly
|
|
10
|
+
* like any inference (a telemetry attempt row with real cost), tagged
|
|
11
|
+
* `rung: "judge"`, and no capture is written (captures severed).
|
|
12
|
+
* - CAPPED at `max_samples` PER ARM: past the cap `score()` returns null and
|
|
13
|
+
* the sample falls back to conformance (counted by omission).
|
|
14
|
+
* - The judge model MUST be a declared candidate (#223 F3) — enforced at load
|
|
15
|
+
* (objective-loader.ts), so it is a real, reachable model here.
|
|
16
|
+
* - Fail-closed: an unparseable/short-of-schema judge answer throws in the run
|
|
17
|
+
* loop; the metered row still stands and the sample's score is null.
|
|
18
|
+
*/
|
|
19
|
+
import type { RunDeps } from "../run/run";
|
|
20
|
+
import type { CandidateSpec, RouteBundle } from "../types/route";
|
|
21
|
+
/** Build the synthetic judge route for a base route + judge model. The judge
|
|
22
|
+
* model is declared WITHOUT its trial flag so it serves live on this route. */
|
|
23
|
+
export declare function buildJudgeRoute(baseRoute: RouteBundle, model: CandidateSpec): RouteBundle;
|
|
24
|
+
/** A per-arm judge scorer: `score(output, taskInput)` returns a value in [0,1],
|
|
25
|
+
* or null when the cap is hit or the judge could not produce a valid answer. */
|
|
26
|
+
export interface JudgeScorer {
|
|
27
|
+
score(output: unknown, taskInput: unknown): Promise<number | null>;
|
|
28
|
+
used(): number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a judge scorer over the run loop. `deps` are the bake-off's deps (the
|
|
32
|
+
* judge model's adapter must be present, as for any candidate). Each arm gets a
|
|
33
|
+
* FRESH scorer so the `max_samples` cap is per arm (spec: max_samples per arm).
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeJudgeScorer(deps: RunDeps, baseRoute: RouteBundle, judge: {
|
|
36
|
+
readonly model: CandidateSpec;
|
|
37
|
+
readonly maxSamples: number;
|
|
38
|
+
}): JudgeScorer;
|
|
39
|
+
//# sourceMappingURL=judge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"judge.d.ts","sourceRoot":"","sources":["../../src/bakeoff/judge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAejE;+EAC+E;AAC/E,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,GAAG,WAAW,CAezF;AAED;gFACgF;AAChF,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,IAAI,IAAI,MAAM,CAAC;CAChB;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,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,WAAW,CAqCb"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SDK-side bake-off judge (RB-1, near-optimal spec §4.3, tier 3). When a sample
|
|
4
|
+
* has NO eval-case expected value and NO declared value check, an opt-in judge
|
|
5
|
+
* (`bakeoff.judge: { model, max_samples }`) scores the arm's output on value.
|
|
6
|
+
*
|
|
7
|
+
* How it stays honest and bounded, reusing the real run loop (no new dispatch
|
|
8
|
+
* path, no run.ts edit):
|
|
9
|
+
* - The judge is a normal `run()` against a synthetic single-candidate route
|
|
10
|
+
* whose only candidate is the judge model — so the call is METERED exactly
|
|
11
|
+
* like any inference (a telemetry attempt row with real cost), tagged
|
|
12
|
+
* `rung: "judge"`, and no capture is written (captures severed).
|
|
13
|
+
* - CAPPED at `max_samples` PER ARM: past the cap `score()` returns null and
|
|
14
|
+
* the sample falls back to conformance (counted by omission).
|
|
15
|
+
* - The judge model MUST be a declared candidate (#223 F3) — enforced at load
|
|
16
|
+
* (objective-loader.ts), so it is a real, reachable model here.
|
|
17
|
+
* - Fail-closed: an unparseable/short-of-schema judge answer throws in the run
|
|
18
|
+
* loop; the metered row still stands and the sample's score is null.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.buildJudgeRoute = buildJudgeRoute;
|
|
22
|
+
exports.makeJudgeScorer = makeJudgeScorer;
|
|
23
|
+
const node_crypto_1 = require("node:crypto");
|
|
24
|
+
const run_1 = require("../run/run");
|
|
25
|
+
const failures_1 = require("../types/failures");
|
|
26
|
+
const JUDGE_SCHEMA = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: { score: { type: "number" }, reason: { type: "string" } },
|
|
29
|
+
required: ["score"],
|
|
30
|
+
additionalProperties: true,
|
|
31
|
+
};
|
|
32
|
+
const JUDGE_RUBRIC = "You are grading ONE candidate output for a task, for VALUE ACCURACY — how correct and " +
|
|
33
|
+
"complete the answer is, not merely whether it is well-formed. Score from 0 (wrong/empty) " +
|
|
34
|
+
"to 1 (fully correct).\n\nTASK INPUT:\n{{task_input}}\n\nCANDIDATE OUTPUT:\n{{candidate_output}}\n\n" +
|
|
35
|
+
'Respond with ONLY compact JSON: {"score": <number 0..1>, "reason": "<one sentence>"}.';
|
|
36
|
+
/** Build the synthetic judge route for a base route + judge model. The judge
|
|
37
|
+
* model is declared WITHOUT its trial flag so it serves live on this route. */
|
|
38
|
+
function buildJudgeRoute(baseRoute, model) {
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
route: `__bakeoff_judge__:${baseRoute.route}`,
|
|
41
|
+
version: baseRoute.version,
|
|
42
|
+
outputSchema: Object.freeze({ ...JUDGE_SCHEMA }),
|
|
43
|
+
candidates: Object.freeze([{ provider: model.provider, model: model.model }]),
|
|
44
|
+
require: Object.freeze([]),
|
|
45
|
+
prefer: Object.freeze([]),
|
|
46
|
+
systemTemplate: JUDGE_RUBRIC,
|
|
47
|
+
variables: Object.freeze(["candidate_output", "task_input"]),
|
|
48
|
+
policy: Object.freeze({ retries: Object.freeze({}), timeoutMs: baseRoute.policy.timeoutMs, tier: "standard", json: "json_mode" }),
|
|
49
|
+
variants: Object.freeze([]),
|
|
50
|
+
capture: false,
|
|
51
|
+
fingerprintCell: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function clamp01(n) {
|
|
55
|
+
return typeof n === "number" && Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a judge scorer over the run loop. `deps` are the bake-off's deps (the
|
|
59
|
+
* judge model's adapter must be present, as for any candidate). Each arm gets a
|
|
60
|
+
* FRESH scorer so the `max_samples` cap is per arm (spec: max_samples per arm).
|
|
61
|
+
*/
|
|
62
|
+
function makeJudgeScorer(deps, baseRoute, judge) {
|
|
63
|
+
const judgeRoute = buildJudgeRoute(baseRoute, judge.model);
|
|
64
|
+
const routes = {
|
|
65
|
+
get: (r) => (r === judgeRoute.route ? judgeRoute : deps.routes.get(r)),
|
|
66
|
+
list: () => Object.freeze([...deps.routes.list(), judgeRoute]),
|
|
67
|
+
};
|
|
68
|
+
// Judge dispatches are real, billed calls — bounded by their own envelope and
|
|
69
|
+
// never written to captures (no replay-of-replay). live mode: the judge model
|
|
70
|
+
// is a plain declared candidate on the synthetic route.
|
|
71
|
+
const judgeDeps = { ...deps, routes, captures: null, mode: "live" };
|
|
72
|
+
const envelope = `bakeoff-judge:${(0, node_crypto_1.randomUUID)()}`;
|
|
73
|
+
let used = 0;
|
|
74
|
+
return {
|
|
75
|
+
used: () => used,
|
|
76
|
+
async score(output, taskInput) {
|
|
77
|
+
if (used >= judge.maxSamples)
|
|
78
|
+
return null;
|
|
79
|
+
used += 1;
|
|
80
|
+
try {
|
|
81
|
+
const result = await (0, run_1.run)(judgeDeps, judgeRoute.route, {
|
|
82
|
+
input: {
|
|
83
|
+
candidate_output: typeof output === "string" ? output : JSON.stringify(output ?? null),
|
|
84
|
+
task_input: typeof taskInput === "string" ? taskInput : JSON.stringify(taskInput ?? null),
|
|
85
|
+
},
|
|
86
|
+
// `bakeoff_judge` marks the row a judge inference (the typed `rung`
|
|
87
|
+
// column is recorder-owned, so a distinct tag key is used); `lane:
|
|
88
|
+
// trial` keeps judge spend off the route's live rollups/odometer.
|
|
89
|
+
tags: { bakeoff_judge: "1", lane: "trial" },
|
|
90
|
+
budget: { envelope },
|
|
91
|
+
});
|
|
92
|
+
return clamp01(result.output?.score);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (!(err instanceof failures_1.RigFailureError))
|
|
96
|
+
throw err;
|
|
97
|
+
// The metered attempt row(s) still stand; this sample gets no judge score.
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-sample value-accuracy resolution for a bake-off (RB-1, near-optimal spec
|
|
3
|
+
* §4.3). DETERMINISTIC tiers only — the judge tier lives in judge.ts because it
|
|
4
|
+
* dispatches a metered inference. Precedence:
|
|
5
|
+
*
|
|
6
|
+
* 1. eval-case expected value (a corrected/golden artifact) ⇒ exact/normalized
|
|
7
|
+
* match for a primitive, or schema-aware field accuracy in [0,1] for an
|
|
8
|
+
* object/array.
|
|
9
|
+
* 2. declared `value_checks` (JSON-pointer assertions) ⇒ fraction passing.
|
|
10
|
+
* 3. null (the caller falls back to the judge, then to conformance).
|
|
11
|
+
*
|
|
12
|
+
* Pure; unit-tested directly. F5 (deterministic before judge) is exactly this
|
|
13
|
+
* ordering.
|
|
14
|
+
*/
|
|
15
|
+
import type { ValueCheck } from "../types/route";
|
|
16
|
+
/**
|
|
17
|
+
* Schema-aware field accuracy in [0,1]: the fraction of the EXPECTED value's leaf
|
|
18
|
+
* fields that the actual output matches. Recurses into objects/arrays keyed by
|
|
19
|
+
* the expected shape (extra keys in `actual` are ignored — the golden defines
|
|
20
|
+
* what "correct" covers). A scalar expected ⇒ 1 or 0. An empty object/array
|
|
21
|
+
* expected ⇒ 1 (nothing to be wrong about).
|
|
22
|
+
*/
|
|
23
|
+
export declare function fieldAccuracy(expected: unknown, actual: unknown): number;
|
|
24
|
+
/** Resolve an RFC 6901 JSON pointer against a value. Returns `undefined` when any
|
|
25
|
+
* segment is missing (a missing field fails its value check). */
|
|
26
|
+
export declare function resolveJsonPointer(value: unknown, pointer: string): unknown;
|
|
27
|
+
/** True when `output` satisfies one value check (RFC 6901 pointer + literal|regex). */
|
|
28
|
+
export declare function checkPasses(output: unknown, check: ValueCheck): boolean;
|
|
29
|
+
export interface DeterministicQualityInput {
|
|
30
|
+
readonly output: unknown;
|
|
31
|
+
readonly valueChecks?: readonly ValueCheck[];
|
|
32
|
+
/** The corrected/golden expected output for this capture, when a bake-off runs
|
|
33
|
+
* from eval cases with a corrected artifact. */
|
|
34
|
+
readonly expected?: unknown;
|
|
35
|
+
/** Set when `expected` is meaningfully present (distinguishes a golden `null`
|
|
36
|
+
* expected value from "no eval case"). */
|
|
37
|
+
readonly hasExpected?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Deterministic value accuracy for one sample, or null when neither an eval-case
|
|
41
|
+
* expected value nor a value check applies (the caller then tries the judge).
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveDeterministicQuality(input: DeterministicQualityInput): number | null;
|
|
44
|
+
//# sourceMappingURL=quality.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quality.d.ts","sourceRoot":"","sources":["../../src/bakeoff/quality.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAiBjD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAgBxE;AAED;iEACiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAmB3E;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAWvE;AAYD,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7C;oDACgD;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;8CAC0C;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,yBAAyB,GAAG,MAAM,GAAG,IAAI,CAU3F"}
|