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
package/dist/run/run.js CHANGED
@@ -17,6 +17,7 @@ const pricing_1 = require("../registry/pricing");
17
17
  const failures_1 = require("../types/failures");
18
18
  const manifest_1 = require("../routes/manifest");
19
19
  const resolve_1 = require("./resolve");
20
+ const variant_1 = require("./variant");
20
21
  const envelope_1 = require("./envelope");
21
22
  const retry_1 = require("./retry");
22
23
  const repair_1 = require("./repair");
@@ -25,37 +26,10 @@ const validate_1 = require("./validate");
25
26
  const dispatch_1 = require("./dispatch");
26
27
  const record_attempt_1 = require("./record-attempt");
27
28
  const ladder_key_1 = require("./ladder-key");
29
+ const quality_gate_1 = require("./quality-gate");
30
+ const accept_1 = require("./accept");
28
31
  const run_context_1 = require("../artifacts/run-context");
29
32
  const capture_write_1 = require("./capture-write");
30
- /** Look up a named variant; null = default (base config). Unknown names are
31
- * config errors — the caller asked for something the bundle doesn't define. */
32
- function resolveVariant(route, name) {
33
- if (name === undefined || name === "default")
34
- return null;
35
- const variant = route.variants.find((v) => v.name === name);
36
- if (variant === undefined) {
37
- throw new loader_1.RouteConfigError(`route "${route.route}" has no variant "${name}" — defined: [${route.variants
38
- .map((v) => v.name)
39
- .join(", ")}]`);
40
- }
41
- return variant;
42
- }
43
- /** The variant-effective view of a route: narrowed candidates, overridden
44
- * json rung, overridden template. Loader validation guarantees variant
45
- * candidates ⊆ declared candidates, so the candidate-set invariant holds. */
46
- function effectiveRoute(route, variant) {
47
- if (variant === null)
48
- return route;
49
- return Object.freeze({
50
- ...route,
51
- candidates: variant.candidates ?? route.candidates,
52
- systemTemplate: variant.promptOverride ?? route.systemTemplate,
53
- policy: Object.freeze({
54
- ...route.policy,
55
- json: variant.json ?? route.policy.json,
56
- }),
57
- });
58
- }
59
33
  /** Phase 1 prompt convention: the bundle's system template carries the whole
60
34
  * task (constitution, data, step instructions); the user turn is a terse
61
35
  * trigger. Providers treat long system prompts + short user turns well, and
@@ -73,10 +47,11 @@ function costPerMTok(provider, model) {
73
47
  }
74
48
  const DEFAULT_SLEEP = (ms) => ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms));
75
49
  async function run(deps, routeName, opts) {
50
+ const mode = deps.mode ?? "live";
76
51
  const baseRoute = deps.routes.get(routeName);
77
- const variant = resolveVariant(baseRoute, opts.variant);
52
+ const variant = (0, variant_1.resolveVariant)(baseRoute, opts.variant, mode);
78
53
  const servedVariant = variant?.name ?? null;
79
- const route = (0, resolve_1.applyRunRequirements)(effectiveRoute(baseRoute, variant), opts.zeroRetention === true);
54
+ const route = (0, resolve_1.applyRunRequirements)((0, variant_1.effectiveRoute)(baseRoute, variant), opts.zeroRetention === true);
80
55
  const effectiveTags = route.require.includes("zero_retention") ? { ...opts.tags, zdr_enforced: "true" } : opts.tags;
81
56
  // Data-hygiene warning (spec WS5): once per process per dimension.
82
57
  (0, manifest_1.warnMissingRequiredDimensions)(deps.manifest ?? null, opts.tags);
@@ -96,7 +71,7 @@ async function run(deps, routeName, opts) {
96
71
  // exit that records an inference also records a ground-truth step referencing
97
72
  // it (attempt-per-row, ruling c). Inert without a run.start on this path.
98
73
  const runCtx = (0, run_context_1.getActiveRunContext)();
99
- const autoStepKey = runCtx !== null ? runCtx.nextStepKey(route.route) : null;
74
+ const autoStepKey = runCtx !== null ? (0, run_context_1.resolveStepKey)(opts.stepKey, opts.tags.step, () => runCtx.nextStepKey(route.route)) : null;
100
75
  // Per-call ladder key (spec §2.1 R0): one call_id, a monotonic attempt_seq per
101
76
  // row, and the current candidate_index (advanced by nextCandidate below).
102
77
  const ladder = (0, ladder_key_1.createLadderKeyMinter)();
@@ -146,6 +121,14 @@ async function run(deps, routeName, opts) {
146
121
  systemPrompt = originalSystemPrompt + (0, repair_1.repairFeedbackBlock)(errors);
147
122
  }
148
123
  else {
124
+ // Declare-first backstop (spec §4.4): the loader rejects a `trial` candidate
125
+ // as `repair_model`, but a programmatically-built bundle (runBundle) could
126
+ // bypass the loader — so refuse to mint a trial candidate for LIVE repair
127
+ // here too (the primary ladder's trial-drop has no repair-path equivalent).
128
+ if (mode === "live" &&
129
+ route.candidates.some((c) => c.provider === step.model.provider && c.model === step.model.model && c.trial === true)) {
130
+ return null;
131
+ }
149
132
  // Brand stays in resolve.ts; loader validated repairModel ⊆ declared.
150
133
  const minted = (0, resolve_1.resolveDeclaredCandidate)(route, step.model);
151
134
  if (minted === null)
@@ -190,6 +173,7 @@ async function run(deps, routeName, opts) {
190
173
  failure: result.failure,
191
174
  repairedBy,
192
175
  pricingMissing: repairFailurePricingMissing,
176
+ metaInputs: (0, dispatch_1.attemptMetaInputs)({ systemPrompt, userDirective: REPAIR_DIRECTIVE, outputSchema: route.outputSchema, result }),
193
177
  });
194
178
  if (failures_1.TERMINAL_CLASSES.has(result.failure.class)) {
195
179
  throw new failures_1.RigFailureError(result.failure);
@@ -221,6 +205,7 @@ async function run(deps, routeName, opts) {
221
205
  failure,
222
206
  repairedBy,
223
207
  pricingMissing: repairPricingMissing,
208
+ metaInputs: (0, dispatch_1.attemptMetaInputs)({ systemPrompt, userDirective: REPAIR_DIRECTIVE, outputSchema: route.outputSchema, result }),
224
209
  });
225
210
  captureAttempt(inferenceId, result.text);
226
211
  if (!outcome.valid) {
@@ -255,7 +240,7 @@ async function run(deps, routeName, opts) {
255
240
  };
256
241
  }
257
242
  const searchAvailable = deps.search !== undefined && deps.search !== null;
258
- const iter = (0, resolve_1.resolveCandidates)(route, deps.capabilities, costPerMTok, searchAvailable, (0, resolve_1.candidateFilterFromOptions)(opts));
243
+ const iter = (0, resolve_1.resolveCandidates)(route, deps.capabilities, costPerMTok, searchAvailable, (0, resolve_1.candidateFilterFromOptions)(opts), undefined, mode);
259
244
  // Advance the iterator AND publish the served candidate's index to the ladder,
260
245
  // so every row of this candidate's turn (incl. repair rows) carries it (§2.1).
261
246
  const nextCandidate = () => {
@@ -405,47 +390,45 @@ async function run(deps, routeName, opts) {
405
390
  servedVariant,
406
391
  pricingMissing,
407
392
  callId: ladder.callId,
393
+ metaInputs: (0, dispatch_1.attemptMetaInputs)({ systemPrompt, userDirective: USER_DIRECTIVE, outputSchema: route.outputSchema, result, tools: opts.tools }),
408
394
  });
409
395
  }
410
396
  const outcome = (0, validate_1.validateOutput)(route, result.text);
397
+ const metaInputs = (0, dispatch_1.attemptMetaInputs)({ systemPrompt, userDirective: USER_DIRECTIVE, outputSchema: route.outputSchema, result, tools: opts.tools });
411
398
  if (outcome.valid) {
412
- const inferenceId = recordAttempt({
413
- candidate,
414
- servedTier: result.usage.servedTier,
415
- tokensIn: result.usage.tokensIn,
416
- tokensOut: result.usage.tokensOut,
417
- tokensCached: result.usage.tokensCached,
418
- tokensCacheWrite: result.usage.tokensCacheWrite ?? 0,
419
- tokensCacheWrite1h: result.usage.tokensCacheWrite1h ?? 0,
420
- costUsd,
421
- latencyMs,
422
- ...(result.usage.ttfbMs !== undefined ? { ttfbMs: result.usage.ttfbMs } : {}),
423
- failure: null,
424
- pricingMissing,
425
- });
399
+ // Q1 quality gate (spec §4.1): a schema-valid output must still clear the
400
+ // builder's bar before it is accepted. Tool-call turns never reach here;
401
+ // a `schema: null` route gates on the parsed JSON just the same. Gate
402
+ // logic (async await, fail-closed throw, tag/message shaping) lives in
403
+ // ./quality-gate.ts so this stays ONE seam.
404
+ const gateMeta = { route: route.route, provider: candidate.provider, model: candidate.model,
405
+ candidateIndex: iter.index, attemptSeq: ladder.peekNextSeq(), servedVariant };
406
+ const gate = opts.qualityGate === undefined
407
+ ? null
408
+ : await (0, quality_gate_1.evaluateQualityGate)(opts.qualityGate, outcome.parsed, gateMeta);
409
+ if (gate !== null && !gate.pass) {
410
+ // Rejected on quality: its OWN class + budget; the repair rung is NEVER
411
+ // entered from here (spec §4.1 step 3). The rejected text is captured
412
+ // so it stays replayable.
413
+ const failure = { class: "quality_rejected",
414
+ message: (0, quality_gate_1.qualityRejectedMessage)(gate), provider: candidate.provider, model: candidate.model };
415
+ const rejectedId = recordAttempt((0, accept_1.successAttemptRow)(candidate, result.usage, costUsd, latencyMs, pricingMissing, failure, metaInputs, (0, quality_gate_1.qualityTags)(gate)));
416
+ captureAttempt(rejectedId, result.text);
417
+ lastFailure = failure;
418
+ if (ledger.consume("quality_rejected"))
419
+ continue; // re-sample the SAME candidate
420
+ candidate = nextCandidate();
421
+ continue;
422
+ }
423
+ const acceptTags = gate !== null ? (0, quality_gate_1.qualityTags)(gate) : undefined;
424
+ const inferenceId = recordAttempt((0, accept_1.successAttemptRow)(candidate, result.usage, costUsd, latencyMs, pricingMissing, null, metaInputs, acceptTags));
426
425
  captureAttempt(inferenceId, result.text);
427
- const meta = {
428
- inferenceId,
429
- route: route.route,
430
- routeVersion: route.version,
431
- provider: candidate.provider,
432
- model: candidate.model,
433
- requestedTier: route.policy.tier,
434
- servedTier: result.usage.servedTier,
435
- tokensIn: result.usage.tokensIn,
436
- tokensOut: result.usage.tokensOut,
437
- tokensCached: result.usage.tokensCached,
438
- costEstimateUsd: costUsd,
439
- latencyMs,
440
- validated: true,
441
- attemptsByClass: ledger.snapshot(),
442
- servedVariant,
443
- repairedBy: null,
444
- callId: ladder.callId,
445
- ...(injectedSources !== null
446
- ? { citations: (0, grounding_1.extractCitations)(result.text, injectedSources) }
447
- : {}),
448
- };
426
+ const meta = (0, accept_1.buildSuccessMeta)({
427
+ inferenceId, route, candidate, usage: result.usage, costUsd, latencyMs,
428
+ attemptsByClass: ledger.snapshot(), servedVariant, callId: ladder.callId,
429
+ ...(injectedSources !== null ? { citations: (0, grounding_1.extractCitations)(result.text, injectedSources) } : {}),
430
+ ...(gate !== null ? { quality: { score: gate.score, verdict: "pass", judgedBy: null } } : {}),
431
+ });
449
432
  return { output: outcome.parsed, meta };
450
433
  }
451
434
  // Schema-invalid output — a real (billed) attempt that failed.
@@ -468,6 +451,7 @@ async function run(deps, routeName, opts) {
468
451
  ...(result.usage.ttfbMs !== undefined ? { ttfbMs: result.usage.ttfbMs } : {}),
469
452
  failure,
470
453
  pricingMissing,
454
+ metaInputs, // computed once above (same `result`) — reused across valid/invalid
471
455
  });
472
456
  captureAttempt(invalidId, result.text);
473
457
  lastFailure = failure;
@@ -520,6 +504,7 @@ async function run(deps, routeName, opts) {
520
504
  ...(result.usage?.ttfbMs !== undefined ? { ttfbMs: result.usage.ttfbMs } : {}),
521
505
  failure,
522
506
  pricingMissing: failurePricingMissing,
507
+ metaInputs: (0, dispatch_1.attemptMetaInputs)({ systemPrompt, userDirective: USER_DIRECTIVE, outputSchema: route.outputSchema, result, tools: opts.tools }),
523
508
  });
524
509
  if (failures_1.TERMINAL_CLASSES.has(failure.class)) {
525
510
  throw new failures_1.RigFailureError(failure);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Variant resolution — extracted from run.ts (spec §4.4, R3) to host the
3
+ * declare-first "a trial variant selected live is a config error" rule and to
4
+ * keep run.ts under the LOC cap. Pure: bundle + requested name (+ run mode) →
5
+ * the resolved variant and the variant-effective route view.
6
+ */
7
+ import type { RouteBundle, RouteVariant } from "../types/route";
8
+ import type { RunMode } from "./resolve";
9
+ /**
10
+ * A variant is trial-only when it has candidates and EVERY one maps to a
11
+ * declared `trial` candidate. Computed dynamically from `route.candidates`
12
+ * rather than trusted from the stored `variant.trialOnly` stamp, so a bundle
13
+ * built in code — a programmatic `runBundle` whose variants never passed
14
+ * through the loader that sets the flag — is judged exactly like a loaded one
15
+ * (and still earns the teaching error on a live trial selection). The loader's
16
+ * stamp, when present, is honored as a fast path.
17
+ */
18
+ export declare function isTrialOnlyVariant(route: RouteBundle, variant: RouteVariant): boolean;
19
+ /**
20
+ * Look up a named variant; null = default (base config). Unknown names are
21
+ * config errors — the caller asked for something the bundle doesn't define.
22
+ *
23
+ * Declare-first (spec §4.4): a `trialOnly` variant — one whose candidates are
24
+ * ALL declared `trial` candidates — is valid only in bake-off replay
25
+ * (`mode: "trial"`). Selecting it on a LIVE path is a `RouteConfigError` with a
26
+ * teaching message that names the bake-off, never a silent fallback (residual
27
+ * flag F2′): the run loop must not dispatch an undeclared OR trial model live.
28
+ */
29
+ export declare function resolveVariant(route: RouteBundle, name: string | undefined, mode?: RunMode): RouteVariant | null;
30
+ /**
31
+ * The variant-effective view of a route: narrowed candidates, overridden json
32
+ * rung, overridden template. Loader validation guarantees variant candidates ⊆
33
+ * declared candidates, so the candidate-set invariant holds.
34
+ *
35
+ * Declare-first (spec §4.4): a variant's `candidates` entries are authored as
36
+ * bare `{provider, model}` and do NOT carry the route-level `trial` flag. We
37
+ * MUST re-attach it from the matching declared candidate — otherwise a variant
38
+ * that narrows to a trial candidate (e.g. a mixed trial+live variant, which is
39
+ * not `trialOnly` and IS selectable live) would strip the flag and
40
+ * `resolveCandidates`' live trial-drop would let the trial model serve live.
41
+ */
42
+ export declare function effectiveRoute(route: RouteBundle, variant: RouteVariant | null): RouteBundle;
43
+ //# sourceMappingURL=variant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variant.d.ts","sourceRoot":"","sources":["../../src/run/variant.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAYrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,IAAI,GAAE,OAAgB,GACrB,YAAY,GAAG,IAAI,CAkBrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,WAAW,CAoB5F"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Variant resolution — extracted from run.ts (spec §4.4, R3) to host the
4
+ * declare-first "a trial variant selected live is a config error" rule and to
5
+ * keep run.ts under the LOC cap. Pure: bundle + requested name (+ run mode) →
6
+ * the resolved variant and the variant-effective route view.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.isTrialOnlyVariant = isTrialOnlyVariant;
10
+ exports.resolveVariant = resolveVariant;
11
+ exports.effectiveRoute = effectiveRoute;
12
+ const loader_1 = require("../routes/loader");
13
+ /**
14
+ * A variant is trial-only when it has candidates and EVERY one maps to a
15
+ * declared `trial` candidate. Computed dynamically from `route.candidates`
16
+ * rather than trusted from the stored `variant.trialOnly` stamp, so a bundle
17
+ * built in code — a programmatic `runBundle` whose variants never passed
18
+ * through the loader that sets the flag — is judged exactly like a loaded one
19
+ * (and still earns the teaching error on a live trial selection). The loader's
20
+ * stamp, when present, is honored as a fast path.
21
+ */
22
+ function isTrialOnlyVariant(route, variant) {
23
+ if (variant.trialOnly === true)
24
+ return true;
25
+ const cands = variant.candidates;
26
+ return (cands !== undefined &&
27
+ cands.length > 0 &&
28
+ cands.every((vc) => route.candidates.some((rc) => rc.provider === vc.provider && rc.model === vc.model && rc.trial === true)));
29
+ }
30
+ /**
31
+ * Look up a named variant; null = default (base config). Unknown names are
32
+ * config errors — the caller asked for something the bundle doesn't define.
33
+ *
34
+ * Declare-first (spec §4.4): a `trialOnly` variant — one whose candidates are
35
+ * ALL declared `trial` candidates — is valid only in bake-off replay
36
+ * (`mode: "trial"`). Selecting it on a LIVE path is a `RouteConfigError` with a
37
+ * teaching message that names the bake-off, never a silent fallback (residual
38
+ * flag F2′): the run loop must not dispatch an undeclared OR trial model live.
39
+ */
40
+ function resolveVariant(route, name, mode = "live") {
41
+ if (name === undefined || name === "default")
42
+ return null;
43
+ const variant = route.variants.find((v) => v.name === name);
44
+ if (variant === undefined) {
45
+ throw new loader_1.RouteConfigError(`route "${route.route}" has no variant "${name}" — defined: [${route.variants
46
+ .map((v) => v.name)
47
+ .join(", ")}]`);
48
+ }
49
+ if (mode === "live" && isTrialOnlyVariant(route, variant)) {
50
+ throw new loader_1.RouteConfigError(`route "${route.route}" variant "${name}" is a trial variant (all its candidates are declared ` +
51
+ `trial: true) — trial variants are declared but never served live. Measure it with a bake-off ` +
52
+ `(rig.bakeoff / "Run bake-off" in the console), then promote the winner to a live candidate.`);
53
+ }
54
+ return variant;
55
+ }
56
+ /**
57
+ * The variant-effective view of a route: narrowed candidates, overridden json
58
+ * rung, overridden template. Loader validation guarantees variant candidates ⊆
59
+ * declared candidates, so the candidate-set invariant holds.
60
+ *
61
+ * Declare-first (spec §4.4): a variant's `candidates` entries are authored as
62
+ * bare `{provider, model}` and do NOT carry the route-level `trial` flag. We
63
+ * MUST re-attach it from the matching declared candidate — otherwise a variant
64
+ * that narrows to a trial candidate (e.g. a mixed trial+live variant, which is
65
+ * not `trialOnly` and IS selectable live) would strip the flag and
66
+ * `resolveCandidates`' live trial-drop would let the trial model serve live.
67
+ */
68
+ function effectiveRoute(route, variant) {
69
+ if (variant === null)
70
+ return route;
71
+ const candidates = variant.candidates === undefined
72
+ ? route.candidates
73
+ : variant.candidates.map((vc) => {
74
+ const declared = route.candidates.find((c) => c.provider === vc.provider && c.model === vc.model);
75
+ return declared?.trial === true ? { ...vc, trial: true } : vc;
76
+ });
77
+ return Object.freeze({
78
+ ...route,
79
+ candidates,
80
+ systemTemplate: variant.promptOverride ?? route.systemTemplate,
81
+ policy: Object.freeze({
82
+ ...route.policy,
83
+ json: variant.json ?? route.policy.json,
84
+ }),
85
+ });
86
+ }
@@ -6,6 +6,13 @@
6
6
  * greps for it.
7
7
  */
8
8
  import type { CandidateSpec, RouteBundle } from "../types/route";
9
+ /** A challenger to declare/promote — just the identifying pair. Loose on purpose
10
+ * (`provider: string`): a challenger comes from a stored proposal or the picker,
11
+ * where the provider is already validated, and the emitters only render YAML. */
12
+ export interface ChallengerRef {
13
+ readonly provider: string;
14
+ readonly model: string;
15
+ }
9
16
  export interface SwapChange {
10
17
  readonly route: string;
11
18
  readonly file: string;
@@ -34,5 +41,31 @@ export declare function buildRestoreYaml(route: string, incumbent: readonly {
34
41
  readonly provider: string;
35
42
  readonly model: string;
36
43
  }[], currentVersion: number): string;
44
+ /** The paste-ready change that DECLARES a `trial` challenger (routing-console
45
+ * spec §4.4, declare step). Additive-only: nothing existing is removed. */
46
+ export interface DeclareTrialChange {
47
+ readonly route: string;
48
+ readonly file: string;
49
+ readonly challenger: ChallengerRef;
50
+ /** The auto-named trial variant a bake-off replays (`trial-<provider>-<model>`). */
51
+ readonly trialVariant: string;
52
+ readonly newVersion: number;
53
+ /** Paste-ready YAML: version bump + full candidates block (challenger appended
54
+ * with `trial: true`) + the trial variant to add. Complete, no elisions (A4). */
55
+ readonly yaml: string;
56
+ }
57
+ /** A stable, YAML-safe trial variant name for a challenger. */
58
+ export declare function trialVariantName(challenger: ChallengerRef): string;
59
+ /**
60
+ * Emit the declare-first YAML that adds a `trial` challenger to a route (spec
61
+ * §4.4 step 1): the challenger appended to `candidates` with `trial: true`, a
62
+ * `trial-<provider>-<model>` variant that narrows to it, and `version: n+1`. The
63
+ * challenger is DECLARED but never served live (the loader/resolver enforce
64
+ * that) — so a bake-off can measure it on real traffic before promotion. Pure,
65
+ * no filesystem (emit.test.ts greps for that). Throws when the challenger is
66
+ * already a declared candidate — you don't declare what's already declared (the
67
+ * server maps this to 422).
68
+ */
69
+ export declare function buildDeclareTrialChange(bundle: RouteBundle, challenger: ChallengerRef): DeclareTrialChange;
37
70
  export declare function buildSwapChange(bundle: RouteBundle, winningVariant: string): SwapChange;
38
71
  //# sourceMappingURL=emit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/savings/emit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAE/E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;yCACqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;+DAC2D;IAC3D,QAAQ,CAAC,mBAAmB,EAAE,SAAS,aAAa,EAAE,CAAC;CACxD;AAmCD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAC3E,cAAc,EAAE,MAAM,GACrB,MAAM,CAgBR;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,UAAU,CA6CvF"}
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/savings/emit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAE/E;;iFAEiF;AACjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;yCACqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;+DAC2D;IAC3D,QAAQ,CAAC,mBAAmB,EAAE,SAAS,aAAa,EAAE,CAAC;CACxD;AA2CD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAC3E,cAAc,EAAE,MAAM,GACrB,MAAM,CAgBR;AAED;2EAC2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;qFACiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,aAAa,GAAG,MAAM,CAElE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,GAAG,kBAAkB,CAiC1G;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,GAAG,UAAU,CAgEvF"}
@@ -8,9 +8,19 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.buildRestoreYaml = buildRestoreYaml;
11
+ exports.trialVariantName = trialVariantName;
12
+ exports.buildDeclareTrialChange = buildDeclareTrialChange;
11
13
  exports.buildSwapChange = buildSwapChange;
12
14
  function candidateLines(candidates) {
13
- return candidates.flatMap((c) => [` - provider: ${c.provider}`, ` model: ${c.model}`]);
15
+ // Preserve a candidate's `trial: true` (declare-first, spec §4.4) so an
16
+ // emitted candidates block never silently promotes a DECLARED-trial candidate
17
+ // to live. The promote path drops the flag from the WINNER explicitly (it
18
+ // passes a winner entry without `trial`), never here.
19
+ return candidates.flatMap((c) => [
20
+ ` - provider: ${c.provider}`,
21
+ ` model: ${c.model}`,
22
+ ...(c.trial === true ? [" trial: true"] : []),
23
+ ]);
14
24
  }
15
25
  function overrideLines(variant) {
16
26
  const lines = [];
@@ -57,26 +67,88 @@ function buildRestoreYaml(route, incumbent, currentVersion) {
57
67
  "",
58
68
  ].join("\n");
59
69
  }
70
+ /** A stable, YAML-safe trial variant name for a challenger. */
71
+ function trialVariantName(challenger) {
72
+ return `trial-${challenger.provider}-${challenger.model}`.replace(/[^A-Za-z0-9._-]/g, "-");
73
+ }
74
+ /**
75
+ * Emit the declare-first YAML that adds a `trial` challenger to a route (spec
76
+ * §4.4 step 1): the challenger appended to `candidates` with `trial: true`, a
77
+ * `trial-<provider>-<model>` variant that narrows to it, and `version: n+1`. The
78
+ * challenger is DECLARED but never served live (the loader/resolver enforce
79
+ * that) — so a bake-off can measure it on real traffic before promotion. Pure,
80
+ * no filesystem (emit.test.ts greps for that). Throws when the challenger is
81
+ * already a declared candidate — you don't declare what's already declared (the
82
+ * server maps this to 422).
83
+ */
84
+ function buildDeclareTrialChange(bundle, challenger) {
85
+ if (bundle.candidates.some((c) => c.provider === challenger.provider && c.model === challenger.model)) {
86
+ throw new Error(`${challenger.provider}/${challenger.model} is already a declared candidate on "${bundle.route}" — ` +
87
+ "nothing to declare (pick a model that is not yet in the route to trial it)");
88
+ }
89
+ const file = `modelrig/routes/${bundle.route}.yaml`;
90
+ const newVersion = bundle.version + 1;
91
+ const variant = trialVariantName(challenger);
92
+ const newCandidates = [...bundle.candidates, { ...challenger, trial: true }];
93
+ const yaml = [
94
+ `# declare a trial of ${challenger.provider}/${challenger.model} on ${bundle.route}`,
95
+ `# a trial candidate is DECLARED but NEVER served live — a bake-off measures it, then Promote makes it live.`,
96
+ `# apply to ${file}:`,
97
+ `# 1) bump the version`,
98
+ `version: ${newVersion}`,
99
+ `# 2) replace your candidates: block with this (the challenger is appended and marked for trial)`,
100
+ "candidates:",
101
+ ...candidateLines(newCandidates),
102
+ `# 3) add this entry to your variants: block (create the block if the route has none)`,
103
+ "variants:",
104
+ ` - name: ${variant}`,
105
+ " candidates:",
106
+ ` - provider: ${challenger.provider}`,
107
+ ` model: ${challenger.model}`,
108
+ `# Requires the modelrig SDK release that understands \`trial:\` — an older SDK ignores the flag and`,
109
+ `# would serve the model live. Bake it off with: modelrig bakeoff --from-proposal <id>`,
110
+ "",
111
+ ].join("\n");
112
+ return { route: bundle.route, file, challenger, trialVariant: variant, newVersion, yaml };
113
+ }
60
114
  function buildSwapChange(bundle, winningVariant) {
61
115
  const variant = bundle.variants.find((v) => v.name === winningVariant);
62
116
  if (variant === undefined) {
63
117
  throw new Error(`route "${bundle.route}" has no variant "${winningVariant}" — cannot emit a swap for it`);
64
118
  }
65
- const winners = variant.candidates ?? bundle.candidates;
119
+ // Promote drops `trial: true` from the WINNER and moves it to position 0. The
120
+ // flag is stripped EXPLICITLY here (not merely relied upon being absent from a
121
+ // bare variant candidate) so a hand-authored `trial: true` inside the variant's
122
+ // candidate block can never leave the promoted winner as a trial candidate — a
123
+ // silent no-op promotion where the "promoted" model is then dropped live. Other
124
+ // declared candidates keep their own trial flags (candidateLines preserves them).
125
+ const winners = (variant.candidates ?? bundle.candidates).map((c) => ({
126
+ provider: c.provider,
127
+ model: c.model,
128
+ }));
66
129
  if (winners.length === 0) {
67
130
  throw new Error(`variant "${winningVariant}" has no candidates to promote`);
68
131
  }
69
132
  const incumbent = bundle.candidates[0];
133
+ // Trial-winner promotes also remove the now-redundant trial variant (spec §4.4).
134
+ const isTrialPromotion = variant.trialOnly === true;
70
135
  const promoted = promotedOrder(bundle.candidates, winners);
71
136
  const file = `modelrig/routes/${bundle.route}.yaml`;
72
137
  const newVersion = bundle.version + 1;
138
+ const applyComment = isTrialPromotion
139
+ ? `# apply to ${file} — replace version + candidates, and REMOVE the trial variant "${winningVariant}" from variants:`
140
+ : `# apply to ${file} — replace the version and candidates fields; nothing else changes`;
141
+ const trialRemovalLine = isTrialPromotion
142
+ ? [`# remove this now-promoted trial variant from variants: (its candidate is live at position 0 above):`, `# - name: ${winningVariant}`]
143
+ : [];
73
144
  const yaml = [
74
145
  `# swap: serve ${bundle.route} on ${winners[0].provider}/${winners[0].model}`,
75
- `# apply to ${file} — replace the version and candidates fields; nothing else changes`,
146
+ applyComment,
76
147
  `version: ${newVersion}`,
77
148
  "candidates:",
78
149
  ...candidateLines(promoted),
79
150
  ...overrideLines(variant),
151
+ ...trialRemovalLine,
80
152
  "",
81
153
  ].join("\n");
82
154
  const revertYaml = [
@@ -3,7 +3,7 @@
3
3
  * Re-exported from the SDK entrypoint so `index.ts` stays under the LOC cap and
4
4
  * the settings surface has one home.
5
5
  */
6
- export { createTaskSettingsResolver, effectiveCapture, DEFAULT_TASK_SETTINGS, } from "./task-settings";
6
+ export { createTaskSettingsResolver, effectiveCapture, effectiveScrubMode, DEFAULT_TASK_SETTINGS, } from "./task-settings";
7
7
  export type { TaskSettings, TaskSettingsResolver, TaskSettingsFetcher } from "./task-settings";
8
8
  export { createApiTaskSettingsFetcher, startApiSettings } from "./api-fetcher";
9
9
  export type { ApiTaskSettingsFetcherConfig } from "./api-fetcher";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/settings/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/settings/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,EAAE,4BAA4B,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC/E,YAAY,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC"}
@@ -5,10 +5,11 @@
5
5
  * the settings surface has one home.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.startApiSettings = exports.createApiTaskSettingsFetcher = exports.DEFAULT_TASK_SETTINGS = exports.effectiveCapture = exports.createTaskSettingsResolver = void 0;
8
+ exports.startApiSettings = exports.createApiTaskSettingsFetcher = exports.DEFAULT_TASK_SETTINGS = exports.effectiveScrubMode = exports.effectiveCapture = exports.createTaskSettingsResolver = void 0;
9
9
  var task_settings_1 = require("./task-settings");
10
10
  Object.defineProperty(exports, "createTaskSettingsResolver", { enumerable: true, get: function () { return task_settings_1.createTaskSettingsResolver; } });
11
11
  Object.defineProperty(exports, "effectiveCapture", { enumerable: true, get: function () { return task_settings_1.effectiveCapture; } });
12
+ Object.defineProperty(exports, "effectiveScrubMode", { enumerable: true, get: function () { return task_settings_1.effectiveScrubMode; } });
12
13
  Object.defineProperty(exports, "DEFAULT_TASK_SETTINGS", { enumerable: true, get: function () { return task_settings_1.DEFAULT_TASK_SETTINGS; } });
13
14
  var api_fetcher_1 = require("./api-fetcher");
14
15
  Object.defineProperty(exports, "createApiTaskSettingsFetcher", { enumerable: true, get: function () { return api_fetcher_1.createApiTaskSettingsFetcher; } });
@@ -88,4 +88,14 @@ export declare function createTaskSettingsResolver(config: TaskSettingsResolverC
88
88
  * because there is no control-plane row to resolve.
89
89
  */
90
90
  export declare function effectiveCapture(resolver: TaskSettingsResolver | null, task: string | undefined, yamlCapture: boolean): boolean;
91
+ /**
92
+ * The EFFECTIVE scrub mode for an artifact, given the task's configured mode and
93
+ * the artifact's `data_classification` (screening SC-0 S8-c). A `pii`/`phi`
94
+ * artifact resolves a FLOOR of `redact` — content carrying PII/PHI is never
95
+ * uploaded verbatim just because the task's mode was `off`/`detect`. A stronger
96
+ * configured mode wins: `block` stays `block` (block ≥ redact — it refuses the
97
+ * upload entirely). A non-sensitive (or absent) classification leaves the
98
+ * configured mode untouched. Pure; the single place the floor is spelled.
99
+ */
100
+ export declare function effectiveScrubMode(configured: ScrubMode, classification: string | null | undefined): ScrubMode;
91
101
  //# sourceMappingURL=task-settings.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-settings.d.ts","sourceRoot":"","sources":["../../src/settings/task-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;0EAC0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AAED;;;uCAGuC;AACvC,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;2EAG2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,oBAAoB;IACnC;sFACkF;IAClF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IAChC;oEACgE;IAChE,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC9C;;gEAE4D;IAC5D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;2EACuE;IACvE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,4CAA4C;IAC5C,KAAK,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC;gFAC4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,KAAK,IAAI,CAAC;CAC3E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,0BAA0B,GACjC,oBAAoB,CA4DtB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,oBAAoB,GAAG,IAAI,EACrC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,EAAE,OAAO,GACnB,OAAO,CAGT"}
1
+ {"version":3,"file":"task-settings.d.ts","sourceRoot":"","sources":["../../src/settings/task-settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C;0EAC0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AAED;;;uCAGuC;AACvC,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;2EAG2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,oBAAoB;IACnC;sFACkF;IAClF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IAChC;oEACgE;IAChE,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC9C;;gEAE4D;IAC5D,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;2EACuE;IACvE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,4CAA4C;IAC5C,KAAK,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC;gFAC4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,KAAK,IAAI,CAAC;CAC3E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,0BAA0B,GACjC,oBAAoB,CA4DtB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,oBAAoB,GAAG,IAAI,EACrC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,EAAE,OAAO,GACnB,OAAO,CAGT;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,SAAS,EACrB,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACxC,SAAS,CAIX"}
@@ -27,6 +27,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
27
27
  exports.DEFAULT_TASK_SETTINGS = void 0;
28
28
  exports.createTaskSettingsResolver = createTaskSettingsResolver;
29
29
  exports.effectiveCapture = effectiveCapture;
30
+ exports.effectiveScrubMode = effectiveScrubMode;
31
+ const classification_1 = require("../artifacts/classification");
30
32
  /** The safe floor for a task the control plane has not answered for yet: capture
31
33
  * OFF, artifacts inherit the default, content OFF, scrub the schema default. A
32
34
  * run that reaches the cache before the first fetch lands sees this — never a
@@ -114,3 +116,18 @@ function effectiveCapture(resolver, task, yamlCapture) {
114
116
  return yamlCapture;
115
117
  return resolver.get(task).capture;
116
118
  }
119
+ /**
120
+ * The EFFECTIVE scrub mode for an artifact, given the task's configured mode and
121
+ * the artifact's `data_classification` (screening SC-0 S8-c). A `pii`/`phi`
122
+ * artifact resolves a FLOOR of `redact` — content carrying PII/PHI is never
123
+ * uploaded verbatim just because the task's mode was `off`/`detect`. A stronger
124
+ * configured mode wins: `block` stays `block` (block ≥ redact — it refuses the
125
+ * upload entirely). A non-sensitive (or absent) classification leaves the
126
+ * configured mode untouched. Pure; the single place the floor is spelled.
127
+ */
128
+ function effectiveScrubMode(configured, classification) {
129
+ if (classification == null || !classification_1.SENSITIVE_CLASSIFICATIONS.has(classification))
130
+ return configured;
131
+ // Sensitive tier: never below `redact`. `block` is stronger and stays.
132
+ return configured === "block" ? "block" : "redact";
133
+ }