@rulvar/cli 1.16.1 → 1.17.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.
@@ -1,626 +0,0 @@
1
- import { KnowledgeCasError, claimExpiry, compileVerifiedLayer, defineWorkflow } from "@rulvar/core";
2
- import { createHash } from "node:crypto";
3
- //#region ../evals/dist/index.js
4
- /**
5
- * @rulvar/evals (M9-T02): EvalCase, the grader contract, and the case and
6
- * suite runners. A separate quality-measurement package built strictly on
7
- * the public APIs (L6).
8
- *
9
- * Determinism rules (https://docs.rulvar.com/guide/evals and
10
- * https://docs.rulvar.com/guide/testing): judge graders run THROUGH the
11
- * engine, so judge calls
12
- * are journaled, budgeted, and VCR-recordable, and an eval suite replays
13
- * deterministically from cassettes with zero live calls.
14
- */
15
- /** Thrown when a judge run does not settle ok. */
16
- var EvalJudgeError = class extends Error {
17
- judgeRun;
18
- status;
19
- constructor(judgeRun, status, detail) {
20
- super(`eval judge run '${judgeRun}' settled '${status}'${detail === void 0 ? "" : `: ${detail}`}`);
21
- this.name = "EvalJudgeError";
22
- this.judgeRun = judgeRun;
23
- this.status = status;
24
- }
25
- };
26
- /**
27
- * Runs one EvalCase on the given engine: the target workflow as its own
28
- * run, pure graders host-side over the outcome, judge graders through the
29
- * engine via GraderContext.judge. Grader thrown errors are not absorbed:
30
- * a grader that cannot grade is a defect of the suite, not a failed case.
31
- */
32
- async function runEvalCase(engine, evalCase, options = {}) {
33
- const name = options.name ?? evalCase.workflow.name;
34
- const timing = {};
35
- const handle = engine.run(evalCase.workflow, evalCase.args, {
36
- name: `eval:${name}`,
37
- ...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd }
38
- });
39
- const offStart = handle.on("run:start", (event) => {
40
- timing.start ??= event.ts;
41
- });
42
- const offEnd = handle.on("run:end", (event) => {
43
- timing.end ??= event.ts;
44
- });
45
- const outcome = await handle.result;
46
- offStart();
47
- offEnd();
48
- let judgeCostUsd = 0;
49
- let judgeOrdinal = 0;
50
- const context = {
51
- value: outcome.value,
52
- outcome,
53
- async judge(spec) {
54
- const ordinal = judgeOrdinal;
55
- judgeOrdinal += 1;
56
- const judged = await runJudge(engine, `${name}:${ordinal}`, spec, options.judgeBudgetUsd);
57
- judgeCostUsd += judged.costUsd;
58
- return judged.output;
59
- }
60
- };
61
- const verdicts = [];
62
- for (const grader of evalCase.graders) verdicts.push(await grader.grade(context));
63
- const latencyMs = timing.start !== void 0 && timing.end !== void 0 ? Math.max(0, Date.parse(timing.end) - Date.parse(timing.start)) : 0;
64
- return {
65
- name,
66
- status: outcome.status,
67
- passed: outcome.status === "ok" && verdicts.every((verdict) => verdict.passed),
68
- verdicts,
69
- costUsd: outcome.cost.totalUsd + judgeCostUsd,
70
- judgeCostUsd,
71
- latencyMs,
72
- usage: outcome.usage,
73
- ...outcome.error === void 0 ? {} : { error: outcome.error }
74
- };
75
- }
76
- async function runJudge(engine, judgeName, spec, budgetUsd) {
77
- const workflowName = `eval-judge:${judgeName}`;
78
- const judgeWorkflow = defineWorkflow({ name: workflowName }, async (ctx) => {
79
- return await ctx.agent(spec.prompt, {
80
- model: spec.model,
81
- schema: spec.schema,
82
- label: "eval-judge",
83
- onError: "throw"
84
- });
85
- });
86
- const outcome = await engine.run(judgeWorkflow, null, {
87
- name: workflowName,
88
- ...budgetUsd === void 0 ? {} : { budgetUsd }
89
- }).result;
90
- if (outcome.status !== "ok") throw new EvalJudgeError(workflowName, outcome.status, outcome.error?.message);
91
- return {
92
- output: outcome.value ?? null,
93
- costUsd: outcome.cost.totalUsd
94
- };
95
- }
96
- /**
97
- * Runs cases sequentially (deterministic journal and cassette order) and
98
- * aggregates. Duplicate workflow names get '#<ordinal>' suffixes so every
99
- * result row and judge journal is unambiguous.
100
- */
101
- async function runEvalSuite(engine, cases, options = {}) {
102
- const seen = /* @__PURE__ */ new Map();
103
- const results = [];
104
- for (const evalCase of cases) {
105
- const base = evalCase.workflow.name;
106
- const ordinal = seen.get(base) ?? 0;
107
- seen.set(base, ordinal + 1);
108
- const name = ordinal === 0 ? base : `${base}#${ordinal}`;
109
- results.push(await runEvalCase(engine, evalCase, {
110
- name,
111
- ...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd },
112
- ...options.judgeBudgetUsd === void 0 ? {} : { judgeBudgetUsd: options.judgeBudgetUsd }
113
- }));
114
- }
115
- return {
116
- results,
117
- passRate: results.length === 0 ? 0 : results.filter((r) => r.passed).length / results.length,
118
- totalCostUsd: results.reduce((sum, r) => sum + r.costUsd, 0),
119
- meanLatencyMs: results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.latencyMs, 0) / results.length
120
- };
121
- }
122
- /**
123
- * Runs the same case list against every cell's engine, sequentially and
124
- * in declaration order (deterministic cassette consumption), and reports
125
- * per-cell aggregates for side-by-side comparison.
126
- */
127
- async function runEvalMatrix(cells, cases, options = {}) {
128
- const reports = [];
129
- for (const cell of cells) {
130
- const suite = await runEvalSuite(await cell.engine(), cases, options);
131
- reports.push({
132
- cell: cell.name,
133
- passRate: suite.passRate,
134
- totalCostUsd: suite.totalCostUsd,
135
- meanLatencyMs: suite.meanLatencyMs,
136
- results: suite.results
137
- });
138
- }
139
- return { cells: reports };
140
- }
141
- function deepEqual(a, b) {
142
- if (a === b) return true;
143
- if (a === null || b === null || a === void 0 || b === void 0) return false;
144
- if (Array.isArray(a) || Array.isArray(b)) {
145
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
146
- return a.every((item, index) => deepEqual(item, b[index]));
147
- }
148
- if (typeof a === "object" && typeof b === "object") {
149
- const aKeys = Object.keys(a).sort();
150
- const bKeys = Object.keys(b).sort();
151
- if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
152
- return aKeys.every((key) => deepEqual(a[key], b[key]));
153
- }
154
- return false;
155
- }
156
- function goldenGrader(expected, options = {}) {
157
- const name = options.name ?? "golden";
158
- return {
159
- name,
160
- grade(context) {
161
- const passed = deepEqual(context.value, expected);
162
- return {
163
- grader: name,
164
- passed,
165
- ...passed ? {} : { details: {
166
- expected,
167
- actual: context.value ?? null
168
- } }
169
- };
170
- }
171
- };
172
- }
173
- function rubricGrader(criteria, options = {}) {
174
- const name = options.name ?? "rubric";
175
- const threshold = options.passThreshold ?? 1;
176
- return {
177
- name,
178
- grade(context) {
179
- const rows = criteria.map((criterion) => ({
180
- name: criterion.name,
181
- passed: criterion.check(context.value)
182
- }));
183
- const score = criteria.length === 0 ? 1 : rows.filter((row) => row.passed).length / rows.length;
184
- return {
185
- grader: name,
186
- passed: score >= threshold,
187
- score,
188
- details: { criteria: rows }
189
- };
190
- }
191
- };
192
- }
193
- /** The default judge verdict shape. */
194
- const JUDGE_VERDICT_SCHEMA = {
195
- type: "object",
196
- properties: {
197
- passed: { type: "boolean" },
198
- reasoning: { type: "string" }
199
- },
200
- required: ["passed"],
201
- additionalProperties: false
202
- };
203
- function defaultToVerdict(output) {
204
- return { passed: typeof output === "object" && output !== null && !Array.isArray(output) && output.passed === true };
205
- }
206
- function judgePrompt(instruction, value) {
207
- return [
208
- "You are an evaluation judge. Judge the candidate output below against the instruction.",
209
- "",
210
- `Instruction: ${instruction}`,
211
- "",
212
- "Candidate output (JSON):",
213
- JSON.stringify(value ?? null),
214
- "",
215
- "Return a verdict object matching the response schema."
216
- ].join("\n");
217
- }
218
- function judgeGrader(options) {
219
- const name = options.name ?? "judge";
220
- if (options.schema !== void 0 && options.toVerdict === void 0) throw new Error(`judgeGrader '${name}': a custom schema requires toVerdict`);
221
- const toVerdict = options.toVerdict ?? defaultToVerdict;
222
- const schema = options.schema ?? JUDGE_VERDICT_SCHEMA;
223
- return {
224
- name,
225
- async grade(context) {
226
- if (context.outcome.status !== "ok") return {
227
- grader: name,
228
- passed: false,
229
- details: { skipped: `target run settled '${context.outcome.status}'` }
230
- };
231
- const output = await context.judge({
232
- model: options.model,
233
- prompt: judgePrompt(options.instruction, context.value),
234
- schema
235
- });
236
- const verdict = toVerdict(output);
237
- return {
238
- grader: name,
239
- passed: verdict.passed,
240
- ...verdict.score === void 0 ? {} : { score: verdict.score },
241
- details: { output }
242
- };
243
- }
244
- };
245
- }
246
- /**
247
- * The eval-committer identity (M11-T01; https://docs.rulvar.com/guide/model-knowledge).
248
- * The pipeline-side commit path: builds
249
- * eval-committer-gated ops (the coherence square: class eval-measured,
250
- * author eval-pipeline, metrics present) and commits them with the
251
- * documented CAS-rebase recipe. Humans never call this; their path is
252
- * the human gate and it structurally cannot carry metrics.
253
- */
254
- /** One measured claim; claimExpiry applies the TTL from the decay table. */
255
- function evalMeasuredClaim(input, committerId) {
256
- return {
257
- id: input.id,
258
- subject: input.subject,
259
- taskClass: input.taskClass,
260
- polarity: input.polarity,
261
- statement: input.statement,
262
- class: "eval-measured",
263
- status: "active",
264
- evidence: input.evidence,
265
- metrics: input.metrics,
266
- confidence: input.confidence,
267
- observedAt: input.observedAt,
268
- expiresAt: claimExpiry("eval-measured", input.polarity, input.observedAt),
269
- ...input.modelEpoch === void 0 ? {} : { modelEpoch: input.modelEpoch },
270
- author: {
271
- kind: "eval-pipeline",
272
- id: committerId
273
- }
274
- };
275
- }
276
- /**
277
- * Commits measured claims through the eval-committer gate with the
278
- * documented rebase recipe: on a CAS rejection, re-read current() and
279
- * retry against the fresh version. Returns the committed version.
280
- */
281
- async function commitEvalMeasured(store, claims, options) {
282
- const gate = {
283
- kind: "eval-committer",
284
- committerId: options.committerId,
285
- reportId: options.reportId
286
- };
287
- const ops = claims.map((input) => ({
288
- op: "add",
289
- claim: evalMeasuredClaim(input, options.committerId),
290
- gate
291
- }));
292
- const attempts = options.attempts ?? 3;
293
- let lastCas;
294
- for (let attempt = 0; attempt < attempts; attempt += 1) {
295
- const snapshot = await store.current();
296
- try {
297
- return await store.commit(ops, snapshot.version);
298
- } catch (thrown) {
299
- if (thrown instanceof KnowledgeCasError) {
300
- lastCas = thrown;
301
- continue;
302
- }
303
- throw thrown;
304
- }
305
- }
306
- throw lastCas ?? /* @__PURE__ */ new Error("commitEvalMeasured: unreachable");
307
- }
308
- /**
309
- * The canary fingerprint (M11-T04; OQ-06). The optional compensation for silent alias
310
- * re-pointing that modelEpoch honestly cannot catch: a FIXED probe set,
311
- * run through the ordinary engine (journaled, budgeted,
312
- * VCR-recordable), hashed over normalized outputs. Sampling parameters
313
- * are not pinned; drift detection rests on the fixed prompts, the
314
- * normalization, and exact fingerprint comparison. A fingerprint change
315
- * flips the model's eval claims to stale in one command.
316
- *
317
- * The committed v1 design (closing OQ-06): the probe set is CALLER
318
- * data (fixed, versioned alongside the store); normalization is NFC,
319
- * trim, and whitespace collapse per output; the fingerprint is the
320
- * sha256 of the JCS-serialized normalized output array, prefixed with
321
- * the probe count so a probe-set edit never collides with drift.
322
- */
323
- /** The committed v1 normalization (OQ-06): NFC, trim, collapse whitespace. */
324
- function normalizeCanaryOutput(output) {
325
- return (typeof output === "string" ? output : JSON.stringify(output ?? null)).normalize("NFC").trim().replace(/\s+/gu, " ");
326
- }
327
- /**
328
- * Runs the fixed probe set through the ordinary engine and returns the
329
- * fingerprint. Probes run sequentially in declaration order, one run
330
- * per probe, so recordings replay deterministically.
331
- */
332
- async function canaryFingerprint(engine, probes) {
333
- const outputs = [];
334
- for (const [index, prompt] of probes.prompts.entries()) {
335
- const workflow = defineWorkflow({ name: `kb-canary:${String(index)}` }, async (ctx) => await ctx.agent(prompt, { agentType: probes.agentType }));
336
- const outcome = await engine.run(workflow, null).result;
337
- outputs.push(outcome.status === "ok" ? normalizeCanaryOutput(outcome.value) : `!${outcome.status}`);
338
- }
339
- const body = JSON.stringify([probes.prompts.length, outputs]);
340
- return createHash("sha256").update(body, "utf8").digest("hex");
341
- }
342
- /**
343
- * Flips the model's ACTIVE eval-measured claims to stale when their
344
- * recorded canary fingerprint differs from the fresh one. Claims
345
- * without a recorded fingerprint have no baseline and
346
- * stay untouched (the documented no-probe posture); a second run is
347
- * an idempotent noop. CAS-rebased like every maintenance commit.
348
- */
349
- async function flipStaleOnCanaryDrift(store, model, freshFingerprint, options) {
350
- const attempts = options?.attempts ?? 3;
351
- let lastCas;
352
- for (let attempt = 0; attempt < attempts; attempt += 1) {
353
- const snapshot = await store.current();
354
- const drifted = snapshot.claims.filter((claim) => claim.status === "active" && claim.class === "eval-measured" && claim.subject.model === model && claim.modelEpoch?.canaryFingerprint !== void 0 && claim.modelEpoch.canaryFingerprint !== freshFingerprint);
355
- if (drifted.length === 0) return {
356
- model,
357
- freshFingerprint,
358
- flipped: []
359
- };
360
- const ops = drifted.map((claim) => ({
361
- op: "mark_stale",
362
- claimId: claim.id,
363
- reason: "canary-drift"
364
- }));
365
- try {
366
- const version = await store.commit(ops, snapshot.version);
367
- return {
368
- model,
369
- freshFingerprint,
370
- flipped: drifted.map((claim) => claim.id),
371
- version
372
- };
373
- } catch (thrown) {
374
- if (thrown instanceof KnowledgeCasError) {
375
- lastCas = thrown;
376
- continue;
377
- }
378
- throw thrown;
379
- }
380
- }
381
- throw lastCas ?? /* @__PURE__ */ new Error("flipStaleOnCanaryDrift: unreachable");
382
- }
383
- /**
384
- * The phases 1-2 measured-value checkpoint (M12-T01; the quantitative
385
- * criteria of OQ-09, closed at M11-T06). The M12 gate: kb_propose
386
- * and the proposal loop ship ONLY if the knowledge card demonstrably
387
- * improves tier and agentType selection on eval cases.
388
- *
389
- * Two experiments, both A/B under identical fixed pools:
390
- *
391
- * 1. RUNG SELECTION, per (ladder, taskClass) cell: the baseline arm
392
- * runs every eval case at the ladder's DEFAULT start tier; the
393
- * treatment arm runs at the tier recommended by
394
- * compileVerifiedLayer over the store's claims (default when no
395
- * recommendation). A cell passes when the treatment reaches a pass
396
- * rate at least equal to the baseline at no more than 90 percent
397
- * of its cost, OR at least 5 points above it at no more than its
398
- * cost. Criterion 1 holds when a MAJORITY of cells pass AND the
399
- * pooled aggregate passes the same rule.
400
- *
401
- * 2. AGENTTYPE SELECTION, pooled: the same orchestrate-role cases run
402
- * with and without the knowledge store configured (the card docks
403
- * into the spawn tool description when configured). Criterion 2
404
- * holds when the card-informed arm matches or beats the baseline
405
- * pass rate at no more than 105 percent of its cost, OR beats it
406
- * by at least 15 points at no more than 115 percent of its cost
407
- * (the quality branch; OQ-09 as amended 2026-07-12: the baseline
408
- * fails CHEAPLY, so the flat cost bar tightened exactly when the
409
- * card was winning on quality).
410
- *
411
- * The checkpoint PASSES only when both criteria hold. Methodology
412
- * guard: the claims the treatment consumes MUST come from a seeding
413
- * sweep over a DISJOINT case set (the seed/eval split is the caller's
414
- * pool contract), or the measurement is leakage.
415
- */
416
- /** IEEE754 guard for the rule boundaries (0.8 + 0.05 exceeds 0.85). */
417
- const EPSILON = 1e-9;
418
- /**
419
- * The OQ-09 criterion 2 rule (as amended 2026-07-12): match-or-beat at
420
- * 105 percent of baseline cost, OR at least 15 points better at 115
421
- * percent (the quality branch: the baseline fails cheaply, so the flat
422
- * bar tightened exactly when the card won on quality). The vacuous-pass
423
- * guard stays with the caller.
424
- */
425
- function agentTypeRuleHolds(baseline, informed) {
426
- const matchesCheaply = informed.passRate >= baseline.passRate - EPSILON && informed.totalCostUsd <= 1.05 * baseline.totalCostUsd + EPSILON;
427
- const clearlyBetterNearCost = informed.passRate >= baseline.passRate + .15 - EPSILON && informed.totalCostUsd <= 1.15 * baseline.totalCostUsd + EPSILON;
428
- return matchesCheaply || clearlyBetterNearCost;
429
- }
430
- /** The OQ-09 cell rule (shared by the per-cell and pooled verdicts). */
431
- function rungRuleHolds(baseline, treatment) {
432
- const equalOrBetterCheaper = treatment.passRate >= baseline.passRate - EPSILON && treatment.totalCostUsd <= .9 * baseline.totalCostUsd + EPSILON;
433
- const clearlyBetterAtCost = treatment.passRate >= baseline.passRate + .05 - EPSILON && treatment.totalCostUsd <= baseline.totalCostUsd + EPSILON;
434
- return equalOrBetterCheaper || clearlyBetterAtCost;
435
- }
436
- function armOf(suite) {
437
- return {
438
- passRate: suite.passRate,
439
- totalCostUsd: suite.totalCostUsd,
440
- n: suite.results.length
441
- };
442
- }
443
- function pool(arms) {
444
- const n = arms.reduce((sum, arm) => sum + arm.n, 0);
445
- const passed = arms.reduce((sum, arm) => sum + arm.passRate * arm.n, 0);
446
- const cost = arms.reduce((sum, arm) => sum + arm.totalCostUsd, 0);
447
- return {
448
- passRate: n === 0 ? 0 : passed / n,
449
- totalCostUsd: cost,
450
- n
451
- };
452
- }
453
- /**
454
- * Runs the checkpoint over the fixed pool. Sequential in declaration
455
- * order (deterministic cassette consumption when recorded); every cell
456
- * runs baseline then treatment.
457
- */
458
- async function runValueCheckpoint(checkpointPool, options) {
459
- const recommendations = compileVerifiedLayer(options.snapshot.claims.filter((claim) => claim.status === "active"), checkpointPool.ladders);
460
- const byTaskClass = /* @__PURE__ */ new Map();
461
- for (const entry of checkpointPool.evalCases) {
462
- const bucket = byTaskClass.get(entry.taskClass) ?? [];
463
- bucket.push(entry.case);
464
- byTaskClass.set(entry.taskClass, bucket);
465
- }
466
- const cells = [];
467
- for (const ladder of checkpointPool.ladders) for (const [taskClass, cases] of byTaskClass) {
468
- const recommendation = recommendations.find((row) => row.ladder === ladder.name && row.taskClass === taskClass);
469
- const treatmentTier = recommendation?.recommendedTier ?? ladder.startTier;
470
- const baseMember = ladder.rungs[ladder.startTier];
471
- const treatMember = ladder.rungs[treatmentTier];
472
- if (baseMember === void 0 || treatMember === void 0) throw new Error(`checkpoint: ladder '${ladder.name}' lacks rung ${String(treatmentTier)}`);
473
- const baseline = armOf(await runEvalSuite(await options.engineFor(baseMember), cases, options.suite ?? {}));
474
- const treatment = treatmentTier === ladder.startTier ? baseline : armOf(await runEvalSuite(await options.engineFor(treatMember), cases, options.suite ?? {}));
475
- cells.push({
476
- ladder: ladder.name,
477
- taskClass,
478
- defaultTier: ladder.startTier,
479
- treatmentTier,
480
- recommended: recommendation !== void 0,
481
- baseline,
482
- treatment,
483
- passed: rungRuleHolds(baseline, treatment)
484
- });
485
- }
486
- const recommendedCells = cells.filter((cell) => cell.recommended);
487
- const cellsPassed = recommendedCells.filter((cell) => cell.passed).length;
488
- const majorityHolds = recommendedCells.length > 0 && cellsPassed * 2 > recommendedCells.length;
489
- const pooledBaseline = pool(cells.map((cell) => cell.baseline));
490
- const pooledTreatment = pool(cells.map((cell) => cell.treatment));
491
- const pooledHolds = rungRuleHolds(pooledBaseline, pooledTreatment);
492
- const criterion1 = {
493
- cells,
494
- cellsPassed,
495
- majorityHolds,
496
- pooledBaseline,
497
- pooledTreatment,
498
- pooledHolds,
499
- passed: majorityHolds && pooledHolds
500
- };
501
- let criterion2;
502
- if (options.orchestrateEngineFor !== void 0 && options.orchestratedCases !== void 0) {
503
- const cases = options.orchestratedCases.map((entry) => entry.case);
504
- const orchestratedSuite = options.orchestratedSuite ?? options.suite ?? {};
505
- const baseline = armOf(await runEvalSuite(await options.orchestrateEngineFor(false), cases, orchestratedSuite));
506
- const informed = armOf(await runEvalSuite(await options.orchestrateEngineFor(true), cases, orchestratedSuite));
507
- criterion2 = {
508
- baseline,
509
- informed,
510
- passed: informed.n > 0 && informed.passRate > 0 && agentTypeRuleHolds(baseline, informed)
511
- };
512
- }
513
- return {
514
- observedAt: options.observedAt,
515
- criterion1,
516
- ...criterion2 === void 0 ? {} : { criterion2 },
517
- passed: criterion1.passed && criterion2 !== void 0 && criterion2.passed
518
- };
519
- }
520
- const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
521
- const usd = (value) => `$${value.toFixed(4)}`;
522
- /** The deterministic render for the M12 gate docs amendment. */
523
- function renderCheckpointReport(report) {
524
- const lines = [
525
- `Measured-value checkpoint (OQ-09) at ${report.observedAt}: ` + (report.passed ? "PASSED" : "FAILED"),
526
- "",
527
- `Criterion 1 (rung selection): ${report.criterion1.passed ? "holds" : "fails"} (${String(report.criterion1.cellsPassed)}/${String(report.criterion1.cells.length)} cells, pooled ${report.criterion1.pooledHolds ? "holds" : "fails"})`
528
- ];
529
- for (const cell of report.criterion1.cells) lines.push(`* ${cell.ladder} :: ${cell.taskClass}: baseline tier ${String(cell.defaultTier)} ${percent(cell.baseline.passRate)} at ${usd(cell.baseline.totalCostUsd)}; treatment tier ${String(cell.treatmentTier)}${cell.recommended ? "" : " (no recommendation)"} ${percent(cell.treatment.passRate)} at ${usd(cell.treatment.totalCostUsd)}; ${cell.passed ? "pass" : "fail"} (n=${String(cell.baseline.n)})`);
530
- if (report.criterion2 !== void 0) {
531
- const c2 = report.criterion2;
532
- lines.push("", `Criterion 2 (agentType selection): ${c2.passed ? "holds" : "fails"} (baseline ${percent(c2.baseline.passRate)} at ${usd(c2.baseline.totalCostUsd)}; card-informed ${percent(c2.informed.passRate)} at ${usd(c2.informed.totalCostUsd)}; n=${String(c2.baseline.n)})`);
533
- } else lines.push("", "Criterion 2 (agentType selection): NOT MEASURED (counts as failed)");
534
- return lines.join("\n");
535
- }
536
- const SWEEP_THRESHOLD_DEFAULTS = {
537
- strength: .9,
538
- weakness: .5
539
- };
540
- /** Deterministic claim id: report-scoped, readable, collision-free. */
541
- function claimIdOf(reportId, member, taskClass) {
542
- const effort = member.effort === void 0 ? "" : `@${member.effort}`;
543
- return `${reportId}/${member.model}${effort}/${taskClass}`;
544
- }
545
- /** The typed statement template: never a quote from tool output. */
546
- function statementOf(cell, polarity) {
547
- const rate = cell.passRate.toFixed(2);
548
- const band = polarity === "strength" ? "at or above the strength band" : "in the weakness band";
549
- return `sweep passRate ${rate} over ${String(cell.n)} ${cell.taskClass} case${cell.n === 1 ? "" : "s"}: ${band}`;
550
- }
551
- /**
552
- * Runs the fixed matrix sequentially in declaration order
553
- * (deterministic cassette consumption), aggregates per (model,
554
- * taskClass) cell, emits threshold-crossing claims, and commits them
555
- * through the eval-committer identity when a store is given.
556
- */
557
- async function runSweepMatrix(pool, options) {
558
- const thresholds = {
559
- ...SWEEP_THRESHOLD_DEFAULTS,
560
- ...options.thresholds
561
- };
562
- const byTaskClass = /* @__PURE__ */ new Map();
563
- for (const entry of pool.cases) {
564
- const bucket = byTaskClass.get(entry.taskClass) ?? [];
565
- bucket.push(entry);
566
- byTaskClass.set(entry.taskClass, bucket);
567
- }
568
- const cells = [];
569
- const claims = [];
570
- for (const member of pool.models) {
571
- const engine = await options.engineFor(member);
572
- for (const [taskClass, bucket] of byTaskClass) {
573
- const suite = await runEvalSuite(engine, bucket.map((entry) => entry.case), options.suite ?? {});
574
- const cell = {
575
- model: member.model,
576
- ...member.effort === void 0 ? {} : { effort: member.effort },
577
- taskClass,
578
- passRate: suite.passRate,
579
- n: suite.results.length,
580
- totalCostUsd: suite.totalCostUsd,
581
- caseNames: suite.results.map((result) => result.name)
582
- };
583
- cells.push(cell);
584
- const polarity = cell.passRate >= thresholds.strength ? "strength" : cell.passRate <= thresholds.weakness ? "weakness" : void 0;
585
- if (polarity !== void 0 && cell.n > 0) {
586
- const epoch = options.modelEpochFor?.(member);
587
- claims.push({
588
- id: claimIdOf(options.reportId, member, taskClass),
589
- subject: {
590
- model: member.model,
591
- ...member.effort === void 0 ? {} : { effort: member.effort }
592
- },
593
- taskClass,
594
- polarity,
595
- statement: statementOf(cell, polarity),
596
- metrics: {
597
- passRate: cell.passRate,
598
- n: cell.n,
599
- graderId: "eval-suite"
600
- },
601
- confidence: cell.n >= 20 ? "high" : cell.n >= 5 ? "medium" : "low",
602
- observedAt: options.observedAt,
603
- evidence: [{
604
- kind: "eval",
605
- reportId: options.reportId,
606
- caseIds: cell.caseNames
607
- }],
608
- ...epoch === void 0 ? {} : { modelEpoch: epoch }
609
- });
610
- }
611
- }
612
- }
613
- const report = {
614
- reportId: options.reportId,
615
- observedAt: options.observedAt,
616
- cells,
617
- claims
618
- };
619
- if (options.store !== void 0 && claims.length > 0) report.committedVersion = await commitEvalMeasured(options.store, claims, {
620
- committerId: options.committerId,
621
- reportId: options.reportId
622
- });
623
- return report;
624
- }
625
- //#endregion
626
- export { EvalJudgeError, JUDGE_VERDICT_SCHEMA, SWEEP_THRESHOLD_DEFAULTS, canaryFingerprint, commitEvalMeasured, evalMeasuredClaim, flipStaleOnCanaryDrift, goldenGrader, judgeGrader, normalizeCanaryOutput, renderCheckpointReport, rubricGrader, runEvalCase, runEvalMatrix, runEvalSuite, runSweepMatrix, runValueCheckpoint, rungRuleHolds };