@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 +2 -2
- package/dist/index.js +1 -1
- package/dist/project-artifacts.js +14 -2
- package/dist/project-contracts.d.ts +11 -0
- package/dist/project-contracts.js +21 -0
- package/dist/project-workflow.js +76 -61
- package/dist/test/project-workflow.test.js +113 -50
- package/package.json +3 -3
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
|
-
|
|
34
|
-
|
|
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,
|
package/dist/project-workflow.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
220
|
+
else {
|
|
221
|
+
validateComparison(compositionComparisons[0], new Set(plan.selectedCompositionCaseIds));
|
|
222
|
+
}
|
|
214
223
|
const qualification = report.qualification;
|
|
215
224
|
if (qualification === undefined) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
qualification.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 = [
|
|
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
|
|
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.
|
|
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.
|
|
36
|
-
"@velum-labs/routekit-runtime": "1.0.
|
|
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",
|