modelrig 0.4.0 → 0.6.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.
Files changed (189) hide show
  1. package/CHANGELOG.md +238 -0
  2. package/README.md +65 -3
  3. package/dist/adapters/anthropic.d.ts.map +1 -1
  4. package/dist/adapters/anthropic.js +36 -7
  5. package/dist/adapters/attempt-meta-map.d.ts +63 -0
  6. package/dist/adapters/attempt-meta-map.d.ts.map +1 -0
  7. package/dist/adapters/attempt-meta-map.js +154 -0
  8. package/dist/adapters/deepseek.d.ts.map +1 -1
  9. package/dist/adapters/deepseek.js +25 -2
  10. package/dist/adapters/gemini.d.ts +39 -0
  11. package/dist/adapters/gemini.d.ts.map +1 -1
  12. package/dist/adapters/gemini.js +82 -4
  13. package/dist/adapters/grok.d.ts.map +1 -1
  14. package/dist/adapters/grok.js +25 -2
  15. package/dist/adapters/openai-compat.d.ts.map +1 -1
  16. package/dist/adapters/openai-compat.js +25 -2
  17. package/dist/adapters/openai-stream.d.ts +2 -0
  18. package/dist/adapters/openai-stream.d.ts.map +1 -1
  19. package/dist/adapters/openai-stream.js +23 -3
  20. package/dist/adapters/openai.d.ts.map +1 -1
  21. package/dist/adapters/openai.js +36 -6
  22. package/dist/artifacts/artifact.d.ts +3 -2
  23. package/dist/artifacts/artifact.d.ts.map +1 -1
  24. package/dist/artifacts/artifact.js +35 -9
  25. package/dist/artifacts/classification.d.ts +27 -0
  26. package/dist/artifacts/classification.d.ts.map +1 -0
  27. package/dist/artifacts/classification.js +38 -0
  28. package/dist/artifacts/index.d.ts +3 -1
  29. package/dist/artifacts/index.d.ts.map +1 -1
  30. package/dist/artifacts/index.js +10 -1
  31. package/dist/artifacts/run-context.d.ts +33 -2
  32. package/dist/artifacts/run-context.d.ts.map +1 -1
  33. package/dist/artifacts/run-context.js +95 -1
  34. package/dist/artifacts/scrub.d.ts +26 -0
  35. package/dist/artifacts/scrub.d.ts.map +1 -1
  36. package/dist/artifacts/scrub.js +53 -0
  37. package/dist/artifacts/step.d.ts +9 -0
  38. package/dist/artifacts/step.d.ts.map +1 -1
  39. package/dist/artifacts/step.js +9 -0
  40. package/dist/bakeoff/engine.d.ts +46 -4
  41. package/dist/bakeoff/engine.d.ts.map +1 -1
  42. package/dist/bakeoff/engine.js +269 -19
  43. package/dist/bakeoff/eval-cases.d.ts +5 -0
  44. package/dist/bakeoff/eval-cases.d.ts.map +1 -1
  45. package/dist/bakeoff/eval-cases.js +3 -0
  46. package/dist/bakeoff/judge.d.ts +39 -0
  47. package/dist/bakeoff/judge.d.ts.map +1 -0
  48. package/dist/bakeoff/judge.js +102 -0
  49. package/dist/bakeoff/quality.d.ts +44 -0
  50. package/dist/bakeoff/quality.d.ts.map +1 -0
  51. package/dist/bakeoff/quality.js +126 -0
  52. package/dist/bakeoff/reference.d.ts +34 -0
  53. package/dist/bakeoff/reference.d.ts.map +1 -0
  54. package/dist/bakeoff/reference.js +68 -0
  55. package/dist/bakeoff/replay.d.ts +25 -0
  56. package/dist/bakeoff/replay.d.ts.map +1 -1
  57. package/dist/bakeoff/replay.js +65 -2
  58. package/dist/bakeoff/report.d.ts +8 -1
  59. package/dist/bakeoff/report.d.ts.map +1 -1
  60. package/dist/bakeoff/report.js +83 -12
  61. package/dist/bakeoff/samples.d.ts +35 -2
  62. package/dist/bakeoff/samples.d.ts.map +1 -1
  63. package/dist/bakeoff/samples.js +67 -6
  64. package/dist/bakeoff/stats.d.ts +17 -1
  65. package/dist/bakeoff/stats.d.ts.map +1 -1
  66. package/dist/bakeoff/stats.js +67 -0
  67. package/dist/bakeoff/types.d.ts +76 -5
  68. package/dist/bakeoff/types.d.ts.map +1 -1
  69. package/dist/bakeoff/types.js +4 -3
  70. package/dist/cli-query.d.ts +14 -0
  71. package/dist/cli-query.d.ts.map +1 -0
  72. package/dist/cli-query.js +59 -0
  73. package/dist/cli.d.ts +2 -1
  74. package/dist/cli.d.ts.map +1 -1
  75. package/dist/cli.js +47 -24
  76. package/dist/config.d.ts +6 -0
  77. package/dist/config.d.ts.map +1 -1
  78. package/dist/config.js +14 -0
  79. package/dist/detect/detector.d.ts +82 -0
  80. package/dist/detect/detector.d.ts.map +1 -0
  81. package/dist/detect/detector.js +23 -0
  82. package/dist/index.d.ts +34 -23
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +49 -12
  85. package/dist/query/analytics.d.ts +84 -0
  86. package/dist/query/analytics.d.ts.map +1 -0
  87. package/dist/query/analytics.js +280 -0
  88. package/dist/query/columns.d.ts +59 -0
  89. package/dist/query/columns.d.ts.map +1 -0
  90. package/dist/query/columns.js +89 -0
  91. package/dist/query/grammar.d.ts +74 -7
  92. package/dist/query/grammar.d.ts.map +1 -1
  93. package/dist/query/grammar.js +179 -71
  94. package/dist/query/saved.d.ts +26 -0
  95. package/dist/query/saved.d.ts.map +1 -0
  96. package/dist/query/saved.js +51 -0
  97. package/dist/registry/select.d.ts +59 -0
  98. package/dist/registry/select.d.ts.map +1 -1
  99. package/dist/registry/select.js +66 -0
  100. package/dist/routes/loader-warnings.d.ts +28 -0
  101. package/dist/routes/loader-warnings.d.ts.map +1 -0
  102. package/dist/routes/loader-warnings.js +56 -0
  103. package/dist/routes/loader.d.ts +1 -11
  104. package/dist/routes/loader.d.ts.map +1 -1
  105. package/dist/routes/loader.js +54 -31
  106. package/dist/routes/objective-loader.d.ts +35 -0
  107. package/dist/routes/objective-loader.d.ts.map +1 -0
  108. package/dist/routes/objective-loader.js +286 -0
  109. package/dist/run/accept.d.ts +43 -0
  110. package/dist/run/accept.d.ts.map +1 -0
  111. package/dist/run/accept.js +65 -0
  112. package/dist/run/cost.d.ts +13 -0
  113. package/dist/run/cost.d.ts.map +1 -1
  114. package/dist/run/cost.js +27 -0
  115. package/dist/run/dispatch.d.ts +24 -1
  116. package/dist/run/dispatch.d.ts.map +1 -1
  117. package/dist/run/dispatch.js +19 -0
  118. package/dist/run/ladder-key.d.ts +4 -0
  119. package/dist/run/ladder-key.d.ts.map +1 -1
  120. package/dist/run/ladder-key.js +3 -0
  121. package/dist/run/quality-gate.d.ts +39 -0
  122. package/dist/run/quality-gate.d.ts.map +1 -0
  123. package/dist/run/quality-gate.js +70 -0
  124. package/dist/run/raw-defaults.d.ts +52 -0
  125. package/dist/run/raw-defaults.d.ts.map +1 -0
  126. package/dist/run/raw-defaults.js +123 -0
  127. package/dist/run/raw.d.ts +12 -0
  128. package/dist/run/raw.d.ts.map +1 -1
  129. package/dist/run/raw.js +107 -8
  130. package/dist/run/record-attempt.d.ts.map +1 -1
  131. package/dist/run/record-attempt.js +22 -1
  132. package/dist/run/resolve.d.ts +17 -1
  133. package/dist/run/resolve.d.ts.map +1 -1
  134. package/dist/run/resolve.js +20 -4
  135. package/dist/run/routing-docs.d.ts.map +1 -1
  136. package/dist/run/routing-docs.js +12 -5
  137. package/dist/run/run.d.ts +7 -1
  138. package/dist/run/run.d.ts.map +1 -1
  139. package/dist/run/run.js +54 -69
  140. package/dist/run/variant.d.ts +43 -0
  141. package/dist/run/variant.d.ts.map +1 -0
  142. package/dist/run/variant.js +86 -0
  143. package/dist/savings/emit.d.ts +33 -0
  144. package/dist/savings/emit.d.ts.map +1 -1
  145. package/dist/savings/emit.js +75 -3
  146. package/dist/settings/index.d.ts +1 -1
  147. package/dist/settings/index.d.ts.map +1 -1
  148. package/dist/settings/index.js +2 -1
  149. package/dist/settings/task-settings.d.ts +10 -0
  150. package/dist/settings/task-settings.d.ts.map +1 -1
  151. package/dist/settings/task-settings.js +17 -0
  152. package/dist/telemetry/attempt-meta.d.ts +178 -0
  153. package/dist/telemetry/attempt-meta.d.ts.map +1 -0
  154. package/dist/telemetry/attempt-meta.js +319 -0
  155. package/dist/telemetry/exporter.d.ts.map +1 -1
  156. package/dist/telemetry/exporter.js +6 -0
  157. package/dist/telemetry/ingest-protocol.d.ts +11 -0
  158. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  159. package/dist/telemetry/ingest-protocol.js +3 -0
  160. package/dist/telemetry/otel-map.d.ts +10 -0
  161. package/dist/telemetry/otel-map.d.ts.map +1 -1
  162. package/dist/telemetry/otel-map.js +64 -0
  163. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  164. package/dist/telemetry/store-migrations.js +15 -0
  165. package/dist/telemetry/store.d.ts.map +1 -1
  166. package/dist/telemetry/store.js +25 -7
  167. package/dist/types/failures-teaching.d.ts.map +1 -1
  168. package/dist/types/failures-teaching.js +20 -0
  169. package/dist/types/failures.d.ts +1 -1
  170. package/dist/types/failures.d.ts.map +1 -1
  171. package/dist/types/failures.js +1 -0
  172. package/dist/types/provider.d.ts +9 -0
  173. package/dist/types/provider.d.ts.map +1 -1
  174. package/dist/types/quality.d.ts +41 -0
  175. package/dist/types/quality.d.ts.map +1 -0
  176. package/dist/types/quality.js +12 -0
  177. package/dist/types/route.d.ts +120 -0
  178. package/dist/types/route.d.ts.map +1 -1
  179. package/dist/types/telemetry.d.ts +13 -0
  180. package/dist/types/telemetry.d.ts.map +1 -1
  181. package/dist/validate-cli.d.ts.map +1 -1
  182. package/dist/validate-cli.js +10 -0
  183. package/dist/watch/evaluate.d.ts +27 -1
  184. package/dist/watch/evaluate.d.ts.map +1 -1
  185. package/dist/watch/evaluate.js +13 -0
  186. package/dist/watch/proposals.d.ts +28 -0
  187. package/dist/watch/proposals.d.ts.map +1 -1
  188. package/dist/watch/proposals.js +51 -0
  189. package/package.json +1 -1
@@ -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
- const proposal = computeProposal(req.variants, metrics);
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
- /** Winner among gate-passing challengers; null when nobody beats the
107
- * incumbent. Exported for direct unit testing. */
108
- function computeProposal(requested, metrics) {
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
- const challengers = metrics.filter((m) => m.variant !== incumbentName &&
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
- m.conformCi95[0] >= incumbent.conformCi95[0] &&
119
- m.effectiveUsdPer1kConformant < incumbent.effectiveUsdPer1kConformant);
120
- if (challengers.length === 0)
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 = [...challengers].sort((a, b) => a.effectiveUsdPer1kConformant - b.effectiveUsdPer1kConformant)[0];
123
- const savings = incumbent.effectiveUsdPer1kConformant === 0 ||
124
- !Number.isFinite(incumbent.effectiveUsdPer1kConformant)
125
- ? 0
126
- : ((incumbent.effectiveUsdPer1kConformant - winner.effectiveUsdPer1kConformant) /
127
- incumbent.effectiveUsdPer1kConformant) *
128
- 100;
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":"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"}
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"}