@velum-labs/routekit-eval-setup 1.0.24 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export type { OriEvalAuthoringApi, OriEvalResult } from "./ori-result.js";
7
7
  export { EvalProjectArtifacts, EvalProjectArtifactsLive, evaluationProposalDigest, makeFileEvalProjectArtifacts, routingBasisDigest } from "./project-artifacts.js";
8
8
  export type { EvalAuthoringCompletion, EvalAuthoringSource, EvalAuthoringTransportShape, EvalProjectAuthorShape } from "./project-authoring.js";
9
9
  export { EVAL_AUTHORING_CASES_PER_DIMENSION, EVAL_AUTHORING_EVALUATION_OUTPUT_TOKENS, EVAL_AUTHORING_REQUEST_BYTES, EVAL_AUTHORING_SOURCE_BYTES, EVAL_AUTHORING_SOURCE_FILES, EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, makeEvalProjectAuthor, readProjectAuthoringSources, selectProjectAuthoringSourceFiles } from "./project-authoring.js";
10
- export type { EvalClassifierObservation, EvalCompositionCase, EvalCompositionCaseResult, EvalCompositionSuite, EvalDecompositionBenchmark, EvalDecompositionBenchmarkCase, EvalDimensionCase, EvalDimensionContrast, EvalDimensionSuite, EvalEvaluationProposal, EvalExecutionPlan, EvalPlanScope, EvalProposedDimension, EvalProjectArtifactsStatus, EvalProjectStatus, EvalRoutingBasisProposal, EvalRunCleanup, EvalRunFailureError, EvalRunLedger, EvalRunQualification, EvalRunReport, EvalRunTarget } from "./project-contracts.js";
11
- export { EVAL_PROJECT_VERSION, EvalArtifactApproval, EvalCompositionSuite as EvalCompositionSuiteSchema, EvalDecompositionBenchmark as EvalDecompositionBenchmarkSchema, EvalDimensionCase as EvalDimensionCaseSchema, EvalDimensionContrast as EvalDimensionContrastSchema, EvalDimensionSuite as EvalDimensionSuiteSchema, EvalEvaluationProposal as EvalEvaluationProposalSchema, EvalExecutionPlan as EvalExecutionPlanSchema, EvalPlanScope as EvalPlanScopeSchema, EvalProposedDimension as EvalProposedDimensionSchema, EvalProjectConfiguration, EvalProjectQuestion, EvalProjectSetupProgress, EvalProjectState, EvalRoutingBasisProposal as EvalRoutingBasisProposalSchema, EvalRunCleanup as EvalRunCleanupSchema, EvalRunFailureError as EvalRunFailureErrorSchema, EvalRunLedger as EvalRunLedgerSchema, EvalRunReport as EvalRunReportSchema, EvalRunTarget as EvalRunTargetSchema, summarizeEvalRunLedger } from "./project-contracts.js";
10
+ export type { EvalClassifierObservation, EvalCompositionCase, EvalCompositionCaseResult, EvalCompositionSuite, EvalDecompositionBenchmark, EvalDecompositionBenchmarkCase, EvalDimensionCase, EvalDimensionContrast, EvalDimensionSuite, EvalEvaluationProposal, EvalExecutionPlan, EvalPlanScope, EvalProposedDimension, EvalProjectArtifactsStatus, EvalProjectStatus, EvalRoutingBasisProposal, EvalRunCleanup, EvalRunFailureError, EvalRunLedger, EvalRunQualification, EvalRunReport, EvalRunTarget, EvalScopedExpectedCalls } from "./project-contracts.js";
11
+ export { EVAL_PROJECT_VERSION, EvalArtifactApproval, EvalCompositionSuite as EvalCompositionSuiteSchema, EvalDecompositionBenchmark as EvalDecompositionBenchmarkSchema, EvalDimensionCase as EvalDimensionCaseSchema, EvalDimensionContrast as EvalDimensionContrastSchema, EvalDimensionSuite as EvalDimensionSuiteSchema, EvalEvaluationProposal as EvalEvaluationProposalSchema, EvalExecutionPlan as EvalExecutionPlanSchema, EvalPlanScope as EvalPlanScopeSchema, EvalProposedDimension as EvalProposedDimensionSchema, EvalProjectConfiguration, EvalProjectQuestion, EvalProjectSetupProgress, EvalProjectState, EvalRoutingBasisProposal as EvalRoutingBasisProposalSchema, EvalRunCleanup as EvalRunCleanupSchema, EvalRunFailureError as EvalRunFailureErrorSchema, EvalRunLedger as EvalRunLedgerSchema, EvalRunReport as EvalRunReportSchema, EvalRunTarget as EvalRunTargetSchema, scopedExpectedCalls, summarizeEvalRunLedger } from "./project-contracts.js";
12
12
  export { EvalProjectStore, EvalProjectStoreLive, makeFileEvalProjectStore } from "./project-store.js";
13
13
  export type { EvalProjectWorkflowError, EvalProjectWorkflowShape } from "./project-workflow.js";
14
14
  export { EvalProjectWorkflow, EvalProjectWorkflowLive, makeEvalProjectWorkflow } from "./project-workflow.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { EvalRepositoryInspector, EvalRepositoryInspectorLive, inspectRepository
4
4
  export { OriEvalAuthoring, oriAuthoringFromApi } from "./ori-authoring.js";
5
5
  export { EvalProjectArtifacts, EvalProjectArtifactsLive, evaluationProposalDigest, makeFileEvalProjectArtifacts, routingBasisDigest } from "./project-artifacts.js";
6
6
  export { EVAL_AUTHORING_CASES_PER_DIMENSION, EVAL_AUTHORING_EVALUATION_OUTPUT_TOKENS, EVAL_AUTHORING_REQUEST_BYTES, EVAL_AUTHORING_SOURCE_BYTES, EVAL_AUTHORING_SOURCE_FILES, EvalAuthoringTransport, EvalProjectAuthor, EvalProjectAuthorLive, makeEvalProjectAuthor, readProjectAuthoringSources, selectProjectAuthoringSourceFiles } from "./project-authoring.js";
7
- export { EVAL_PROJECT_VERSION, EvalArtifactApproval, EvalCompositionSuite as EvalCompositionSuiteSchema, EvalDecompositionBenchmark as EvalDecompositionBenchmarkSchema, EvalDimensionCase as EvalDimensionCaseSchema, EvalDimensionContrast as EvalDimensionContrastSchema, EvalDimensionSuite as EvalDimensionSuiteSchema, EvalEvaluationProposal as EvalEvaluationProposalSchema, EvalExecutionPlan as EvalExecutionPlanSchema, EvalPlanScope as EvalPlanScopeSchema, EvalProposedDimension as EvalProposedDimensionSchema, EvalProjectConfiguration, EvalProjectQuestion, EvalProjectSetupProgress, EvalProjectState, EvalRoutingBasisProposal as EvalRoutingBasisProposalSchema, EvalRunCleanup as EvalRunCleanupSchema, EvalRunFailureError as EvalRunFailureErrorSchema, EvalRunLedger as EvalRunLedgerSchema, EvalRunReport as EvalRunReportSchema, EvalRunTarget as EvalRunTargetSchema, summarizeEvalRunLedger } from "./project-contracts.js";
7
+ export { EVAL_PROJECT_VERSION, EvalArtifactApproval, EvalCompositionSuite as EvalCompositionSuiteSchema, EvalDecompositionBenchmark as EvalDecompositionBenchmarkSchema, EvalDimensionCase as EvalDimensionCaseSchema, EvalDimensionContrast as EvalDimensionContrastSchema, EvalDimensionSuite as EvalDimensionSuiteSchema, EvalEvaluationProposal as EvalEvaluationProposalSchema, EvalExecutionPlan as EvalExecutionPlanSchema, EvalPlanScope as EvalPlanScopeSchema, EvalProposedDimension as EvalProposedDimensionSchema, EvalProjectConfiguration, EvalProjectQuestion, EvalProjectSetupProgress, EvalProjectState, EvalRoutingBasisProposal as EvalRoutingBasisProposalSchema, EvalRunCleanup as EvalRunCleanupSchema, EvalRunFailureError as EvalRunFailureErrorSchema, EvalRunLedger as EvalRunLedgerSchema, EvalRunReport as EvalRunReportSchema, EvalRunTarget as EvalRunTargetSchema, scopedExpectedCalls, summarizeEvalRunLedger } from "./project-contracts.js";
8
8
  export { EvalProjectStore, EvalProjectStoreLive, makeFileEvalProjectStore } from "./project-store.js";
9
9
  export { EvalProjectWorkflow, EvalProjectWorkflowLive, makeEvalProjectWorkflow } from "./project-workflow.js";
10
10
  export { questionForStage, withOpenQuestion } from "./questions.js";
@@ -30,8 +30,20 @@ for (const model of manifest.candidateModels) {
30
30
  ? testCase.prompt
31
31
  : [testCase.prompt, "", "Reference material:", "-----", testCase.context, "-----"].join("\\n");
32
32
  const run = await candidate.run({ prompt, caseId: testCase.id });
33
- await judge.autoEvals({ criteria: testCase.rubric, prompt, run });
34
- run.toComplete();
33
+ let candidateCompletionError;
34
+ try {
35
+ run.toComplete();
36
+ } catch (error) {
37
+ candidateCompletionError = error;
38
+ }
39
+ let judgeError;
40
+ try {
41
+ await judge.autoEvals({ criteria: testCase.rubric, prompt, run });
42
+ } catch (error) {
43
+ judgeError = error;
44
+ }
45
+ if (candidateCompletionError !== undefined) throw candidateCompletionError;
46
+ if (judgeError !== undefined) throw judgeError;
35
47
  });
36
48
  }
37
49
  }
@@ -688,6 +688,17 @@ export declare const EvalExecutionPlan: Schema.Struct<{
688
688
  readonly expectedCallCount: Schema.Finite;
689
689
  }>;
690
690
  export type EvalExecutionPlan = typeof EvalExecutionPlan.Type;
691
+ export type EvalScopedExpectedCalls = {
692
+ readonly expectedDimensionCandidateCalls: number;
693
+ readonly expectedDimensionJudgeCalls: number;
694
+ readonly expectedClassifierCalls: number;
695
+ readonly expectedCompositionCandidateCalls: number;
696
+ readonly expectedCompositionJudgeCalls: number;
697
+ readonly expectedCandidateCalls: number;
698
+ readonly expectedJudgeCalls: number;
699
+ readonly expectedCallCount: number;
700
+ };
701
+ export declare function scopedExpectedCalls(plan: Pick<EvalExecutionPlan, "candidateModels" | "selectedCaseIds" | "selectedDecompositionCaseIds" | "selectedCompositionCaseIds">): EvalScopedExpectedCalls;
691
702
  export declare const EvalRunTarget: Schema.Struct<{
692
703
  readonly kind: Schema.Literals<readonly ["configured", "external"]>;
693
704
  readonly identity: Schema.String;
@@ -248,6 +248,27 @@ export const EvalExecutionPlan = Schema.Struct({
248
248
  expectedJudgeCalls: NonNegativeInteger,
249
249
  expectedCallCount: NonNegativeInteger
250
250
  });
251
+ export function scopedExpectedCalls(plan) {
252
+ const candidateModelCount = plan.candidateModels.length;
253
+ const dimensionCaseCount = plan.selectedCaseIds.reduce((sum, selection) => sum + selection.caseIds.length, 0);
254
+ const expectedDimensionCandidateCalls = dimensionCaseCount * candidateModelCount;
255
+ const expectedDimensionJudgeCalls = expectedDimensionCandidateCalls;
256
+ const expectedClassifierCalls = plan.selectedDecompositionCaseIds.length;
257
+ const expectedCompositionCandidateCalls = plan.selectedCompositionCaseIds.length * candidateModelCount;
258
+ const expectedCompositionJudgeCalls = expectedCompositionCandidateCalls;
259
+ const expectedCandidateCalls = expectedDimensionCandidateCalls + expectedCompositionCandidateCalls;
260
+ const expectedJudgeCalls = expectedDimensionJudgeCalls + expectedCompositionJudgeCalls;
261
+ return {
262
+ expectedDimensionCandidateCalls,
263
+ expectedDimensionJudgeCalls,
264
+ expectedClassifierCalls,
265
+ expectedCompositionCandidateCalls,
266
+ expectedCompositionJudgeCalls,
267
+ expectedCandidateCalls,
268
+ expectedJudgeCalls,
269
+ expectedCallCount: expectedCandidateCalls + expectedJudgeCalls + expectedClassifierCalls
270
+ };
271
+ }
251
272
  export const EvalRunTarget = Schema.Struct({
252
273
  kind: Schema.Literals(["configured", "external"]),
253
274
  identity: Schema.String,
@@ -4,7 +4,7 @@ import { Clock, Context, Effect, Layer, Path, Schema } from "effect";
4
4
  import { EvalDimensionContrastError, EvalProjectTransitionError } from "./errors.js";
5
5
  import { EvalRepositoryInspector } from "./inspection.js";
6
6
  import { EvalProjectArtifacts, evaluationProposalDigest, routingBasisDigest } from "./project-artifacts.js";
7
- import { EVAL_PROJECT_VERSION, summarizeEvalRunLedger } from "./project-contracts.js";
7
+ import { EVAL_PROJECT_VERSION, scopedExpectedCalls, summarizeEvalRunLedger } from "./project-contracts.js";
8
8
  import { EvalProjectStore } from "./project-store.js";
9
9
  const isoNow = Effect.map(Clock.currentTimeMillis, (millis) => new Date(millis).toISOString());
10
10
  const questionForProgress = (progress) => {
@@ -134,7 +134,9 @@ const sameLedger = (left, right) => left.expectedCalls === right.expectedCalls &
134
134
  left.knownPricedSubtotalUsd === right.knownPricedSubtotalUsd &&
135
135
  left.unpricedCalls === right.unpricedCalls;
136
136
  const validateQualifiedReport = (state, plan, proposal, report) => {
137
- assertPublishedRoutingActivation(report.activation);
137
+ assertPublishedRoutingActivation(report.activation, {
138
+ dimensionIds: plan.selectedCaseIds.map((entry) => entry.dimensionId)
139
+ });
138
140
  if (report.activation.basisDigest !== state.basisDigest ||
139
141
  report.activation.classifierModel !== state.configuration.classifierModel ||
140
142
  !sameStrings(report.activation.candidateModels, plan.candidateModels)) {
@@ -207,55 +209,77 @@ const validateQualifiedReport = (state, plan, proposal, report) => {
207
209
  throw new Error("comparison results are missing a planned dimension");
208
210
  }
209
211
  const compositionComparisons = report.comparisons.filter((comparison) => comparison.profileId === "composition");
210
- if (compositionComparisons.length !== 1 || compositionComparisons[0] === undefined) {
212
+ if (plan.selectedCompositionCaseIds.length === 0) {
213
+ if (compositionComparisons.length !== 0) {
214
+ throw new Error("comparison results do not cover the planned dimensions exactly once");
215
+ }
216
+ }
217
+ else if (compositionComparisons.length !== 1 || compositionComparisons[0] === undefined) {
211
218
  throw new Error("qualification requires exactly one composition comparison");
212
219
  }
213
- validateComparison(compositionComparisons[0], new Set(plan.selectedCompositionCaseIds));
220
+ else {
221
+ validateComparison(compositionComparisons[0], new Set(plan.selectedCompositionCaseIds));
222
+ }
214
223
  const qualification = report.qualification;
215
224
  if (qualification === undefined) {
216
- throw new Error("qualification report is missing decomposition and composition results");
217
- }
218
- const expectedDecompositionCases = new Set(plan.selectedDecompositionCaseIds);
219
- const observedDecompositionCases = new Set();
220
- for (const observation of qualification.decomposition.observations) {
221
- if (!expectedDecompositionCases.has(observation.caseId) ||
222
- observedDecompositionCases.has(observation.caseId) ||
223
- !observation.passed ||
224
- observation.vectorL1Error > proposal.decompositionBenchmark.maximumVectorL1Error) {
225
- throw new Error("decomposition qualification is incomplete, duplicated, or failed");
226
- }
227
- observedDecompositionCases.add(observation.caseId);
228
- }
229
- if (qualification.decomposition.expectedCases !== expectedDecompositionCases.size ||
230
- qualification.decomposition.passedCases !== expectedDecompositionCases.size ||
231
- observedDecompositionCases.size !== expectedDecompositionCases.size ||
232
- qualification.decomposition.maximumObservedL1Error >
233
- proposal.decompositionBenchmark.maximumVectorL1Error) {
234
- throw new Error("decomposition qualification summary does not match the execution plan");
235
- }
236
- const expectedCompositionCases = new Set(plan.selectedCompositionCaseIds);
237
- const observedCompositionCases = new Set();
238
- for (const result of qualification.composition.cases) {
239
- if (!expectedCompositionCases.has(result.caseId) ||
240
- observedCompositionCases.has(result.caseId)) {
241
- throw new Error("composition qualification contains an unknown or duplicate case");
242
- }
243
- observedCompositionCases.add(result.caseId);
225
+ if (plan.selectedCompositionCaseIds.length !== 0 ||
226
+ plan.selectedDecompositionCaseIds.length !== 0) {
227
+ throw new Error("qualification report is missing decomposition and composition results");
228
+ }
244
229
  }
245
- if (qualification.composition.expectedCases !== expectedCompositionCases.size ||
246
- observedCompositionCases.size !== expectedCompositionCases.size ||
247
- qualification.composition.comparableCases < 1 ||
248
- qualification.composition.agreeingCases !==
249
- qualification.composition.cases.filter((result) => result.passed).length ||
250
- Math.abs(qualification.composition.winnerAgreement -
251
- qualification.composition.agreeingCases / qualification.composition.comparableCases) > 1e-6 ||
252
- qualification.composition.winnerAgreement <
253
- proposal.compositionSuite.minimumWinnerAgreement) {
254
- throw new Error("composition qualification summary does not meet the reviewed threshold");
230
+ else {
231
+ const expectedDecompositionCases = new Set(plan.selectedDecompositionCaseIds);
232
+ const observedDecompositionCases = new Set();
233
+ for (const observation of qualification.decomposition.observations) {
234
+ if (!expectedDecompositionCases.has(observation.caseId) ||
235
+ observedDecompositionCases.has(observation.caseId) ||
236
+ !observation.passed ||
237
+ observation.vectorL1Error > proposal.decompositionBenchmark.maximumVectorL1Error) {
238
+ throw new Error("decomposition qualification is incomplete, duplicated, or failed");
239
+ }
240
+ observedDecompositionCases.add(observation.caseId);
241
+ }
242
+ if (qualification.decomposition.expectedCases !== expectedDecompositionCases.size ||
243
+ qualification.decomposition.passedCases !== expectedDecompositionCases.size ||
244
+ observedDecompositionCases.size !== expectedDecompositionCases.size ||
245
+ qualification.decomposition.maximumObservedL1Error >
246
+ proposal.decompositionBenchmark.maximumVectorL1Error) {
247
+ throw new Error("decomposition qualification summary does not match the execution plan");
248
+ }
249
+ const expectedCompositionCases = new Set(plan.selectedCompositionCaseIds);
250
+ const observedCompositionCases = new Set();
251
+ for (const result of qualification.composition.cases) {
252
+ if (!expectedCompositionCases.has(result.caseId) ||
253
+ observedCompositionCases.has(result.caseId)) {
254
+ throw new Error("composition qualification contains an unknown or duplicate case");
255
+ }
256
+ observedCompositionCases.add(result.caseId);
257
+ }
258
+ if (expectedCompositionCases.size === 0) {
259
+ if (qualification.composition.expectedCases !== 0 ||
260
+ observedCompositionCases.size !== 0 ||
261
+ qualification.composition.comparableCases !== 0 ||
262
+ qualification.composition.agreeingCases !== 0 ||
263
+ qualification.composition.cases.length !== 0) {
264
+ throw new Error("composition qualification summary does not meet the reviewed threshold");
265
+ }
266
+ }
267
+ else if (qualification.composition.expectedCases !== expectedCompositionCases.size ||
268
+ observedCompositionCases.size !== expectedCompositionCases.size ||
269
+ qualification.composition.comparableCases < 1 ||
270
+ qualification.composition.agreeingCases !==
271
+ qualification.composition.cases.filter((result) => result.passed).length ||
272
+ Math.abs(qualification.composition.winnerAgreement -
273
+ qualification.composition.agreeingCases / qualification.composition.comparableCases) > 1e-6 ||
274
+ qualification.composition.winnerAgreement <
275
+ proposal.compositionSuite.minimumWinnerAgreement) {
276
+ throw new Error("composition qualification summary does not meet the reviewed threshold");
277
+ }
255
278
  }
256
- const ledger = summarizeEvalRunLedger(report.comparisons, plan.expectedCallCount, qualification.decomposition.observations);
257
- if (ledger.observedCalls !== plan.expectedCallCount ||
258
- ledger.observedCandidateRows !== plan.expectedCandidateCalls ||
279
+ const expected = scopedExpectedCalls(plan);
280
+ const ledger = summarizeEvalRunLedger(report.comparisons, expected.expectedCallCount, qualification?.decomposition.observations ?? []);
281
+ if (ledger.observedCalls !== expected.expectedCallCount ||
282
+ ledger.observedCandidateRows !== expected.expectedCandidateCalls ||
259
283
  !sameLedger(ledger, report.ledger)) {
260
284
  throw new Error("qualification ledger does not match the completed evidence");
261
285
  }
@@ -718,20 +742,18 @@ export const makeEvalProjectWorkflow = Effect.gen(function* () {
718
742
  .slice(0, scope === "pilot" ? 5 : undefined)
719
743
  .map((testCase) => testCase.id)
720
744
  }));
721
- const caseCount = selectedCaseIds.reduce((sum, entry) => sum + entry.caseIds.length, 0);
722
745
  const selectedDecompositionCaseIds = proposal.decompositionBenchmark.cases
723
746
  .slice(0, scope === "pilot" ? 5 : undefined)
724
747
  .map((testCase) => testCase.id);
725
748
  const selectedCompositionCaseIds = proposal.compositionSuite.cases
726
749
  .slice(0, scope === "pilot" ? 5 : undefined)
727
750
  .map((testCase) => testCase.id);
728
- const expectedDimensionCandidateCalls = caseCount * proposal.candidateModels.length;
729
- const expectedDimensionJudgeCalls = expectedDimensionCandidateCalls;
730
- const expectedClassifierCalls = selectedDecompositionCaseIds.length;
731
- const expectedCompositionCandidateCalls = selectedCompositionCaseIds.length * proposal.candidateModels.length;
732
- const expectedCompositionJudgeCalls = expectedCompositionCandidateCalls;
733
- const expectedCandidateCalls = expectedDimensionCandidateCalls + expectedCompositionCandidateCalls;
734
- const expectedJudgeCalls = expectedDimensionJudgeCalls + expectedCompositionJudgeCalls;
751
+ const expected = scopedExpectedCalls({
752
+ candidateModels: proposal.candidateModels,
753
+ selectedCaseIds,
754
+ selectedDecompositionCaseIds,
755
+ selectedCompositionCaseIds
756
+ });
735
757
  const plan = {
736
758
  version: EVAL_PROJECT_VERSION,
737
759
  planId: crypto.randomUUID(),
@@ -749,14 +771,7 @@ export const makeEvalProjectWorkflow = Effect.gen(function* () {
749
771
  selectedDecompositionCaseIds,
750
772
  selectedCompositionCaseIds,
751
773
  maximumOutputTokens: Math.max(proposal.compositionSuite.maximumOutputTokens, ...proposal.suites.map((suite) => suite.maximumOutputTokens)),
752
- expectedDimensionCandidateCalls,
753
- expectedDimensionJudgeCalls,
754
- expectedClassifierCalls,
755
- expectedCompositionCandidateCalls,
756
- expectedCompositionJudgeCalls,
757
- expectedCandidateCalls,
758
- expectedJudgeCalls,
759
- expectedCallCount: expectedCandidateCalls + expectedJudgeCalls + expectedClassifierCalls
774
+ ...expected
760
775
  };
761
776
  yield* artifacts.materializePlanSuites(root, plan, proposal);
762
777
  yield* artifacts.savePlan(root, plan);
@@ -8,7 +8,7 @@ import { Cause, Effect, Layer } from "effect";
8
8
  import { EvalDimensionContrastError } from "../errors.js";
9
9
  import { EvalRepositoryInspectorLive } from "../inspection.js";
10
10
  import { EvalProjectArtifactsLive } from "../project-artifacts.js";
11
- import { summarizeEvalRunLedger } from "../project-contracts.js";
11
+ import { scopedExpectedCalls, summarizeEvalRunLedger } from "../project-contracts.js";
12
12
  import { EvalProjectStoreLive } from "../project-store.js";
13
13
  import { EvalProjectWorkflow, EvalProjectWorkflowLive } from "../project-workflow.js";
14
14
  const roots = [];
@@ -190,12 +190,12 @@ const reviewedEvaluationInput = {
190
190
  decompositionBenchmark: reviewedDecompositionBenchmark,
191
191
  compositionSuite: reviewedCompositionSuite
192
192
  };
193
- async function completeProjectSetup(root) {
193
+ async function completeProjectSetup(root, candidateModels = "openai/gpt-5.6-luna openai/gpt-5.6-terra openai/gpt-5.6-sol") {
194
194
  await Effect.runPromise(Effect.gen(function* () {
195
195
  const workflow = yield* EvalProjectWorkflow;
196
196
  yield* workflow.setup(root);
197
197
  yield* workflow.answer(root, "Production workload");
198
- yield* workflow.answer(root, "openai/gpt-5.6-luna openai/gpt-5.6-terra openai/gpt-5.6-sol");
198
+ yield* workflow.answer(root, candidateModels);
199
199
  yield* workflow.answer(root, "openai/gpt-5.6-luna");
200
200
  yield* workflow.answer(root, "openai/gpt-5.6-terra");
201
201
  yield* workflow.answer(root, "openai/gpt-5.6-terra");
@@ -310,9 +310,13 @@ test("reviewed artifacts are digest-bound and produce an immutable exact-call pl
310
310
  const generatedSuite = await readFile(path.join(root, ".routekit", "evals", "plans", result.plan.planId, "dimensions", firstDimension.id, `${firstDimension.id}.eval.ts`), "utf8");
311
311
  const completionAssertion = generatedSuite.indexOf("run.toComplete();");
312
312
  const judgeCall = generatedSuite.indexOf("await judge.autoEvals");
313
+ const completionRethrow = generatedSuite.indexOf("throw candidateCompletionError;");
314
+ const judgeRethrow = generatedSuite.indexOf("throw judgeError;");
313
315
  assert.ok(judgeCall >= 0);
314
316
  assert.ok(completionAssertion >= 0);
315
- assert.ok(completionAssertion > judgeCall);
317
+ assert.ok(completionAssertion < judgeCall);
318
+ assert.ok(judgeCall < completionRethrow);
319
+ assert.ok(completionRethrow < judgeRethrow);
316
320
  for (const dimension of reviewedDimensions) {
317
321
  assert.equal((await stat(path.join(root, ".routekit", "evals", "dimensions", dimension.id, "suite.json")))
318
322
  .mode & 0o777, 0o600);
@@ -344,50 +348,48 @@ test("full qualification refuses fewer than twenty reviewed cases per dimension"
344
348
  assert.match(String(exit.cause), /at least 20 reviewed cases/u);
345
349
  }
346
350
  });
347
- const comparisonResults = (plan, runId) => reviewedSuites.map((suite) => {
351
+ const comparisonResults = (plan, runId) => reviewedSuites.flatMap((suite) => {
348
352
  const selected = plan.selectedCaseIds.find((entry) => entry.dimensionId === suite.dimensionId);
353
+ if (selected === undefined)
354
+ return [];
349
355
  const cases = suite.cases.filter((testCase) => selected.caseIds.includes(testCase.id));
350
- const candidateModels = [
351
- "openai/gpt-5.6-luna",
352
- "openai/gpt-5.6-terra",
353
- "openai/gpt-5.6-sol"
354
- ];
355
- return {
356
- version: 1,
357
- comparisonId: `${runId}-${suite.dimensionId}`,
358
- profileId: suite.dimensionId,
359
- suiteDigest: `suite-${suite.dimensionId}`,
360
- judgeModel: "openai/gpt-5.6-terra",
361
- startedAt: "2026-08-18T00:00:00.000Z",
362
- finishedAt: "2026-08-18T00:01:00.000Z",
363
- models: candidateModels.map((model) => ({
364
- model,
365
- cases: cases.map((testCase) => ({
366
- caseId: testCase.id,
367
- outcome: "passed",
368
- measurement: {
369
- judgeScore: 0.9,
370
- inputTokens: 100,
371
- outputTokens: 50,
372
- durationMs: 250
373
- }
374
- }))
375
- })),
376
- calls: [
377
- ...candidateModels.flatMap((model) => cases.map((testCase) => ({
378
- role: "candidate",
356
+ const candidateModels = [...plan.candidateModels];
357
+ return [{
358
+ version: 1,
359
+ comparisonId: `${runId}-${suite.dimensionId}`,
360
+ profileId: suite.dimensionId,
361
+ suiteDigest: `suite-${suite.dimensionId}`,
362
+ judgeModel: "openai/gpt-5.6-terra",
363
+ startedAt: "2026-08-18T00:00:00.000Z",
364
+ finishedAt: "2026-08-18T00:01:00.000Z",
365
+ models: candidateModels.map((model) => ({
379
366
  model,
380
- caseId: testCase.id,
381
- measurement: { inputTokens: 100, outputTokens: 50 }
382
- }))),
383
- ...candidateModels.flatMap(() => cases.map((testCase) => ({
384
- role: "judge",
385
- model: "openai/gpt-5.6-terra",
386
- caseId: testCase.id,
387
- measurement: { inputTokens: 80, outputTokens: 20 }
388
- })))
389
- ]
390
- };
367
+ cases: cases.map((testCase) => ({
368
+ caseId: testCase.id,
369
+ outcome: "passed",
370
+ measurement: {
371
+ judgeScore: 0.9,
372
+ inputTokens: 100,
373
+ outputTokens: 50,
374
+ durationMs: 250
375
+ }
376
+ }))
377
+ })),
378
+ calls: [
379
+ ...candidateModels.flatMap((model) => cases.map((testCase) => ({
380
+ role: "candidate",
381
+ model,
382
+ caseId: testCase.id,
383
+ measurement: { inputTokens: 100, outputTokens: 50 }
384
+ }))),
385
+ ...candidateModels.flatMap(() => cases.map((testCase) => ({
386
+ role: "judge",
387
+ model: "openai/gpt-5.6-terra",
388
+ caseId: testCase.id,
389
+ measurement: { inputTokens: 80, outputTokens: 20 }
390
+ })))
391
+ ]
392
+ }];
391
393
  });
392
394
  const compositionComparison = (plan, runId) => {
393
395
  const cases = reviewedCompositionSuite.cases.filter((testCase) => plan.selectedCompositionCaseIds.includes(testCase.id));
@@ -430,7 +432,12 @@ const compositionComparison = (plan, runId) => {
430
432
  };
431
433
  };
432
434
  const qualifiedReport = (plan, runId, projectId, publishAllowed = true) => {
433
- const comparisons = [...comparisonResults(plan, runId), compositionComparison(plan, runId)];
435
+ const comparisons = [
436
+ ...comparisonResults(plan, runId),
437
+ ...(plan.selectedCompositionCaseIds.length === 0
438
+ ? []
439
+ : [compositionComparison(plan, runId)])
440
+ ];
434
441
  const candidateModels = [...plan.candidateModels];
435
442
  const observations = reviewedDecompositionBenchmark.cases
436
443
  .filter((testCase) => plan.selectedDecompositionCaseIds.includes(testCase.id))
@@ -515,8 +522,8 @@ const qualifiedReport = (plan, runId, projectId, publishAllowed = true) => {
515
522
  }
516
523
  };
517
524
  };
518
- async function createPlan(root, scope = "pilot") {
519
- await completeProjectSetup(root);
525
+ async function createPlan(root, scope = "pilot", candidateModels) {
526
+ await completeProjectSetup(root, candidateModels);
520
527
  return Effect.runPromise(Effect.gen(function* () {
521
528
  const workflow = yield* EvalProjectWorkflow;
522
529
  const basis = yield* workflow.proposeDimensions(root, reviewedProposedDimensions);
@@ -549,6 +556,58 @@ test("run lifecycle retains complete sanitized evidence and activates only after
549
556
  const reportPath = path.join(root, ".routekit", "evals", "runs", outcome.started.runId, "report.json");
550
557
  assert.equal((await stat(reportPath)).mode & 0o777, 0o600);
551
558
  });
559
+ test("finishRun derives ledger expectations from the selected case slice", async () => {
560
+ const root = await makeRepository();
561
+ const created = await createPlan(root, "full", "openai/gpt-5.6-luna openai/gpt-5.6-terra");
562
+ const selectedDimension = created.selectedCaseIds[0];
563
+ const stalePlan = {
564
+ ...created,
565
+ selectedCaseIds: [
566
+ {
567
+ dimensionId: selectedDimension.dimensionId,
568
+ caseIds: [selectedDimension.caseIds[0]]
569
+ }
570
+ ],
571
+ selectedDecompositionCaseIds: [],
572
+ selectedCompositionCaseIds: [],
573
+ expectedDimensionCandidateCalls: 3_400,
574
+ expectedDimensionJudgeCalls: 3_400,
575
+ expectedClassifierCalls: 20,
576
+ expectedCompositionCandidateCalls: 0,
577
+ expectedCompositionJudgeCalls: 0,
578
+ expectedCandidateCalls: 3_400,
579
+ expectedJudgeCalls: 3_400,
580
+ expectedCallCount: 6_820
581
+ };
582
+ await writeFile(path.join(root, ".routekit", "evals", "plans", `${created.planId}.json`), `${JSON.stringify(stalePlan)}\n`);
583
+ const outcome = await Effect.runPromise(Effect.gen(function* () {
584
+ const workflow = yield* EvalProjectWorkflow;
585
+ const started = yield* workflow.startRun(root, stalePlan.planId);
586
+ const current = yield* workflow.status(root);
587
+ const report = qualifiedReport(started.plan, started.runId, current.state.projectId);
588
+ const expected = scopedExpectedCalls(started.plan);
589
+ assert.deepEqual(expected, {
590
+ expectedDimensionCandidateCalls: 2,
591
+ expectedDimensionJudgeCalls: 2,
592
+ expectedClassifierCalls: 0,
593
+ expectedCompositionCandidateCalls: 0,
594
+ expectedCompositionJudgeCalls: 0,
595
+ expectedCandidateCalls: 2,
596
+ expectedJudgeCalls: 2,
597
+ expectedCallCount: 4
598
+ });
599
+ const scopedReport = {
600
+ ...report,
601
+ ledger: summarizeEvalRunLedger(report.comparisons, expected.expectedCallCount)
602
+ };
603
+ const finished = yield* workflow.finishRun(root, scopedReport);
604
+ return { finished, report: scopedReport };
605
+ }).pipe(Effect.provide(ProjectWorkflowTestLive)));
606
+ assert.equal(outcome.finished.state.stage, "qualified");
607
+ assert.equal(outcome.report.ledger.expectedCalls, 4);
608
+ assert.equal(outcome.report.ledger.observedCalls, 4);
609
+ assert.equal(outcome.report.ledger.observedCandidateRows, 2);
610
+ });
552
611
  test("failed runs preserve plan revisions for retry while mismatched plans remain stale", async () => {
553
612
  const root = await makeRepository();
554
613
  const plan = await createPlan(root, "full");
@@ -632,8 +691,8 @@ test("startRun rematerializes stale dimension and composition suite templates",
632
691
  const dimensionManifestPath = path.join(dimensionRoot, "routekit.eval-manifest.json");
633
692
  const compositionManifestPath = path.join(compositionRoot, "routekit.eval-manifest.json");
634
693
  const staleTemplate = `// generated by RouteKit 1.0.21
635
- run.toComplete();
636
694
  await judge.autoEvals({ criteria: testCase.rubric, prompt, run });
695
+ run.toComplete();
637
696
  `;
638
697
  await Promise.all([
639
698
  writeFile(dimensionSuitePath, staleTemplate),
@@ -656,10 +715,14 @@ await judge.autoEvals({ criteria: testCase.rubric, prompt, run });
656
715
  for (const suite of [dimensionSuite, compositionSuite]) {
657
716
  const completionAssertion = suite.indexOf("run.toComplete();");
658
717
  const judgeCall = suite.indexOf("await judge.autoEvals");
718
+ const completionRethrow = suite.indexOf("throw candidateCompletionError;");
719
+ const judgeRethrow = suite.indexOf("throw judgeError;");
659
720
  assert.doesNotMatch(suite, /generated by RouteKit 1\.0\.21/u);
660
721
  assert.ok(judgeCall >= 0);
661
722
  assert.ok(completionAssertion >= 0);
662
- assert.ok(completionAssertion > judgeCall);
723
+ assert.ok(completionAssertion < judgeCall);
724
+ assert.ok(judgeCall < completionRethrow);
725
+ assert.ok(completionRethrow < judgeRethrow);
663
726
  }
664
727
  assert.equal(dimensionManifestAfter, dimensionManifestBefore);
665
728
  assert.equal(compositionManifestAfter, compositionManifestBefore);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-eval-setup",
3
3
  "private": false,
4
- "version": "1.0.24",
4
+ "version": "1.0.25",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "effect": "4.0.0-rc.108",
35
- "@velum-labs/routekit-eval-contracts": "1.0.24",
36
- "@velum-labs/routekit-runtime": "1.0.24"
35
+ "@velum-labs/routekit-eval-contracts": "1.0.25",
36
+ "@velum-labs/routekit-runtime": "1.0.25"
37
37
  },
38
38
  "keywords": [
39
39
  "routekit",