@velum-labs/routekit-eval-setup 1.0.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.
- package/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/effect-api.d.ts +12 -0
- package/dist/effect-api.js +9 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +46 -0
- package/dist/host-metadata.d.ts +32 -0
- package/dist/host-metadata.js +46 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +261 -0
- package/dist/model-selection.d.ts +6 -0
- package/dist/model-selection.js +37 -0
- package/dist/ori-authoring.d.ts +16 -0
- package/dist/ori-authoring.js +17 -0
- package/dist/ori-result.d.ts +45 -0
- package/dist/ori-result.js +1 -0
- package/dist/project-artifacts.d.ts +31 -0
- package/dist/project-artifacts.js +353 -0
- package/dist/project-authoring.d.ts +68 -0
- package/dist/project-authoring.js +431 -0
- package/dist/project-contracts.d.ts +1197 -0
- package/dist/project-contracts.js +396 -0
- package/dist/project-store.d.ts +13 -0
- package/dist/project-store.js +53 -0
- package/dist/project-workflow.d.ts +33 -0
- package/dist/project-workflow.js +904 -0
- package/dist/questions.d.ts +7 -0
- package/dist/questions.js +67 -0
- package/dist/runner.d.ts +8 -0
- package/dist/runner.js +16 -0
- package/dist/service.d.ts +24 -0
- package/dist/service.js +279 -0
- package/dist/state-store.d.ts +21 -0
- package/dist/state-store.js +86 -0
- package/dist/test/inspection.test.d.ts +1 -0
- package/dist/test/inspection.test.js +68 -0
- package/dist/test/model-selection.test.d.ts +1 -0
- package/dist/test/model-selection.test.js +15 -0
- package/dist/test/project-authoring.test.d.ts +1 -0
- package/dist/test/project-authoring.test.js +67 -0
- package/dist/test/project-workflow.test.d.ts +1 -0
- package/dist/test/project-workflow.test.js +516 -0
- package/dist/test/questions.test.d.ts +1 -0
- package/dist/test/questions.test.js +48 -0
- package/dist/test/skill.test.d.ts +1 -0
- package/dist/test/skill.test.js +31 -0
- package/dist/test/state-store.test.d.ts +1 -0
- package/dist/test/state-store.test.js +30 -0
- package/dist/test/workflow.test.d.ts +1 -0
- package/dist/test/workflow.test.js +167 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +1 -0
- package/package.json +52 -0
- package/skills/setup-eval-routing/SKILL.md +149 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
|
|
7
|
+
import { Effect } from "effect";
|
|
8
|
+
import { readProjectAuthoringSources } from "../project-authoring.js";
|
|
9
|
+
const withRepository = async (use) => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-author-sources-"));
|
|
11
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "routekit-author-outside-"));
|
|
12
|
+
try {
|
|
13
|
+
await writeFile(path.join(root, "source.md"), "bounded repository source\n");
|
|
14
|
+
await writeFile(path.join(outside, "secret.md"), "outside\n");
|
|
15
|
+
await use({ root, outside });
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await Promise.all([
|
|
19
|
+
rm(root, { recursive: true, force: true }),
|
|
20
|
+
rm(outside, { recursive: true, force: true })
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const read = (input) => Effect.runPromise(readProjectAuthoringSources(input).pipe(Effect.provide(NodeServicesLayer)));
|
|
25
|
+
test("authoring reads an exact discovered regular source", async () => {
|
|
26
|
+
await withRepository(async ({ root }) => {
|
|
27
|
+
assert.deepEqual(await read({
|
|
28
|
+
repositoryRoot: root,
|
|
29
|
+
selectedFiles: ["source.md"],
|
|
30
|
+
sourceInventory: ["source.md"]
|
|
31
|
+
}), [{ path: "source.md", content: "bounded repository source\n" }]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
test("authoring rejects traversal and absolute selected paths", async () => {
|
|
35
|
+
await withRepository(async ({ root, outside }) => {
|
|
36
|
+
await assert.rejects(read({
|
|
37
|
+
repositoryRoot: root,
|
|
38
|
+
selectedFiles: ["../escape.md"],
|
|
39
|
+
sourceInventory: ["../escape.md"]
|
|
40
|
+
}), /canonical relative path/u);
|
|
41
|
+
const absolute = path.join(outside, "secret.md");
|
|
42
|
+
await assert.rejects(read({
|
|
43
|
+
repositoryRoot: root,
|
|
44
|
+
selectedFiles: [absolute],
|
|
45
|
+
sourceInventory: [absolute]
|
|
46
|
+
}), /canonical relative path/u);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
test("authoring rejects a discovered path replaced by an external symlink", async () => {
|
|
50
|
+
await withRepository(async ({ root, outside }) => {
|
|
51
|
+
await symlink(path.join(outside, "secret.md"), path.join(root, "external.md"));
|
|
52
|
+
await assert.rejects(read({
|
|
53
|
+
repositoryRoot: root,
|
|
54
|
+
selectedFiles: ["external.md"],
|
|
55
|
+
sourceInventory: ["external.md"]
|
|
56
|
+
}), /regular non-symlink file/u);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test("authoring rejects a selected file absent from discovery inventory", async () => {
|
|
60
|
+
await withRepository(async ({ root }) => {
|
|
61
|
+
await assert.rejects(read({
|
|
62
|
+
repositoryRoot: root,
|
|
63
|
+
selectedFiles: ["source.md"],
|
|
64
|
+
sourceInventory: []
|
|
65
|
+
}), /not in the bounded discovery inventory/u);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { after, test } from "node:test";
|
|
6
|
+
import { layer as NodeServicesLayer } from "@effect/platform-node/NodeServices";
|
|
7
|
+
import { Effect, Layer } from "effect";
|
|
8
|
+
import { EvalRepositoryInspectorLive } from "../inspection.js";
|
|
9
|
+
import { EvalProjectArtifactsLive } from "../project-artifacts.js";
|
|
10
|
+
import { summarizeEvalRunLedger } from "../project-contracts.js";
|
|
11
|
+
import { EvalProjectStoreLive } from "../project-store.js";
|
|
12
|
+
import { EvalProjectWorkflow, EvalProjectWorkflowLive } from "../project-workflow.js";
|
|
13
|
+
const roots = [];
|
|
14
|
+
after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
|
|
15
|
+
const ProjectDependenciesLive = Layer.mergeAll(EvalProjectStoreLive, EvalProjectArtifactsLive, EvalRepositoryInspectorLive).pipe(Layer.provide(NodeServicesLayer));
|
|
16
|
+
const ProjectWorkflowTestLive = EvalProjectWorkflowLive.pipe(Layer.provide(ProjectDependenciesLive), Layer.provide(NodeServicesLayer));
|
|
17
|
+
const makeRepository = async () => {
|
|
18
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-project-"));
|
|
19
|
+
roots.push(root);
|
|
20
|
+
await mkdir(path.join(root, "src"), { recursive: true });
|
|
21
|
+
await writeFile(path.join(root, "README.md"), "# Example application\n");
|
|
22
|
+
await writeFile(path.join(root, "src", "model.ts"), 'client.responses.create({ model: "openai/current" });\n');
|
|
23
|
+
return root;
|
|
24
|
+
};
|
|
25
|
+
test("setup creates one durable repository project without invoking model-backed authoring", async () => {
|
|
26
|
+
const root = await makeRepository();
|
|
27
|
+
const first = await Effect.runPromise(Effect.gen(function* () {
|
|
28
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
29
|
+
return yield* workflow.setup(root);
|
|
30
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
31
|
+
assert.equal(first.state.stage, "setup-required");
|
|
32
|
+
assert.equal(first.question?.id, "workload-description");
|
|
33
|
+
assert.equal(first.nextAction, "answer");
|
|
34
|
+
assert.deepEqual(first.state.sourceInventory, ["README.md", "src/model.ts"]);
|
|
35
|
+
const resumed = await Effect.runPromise(Effect.gen(function* () {
|
|
36
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
37
|
+
return yield* workflow.setup(root);
|
|
38
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
39
|
+
assert.equal(resumed.state.projectId, first.state.projectId);
|
|
40
|
+
assert.equal(resumed.state.revision, 0);
|
|
41
|
+
const projectPath = path.join(root, ".routekit", "evals", "project.json");
|
|
42
|
+
const project = JSON.parse(await readFile(projectPath, "utf8"));
|
|
43
|
+
assert.equal(project.stage, "setup-required");
|
|
44
|
+
assert.equal(project.projectId, first.state.projectId);
|
|
45
|
+
assert.equal((await stat(projectPath)).mode & 0o777, 0o600);
|
|
46
|
+
});
|
|
47
|
+
test("answers advance exactly one question and persist a complete compositional configuration", async () => {
|
|
48
|
+
const root = await makeRepository();
|
|
49
|
+
const statuses = await Effect.runPromise(Effect.gen(function* () {
|
|
50
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
51
|
+
yield* workflow.setup(root);
|
|
52
|
+
const workload = yield* workflow.answer(root, "Customer support and documentation requests");
|
|
53
|
+
const candidates = yield* workflow.answer(root, "openai/gpt-5.6-luna, openai/gpt-5.6-terra openai/gpt-5.6-sol");
|
|
54
|
+
const classifier = yield* workflow.answer(root, "openai/gpt-5.6-luna");
|
|
55
|
+
const author = yield* workflow.answer(root, "openai/gpt-5.6-terra");
|
|
56
|
+
const judge = yield* workflow.answer(root, "openai/gpt-5.6-terra");
|
|
57
|
+
const objective = yield* workflow.answer(root, '{"kind":"highest-quality"}');
|
|
58
|
+
const unknown = yield* workflow.answer(root, "0.2");
|
|
59
|
+
const completed = yield* workflow.answer(root, "{}");
|
|
60
|
+
return { workload, candidates, classifier, author, judge, objective, unknown, completed };
|
|
61
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
62
|
+
assert.equal(statuses.workload.question?.id, "candidate-models");
|
|
63
|
+
assert.equal(statuses.workload.state.revision, 1);
|
|
64
|
+
assert.equal(statuses.candidates.question?.id, "classifier-model");
|
|
65
|
+
assert.equal(statuses.classifier.question?.id, "author-model");
|
|
66
|
+
assert.equal(statuses.author.question?.id, "judge-model");
|
|
67
|
+
assert.equal(statuses.judge.question?.id, "routing-objective");
|
|
68
|
+
assert.equal(statuses.objective.question?.id, "maximum-unknown-weight");
|
|
69
|
+
assert.equal(statuses.unknown.question?.id, "routing-constraints");
|
|
70
|
+
assert.equal(statuses.completed.state.stage, "dimensions-review");
|
|
71
|
+
assert.equal(statuses.completed.question, undefined);
|
|
72
|
+
assert.equal(statuses.completed.nextAction, "propose-dimensions");
|
|
73
|
+
if (statuses.completed.state.stage !== "dimensions-review") {
|
|
74
|
+
assert.fail("expected dimensions-review state");
|
|
75
|
+
}
|
|
76
|
+
assert.deepEqual(statuses.completed.state.configuration.candidateModels, [
|
|
77
|
+
"openai/gpt-5.6-luna",
|
|
78
|
+
"openai/gpt-5.6-terra",
|
|
79
|
+
"openai/gpt-5.6-sol"
|
|
80
|
+
]);
|
|
81
|
+
assert.equal(statuses.completed.state.configuration.classifierModel, "openai/gpt-5.6-luna");
|
|
82
|
+
assert.equal(statuses.completed.state.configuration.authorModel, "openai/gpt-5.6-terra");
|
|
83
|
+
assert.equal(statuses.completed.state.configuration.judgeModel, "openai/gpt-5.6-terra");
|
|
84
|
+
assert.deepEqual(statuses.completed.state.configuration.objective, {
|
|
85
|
+
kind: "highest-quality"
|
|
86
|
+
});
|
|
87
|
+
assert.equal(statuses.completed.state.configuration.maximumUnknownWeight, 0.2);
|
|
88
|
+
assert.equal(statuses.completed.state.configuration.constraints, undefined);
|
|
89
|
+
const resumed = await Effect.runPromise(Effect.gen(function* () {
|
|
90
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
91
|
+
return yield* workflow.status(root);
|
|
92
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
93
|
+
assert.equal(resumed?.state.revision, 8);
|
|
94
|
+
assert.equal(resumed?.state.stage, "dimensions-review");
|
|
95
|
+
});
|
|
96
|
+
test("invalid answers fail without changing the durable project revision", async () => {
|
|
97
|
+
const root = await makeRepository();
|
|
98
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
99
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
100
|
+
yield* workflow.setup(root);
|
|
101
|
+
const empty = yield* Effect.exit(workflow.answer(root, " "));
|
|
102
|
+
assert.equal(empty._tag, "Failure");
|
|
103
|
+
const current = yield* workflow.status(root);
|
|
104
|
+
assert.equal(current?.state.revision, 0);
|
|
105
|
+
assert.equal(current?.question?.id, "workload-description");
|
|
106
|
+
yield* workflow.answer(root, "Production workload");
|
|
107
|
+
const duplicateModels = yield* Effect.exit(workflow.answer(root, "openai/one openai/one"));
|
|
108
|
+
assert.equal(duplicateModels._tag, "Failure");
|
|
109
|
+
const afterDuplicate = yield* workflow.status(root);
|
|
110
|
+
assert.equal(afterDuplicate?.state.revision, 1);
|
|
111
|
+
assert.equal(afterDuplicate?.question?.id, "candidate-models");
|
|
112
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
113
|
+
});
|
|
114
|
+
test("corrupt project state preserves the typed project-store failure", async () => {
|
|
115
|
+
const root = await makeRepository();
|
|
116
|
+
await mkdir(path.join(root, ".routekit", "evals"), { recursive: true });
|
|
117
|
+
await writeFile(path.join(root, ".routekit", "evals", "project.json"), "{not-json\n");
|
|
118
|
+
const exit = await Effect.runPromise(Effect.gen(function* () {
|
|
119
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
120
|
+
return yield* Effect.exit(workflow.status(root));
|
|
121
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
122
|
+
assert.equal(exit._tag, "Failure");
|
|
123
|
+
if (exit._tag === "Failure") {
|
|
124
|
+
assert.equal(String(exit.cause).includes("EvalProjectStoreError"), true);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const reviewedDimensions = Array.from({ length: 5 }, (_, index) => ({
|
|
128
|
+
id: `dimension-${String(index + 1)}`,
|
|
129
|
+
description: `Production workload dimension ${String(index + 1)}`,
|
|
130
|
+
includes: [`Requests that primarily exercise dimension ${String(index + 1)}`],
|
|
131
|
+
excludes: [`Requests that primarily exercise another dimension`]
|
|
132
|
+
}));
|
|
133
|
+
const reviewedSuites = reviewedDimensions.map((dimension) => ({
|
|
134
|
+
version: 1,
|
|
135
|
+
dimensionId: dimension.id,
|
|
136
|
+
maximumOutputTokens: 256,
|
|
137
|
+
cases: Array.from({ length: 20 }, (_, index) => ({
|
|
138
|
+
id: `${dimension.id}-case-${String(index + 1)}`,
|
|
139
|
+
prompt: `Solve ${dimension.id} case ${String(index + 1)}`,
|
|
140
|
+
rubric: `The response must satisfy ${dimension.id} case ${String(index + 1)}`
|
|
141
|
+
}))
|
|
142
|
+
}));
|
|
143
|
+
const reviewedDecompositionBenchmark = {
|
|
144
|
+
maximumVectorL1Error: 0.3,
|
|
145
|
+
cases: Array.from({ length: 20 }, (_, index) => ({
|
|
146
|
+
id: `decomposition-case-${String(index + 1)}`,
|
|
147
|
+
request: `Classify production request ${String(index + 1)}`,
|
|
148
|
+
expected: {
|
|
149
|
+
weights: reviewedDimensions.map((dimension, dimensionIndex) => ({
|
|
150
|
+
dimensionId: dimension.id,
|
|
151
|
+
weight: dimensionIndex === index % reviewedDimensions.length ? 1 : 0
|
|
152
|
+
})),
|
|
153
|
+
unknownWeight: 0
|
|
154
|
+
}
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
const reviewedCompositionSuite = {
|
|
158
|
+
maximumOutputTokens: 256,
|
|
159
|
+
minimumWinnerScoreGap: 0.05,
|
|
160
|
+
minimumWinnerAgreement: 0.8,
|
|
161
|
+
cases: Array.from({ length: 20 }, (_, index) => ({
|
|
162
|
+
id: `composition-case-${String(index + 1)}`,
|
|
163
|
+
prompt: `Solve multi-dimension production case ${String(index + 1)}`,
|
|
164
|
+
rubric: `The response must satisfy multi-dimension case ${String(index + 1)}`,
|
|
165
|
+
decomposition: {
|
|
166
|
+
weights: reviewedDimensions.map((dimension, dimensionIndex) => ({
|
|
167
|
+
dimensionId: dimension.id,
|
|
168
|
+
weight: dimensionIndex === index % reviewedDimensions.length ||
|
|
169
|
+
dimensionIndex === (index + 1) % reviewedDimensions.length
|
|
170
|
+
? 0.5
|
|
171
|
+
: 0
|
|
172
|
+
})),
|
|
173
|
+
unknownWeight: 0
|
|
174
|
+
},
|
|
175
|
+
requirements: {
|
|
176
|
+
endpoint: "responses",
|
|
177
|
+
requiresTools: false,
|
|
178
|
+
requiresVision: false
|
|
179
|
+
}
|
|
180
|
+
}))
|
|
181
|
+
};
|
|
182
|
+
const reviewedEvaluationInput = {
|
|
183
|
+
suites: reviewedSuites,
|
|
184
|
+
decompositionBenchmark: reviewedDecompositionBenchmark,
|
|
185
|
+
compositionSuite: reviewedCompositionSuite
|
|
186
|
+
};
|
|
187
|
+
async function completeProjectSetup(root) {
|
|
188
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
189
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
190
|
+
yield* workflow.setup(root);
|
|
191
|
+
yield* workflow.answer(root, "Production workload");
|
|
192
|
+
yield* workflow.answer(root, "openai/gpt-5.6-luna openai/gpt-5.6-terra openai/gpt-5.6-sol");
|
|
193
|
+
yield* workflow.answer(root, "openai/gpt-5.6-luna");
|
|
194
|
+
yield* workflow.answer(root, "openai/gpt-5.6-terra");
|
|
195
|
+
yield* workflow.answer(root, "openai/gpt-5.6-terra");
|
|
196
|
+
yield* workflow.answer(root, '{"kind":"highest-quality"}');
|
|
197
|
+
yield* workflow.answer(root, "0.2");
|
|
198
|
+
yield* workflow.answer(root, "{}");
|
|
199
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
200
|
+
}
|
|
201
|
+
test("reviewed artifacts are digest-bound and produce an immutable exact-call plan", async () => {
|
|
202
|
+
const root = await makeRepository();
|
|
203
|
+
await completeProjectSetup(root);
|
|
204
|
+
const result = await Effect.runPromise(Effect.gen(function* () {
|
|
205
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
206
|
+
const proposedBasis = yield* workflow.proposeDimensions(root, reviewedDimensions);
|
|
207
|
+
assert.equal(proposedBasis.nextAction, "approve-dimensions");
|
|
208
|
+
const basisDigest = proposedBasis.artifacts?.basisProposalDigest;
|
|
209
|
+
assert.ok(basisDigest !== undefined);
|
|
210
|
+
const staleBasisApproval = yield* Effect.exit(workflow.approveDimensions(root, "stale-basis-digest"));
|
|
211
|
+
assert.equal(staleBasisApproval._tag, "Failure");
|
|
212
|
+
const basisApproved = yield* workflow.approveDimensions(root, basisDigest);
|
|
213
|
+
assert.equal(basisApproved.state.stage, "evaluations-review");
|
|
214
|
+
assert.equal(basisApproved.nextAction, "propose-evaluations");
|
|
215
|
+
const evaluationsProposed = yield* workflow.proposeEvaluations(root, {
|
|
216
|
+
...reviewedEvaluationInput
|
|
217
|
+
});
|
|
218
|
+
assert.equal(evaluationsProposed.nextAction, "approve-evaluations");
|
|
219
|
+
const evaluationDigest = evaluationsProposed.artifacts?.evaluationProposalDigest;
|
|
220
|
+
assert.ok(evaluationDigest !== undefined);
|
|
221
|
+
const staleEvaluationApproval = yield* Effect.exit(workflow.approveEvaluations(root, "stale-evaluation-digest"));
|
|
222
|
+
assert.equal(staleEvaluationApproval._tag, "Failure");
|
|
223
|
+
const ready = yield* workflow.approveEvaluations(root, evaluationDigest);
|
|
224
|
+
assert.equal(ready.state.stage, "ready");
|
|
225
|
+
assert.equal(ready.nextAction, "run");
|
|
226
|
+
const plan = yield* workflow.createPlan(root, "pilot");
|
|
227
|
+
return { basisDigest, evaluationDigest, plan };
|
|
228
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
229
|
+
assert.equal(result.plan.basisDigest, result.basisDigest);
|
|
230
|
+
assert.equal(result.plan.evaluationDigest, result.evaluationDigest);
|
|
231
|
+
assert.equal(result.plan.expectedDimensionCandidateCalls, 75);
|
|
232
|
+
assert.equal(result.plan.expectedDimensionJudgeCalls, 75);
|
|
233
|
+
assert.equal(result.plan.expectedClassifierCalls, 5);
|
|
234
|
+
assert.equal(result.plan.expectedCompositionCandidateCalls, 15);
|
|
235
|
+
assert.equal(result.plan.expectedCompositionJudgeCalls, 15);
|
|
236
|
+
assert.equal(result.plan.expectedCandidateCalls, 90);
|
|
237
|
+
assert.equal(result.plan.expectedJudgeCalls, 90);
|
|
238
|
+
assert.equal(result.plan.expectedCallCount, 185);
|
|
239
|
+
assert.equal(result.plan.maximumOutputTokens, 256);
|
|
240
|
+
const planPath = path.join(root, ".routekit", "evals", "plans", `${result.plan.planId}.json`);
|
|
241
|
+
assert.equal((await stat(planPath)).mode & 0o777, 0o600);
|
|
242
|
+
assert.deepEqual(JSON.parse(await readFile(planPath, "utf8")), result.plan);
|
|
243
|
+
for (const dimension of reviewedDimensions) {
|
|
244
|
+
assert.equal((await stat(path.join(root, ".routekit", "evals", "dimensions", dimension.id, "suite.json")))
|
|
245
|
+
.mode & 0o777, 0o600);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
test("full qualification refuses fewer than twenty reviewed cases per dimension", async () => {
|
|
249
|
+
const root = await makeRepository();
|
|
250
|
+
await completeProjectSetup(root);
|
|
251
|
+
const exit = await Effect.runPromise(Effect.gen(function* () {
|
|
252
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
253
|
+
const proposed = yield* workflow.proposeDimensions(root, reviewedDimensions);
|
|
254
|
+
yield* workflow.approveDimensions(root, proposed.artifacts.basisProposalDigest);
|
|
255
|
+
const evaluations = yield* workflow.proposeEvaluations(root, {
|
|
256
|
+
suites: reviewedSuites.map((suite) => ({ ...suite, cases: suite.cases.slice(0, 5) })),
|
|
257
|
+
decompositionBenchmark: {
|
|
258
|
+
...reviewedDecompositionBenchmark,
|
|
259
|
+
cases: reviewedDecompositionBenchmark.cases.slice(0, 5)
|
|
260
|
+
},
|
|
261
|
+
compositionSuite: {
|
|
262
|
+
...reviewedCompositionSuite,
|
|
263
|
+
cases: reviewedCompositionSuite.cases.slice(0, 5)
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
yield* workflow.approveEvaluations(root, evaluations.artifacts.evaluationProposalDigest);
|
|
267
|
+
return yield* Effect.exit(workflow.createPlan(root, "full"));
|
|
268
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
269
|
+
assert.equal(exit._tag, "Failure");
|
|
270
|
+
if (exit._tag === "Failure") {
|
|
271
|
+
assert.match(String(exit.cause), /at least 20 reviewed cases/u);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const comparisonResults = (plan, runId) => reviewedSuites.map((suite) => {
|
|
275
|
+
const selected = plan.selectedCaseIds.find((entry) => entry.dimensionId === suite.dimensionId);
|
|
276
|
+
const cases = suite.cases.filter((testCase) => selected.caseIds.includes(testCase.id));
|
|
277
|
+
const candidateModels = [
|
|
278
|
+
"openai/gpt-5.6-luna",
|
|
279
|
+
"openai/gpt-5.6-terra",
|
|
280
|
+
"openai/gpt-5.6-sol"
|
|
281
|
+
];
|
|
282
|
+
return {
|
|
283
|
+
version: 1,
|
|
284
|
+
comparisonId: `${runId}-${suite.dimensionId}`,
|
|
285
|
+
profileId: suite.dimensionId,
|
|
286
|
+
suiteDigest: `suite-${suite.dimensionId}`,
|
|
287
|
+
judgeModel: "openai/gpt-5.6-terra",
|
|
288
|
+
startedAt: "2026-08-18T00:00:00.000Z",
|
|
289
|
+
finishedAt: "2026-08-18T00:01:00.000Z",
|
|
290
|
+
models: candidateModels.map((model) => ({
|
|
291
|
+
model,
|
|
292
|
+
cases: cases.map((testCase) => ({
|
|
293
|
+
caseId: testCase.id,
|
|
294
|
+
outcome: "passed",
|
|
295
|
+
measurement: {
|
|
296
|
+
judgeScore: 0.9,
|
|
297
|
+
inputTokens: 100,
|
|
298
|
+
outputTokens: 50,
|
|
299
|
+
durationMs: 250
|
|
300
|
+
}
|
|
301
|
+
}))
|
|
302
|
+
})),
|
|
303
|
+
calls: [
|
|
304
|
+
...candidateModels.flatMap((model) => cases.map((testCase) => ({
|
|
305
|
+
role: "candidate",
|
|
306
|
+
model,
|
|
307
|
+
caseId: testCase.id,
|
|
308
|
+
measurement: { inputTokens: 100, outputTokens: 50 }
|
|
309
|
+
}))),
|
|
310
|
+
...candidateModels.flatMap(() => cases.map((testCase) => ({
|
|
311
|
+
role: "judge",
|
|
312
|
+
model: "openai/gpt-5.6-terra",
|
|
313
|
+
caseId: testCase.id,
|
|
314
|
+
measurement: { inputTokens: 80, outputTokens: 20 }
|
|
315
|
+
})))
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
const compositionComparison = (plan, runId) => {
|
|
320
|
+
const cases = reviewedCompositionSuite.cases.filter((testCase) => plan.selectedCompositionCaseIds.includes(testCase.id));
|
|
321
|
+
const candidateModels = [...plan.candidateModels];
|
|
322
|
+
return {
|
|
323
|
+
version: 1,
|
|
324
|
+
comparisonId: `${runId}-composition`,
|
|
325
|
+
profileId: "composition",
|
|
326
|
+
suiteDigest: "suite-composition",
|
|
327
|
+
judgeModel: plan.judgeModel,
|
|
328
|
+
startedAt: "2026-08-18T00:00:00.000Z",
|
|
329
|
+
finishedAt: "2026-08-18T00:01:00.000Z",
|
|
330
|
+
models: candidateModels.map((model) => ({
|
|
331
|
+
model,
|
|
332
|
+
cases: cases.map((testCase) => ({
|
|
333
|
+
caseId: testCase.id,
|
|
334
|
+
outcome: "passed",
|
|
335
|
+
measurement: {
|
|
336
|
+
judgeScore: model === candidateModels[0] ? 0.9 : 0.7,
|
|
337
|
+
inputTokens: 100,
|
|
338
|
+
outputTokens: 50,
|
|
339
|
+
durationMs: 250
|
|
340
|
+
}
|
|
341
|
+
}))
|
|
342
|
+
})),
|
|
343
|
+
calls: [
|
|
344
|
+
...candidateModels.flatMap((model) => cases.map((testCase) => ({
|
|
345
|
+
role: "candidate",
|
|
346
|
+
model,
|
|
347
|
+
caseId: testCase.id,
|
|
348
|
+
measurement: { inputTokens: 100, outputTokens: 50 }
|
|
349
|
+
}))),
|
|
350
|
+
...candidateModels.flatMap(() => cases.map((testCase) => ({
|
|
351
|
+
role: "judge",
|
|
352
|
+
model: plan.judgeModel,
|
|
353
|
+
caseId: testCase.id,
|
|
354
|
+
measurement: { inputTokens: 80, outputTokens: 20 }
|
|
355
|
+
})))
|
|
356
|
+
]
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const qualifiedReport = (plan, runId, projectId, publishAllowed = true) => {
|
|
360
|
+
const comparisons = [...comparisonResults(plan, runId), compositionComparison(plan, runId)];
|
|
361
|
+
const candidateModels = [...plan.candidateModels];
|
|
362
|
+
const observations = reviewedDecompositionBenchmark.cases
|
|
363
|
+
.filter((testCase) => plan.selectedDecompositionCaseIds.includes(testCase.id))
|
|
364
|
+
.map((testCase) => ({
|
|
365
|
+
caseId: testCase.id,
|
|
366
|
+
weights: testCase.expected.weights,
|
|
367
|
+
unknownWeight: testCase.expected.unknownWeight,
|
|
368
|
+
vectorL1Error: 0,
|
|
369
|
+
passed: true,
|
|
370
|
+
measurement: {
|
|
371
|
+
inputTokens: 40,
|
|
372
|
+
outputTokens: 20
|
|
373
|
+
}
|
|
374
|
+
}));
|
|
375
|
+
return {
|
|
376
|
+
version: 1,
|
|
377
|
+
status: "passed",
|
|
378
|
+
runId,
|
|
379
|
+
planId: plan.planId,
|
|
380
|
+
projectId,
|
|
381
|
+
startedAt: "2026-08-18T00:00:00.000Z",
|
|
382
|
+
finishedAt: "2026-08-18T00:02:00.000Z",
|
|
383
|
+
basisDigest: plan.basisDigest,
|
|
384
|
+
evaluationDigest: plan.evaluationDigest,
|
|
385
|
+
target: {
|
|
386
|
+
kind: publishAllowed ? "configured" : "external",
|
|
387
|
+
identity: "target:test",
|
|
388
|
+
publishAllowed
|
|
389
|
+
},
|
|
390
|
+
cleanup: publishAllowed
|
|
391
|
+
? { sessionOpened: true, sessionClosed: true }
|
|
392
|
+
: { sessionOpened: false, sessionClosed: false },
|
|
393
|
+
comparisons,
|
|
394
|
+
qualification: {
|
|
395
|
+
decomposition: {
|
|
396
|
+
expectedCases: observations.length,
|
|
397
|
+
passedCases: observations.length,
|
|
398
|
+
maximumObservedL1Error: 0,
|
|
399
|
+
observations
|
|
400
|
+
},
|
|
401
|
+
composition: {
|
|
402
|
+
expectedCases: plan.selectedCompositionCaseIds.length,
|
|
403
|
+
comparableCases: plan.selectedCompositionCaseIds.length,
|
|
404
|
+
agreeingCases: plan.selectedCompositionCaseIds.length,
|
|
405
|
+
winnerAgreement: 1,
|
|
406
|
+
cases: plan.selectedCompositionCaseIds.map((caseId) => ({
|
|
407
|
+
caseId,
|
|
408
|
+
predictedWinner: candidateModels[0],
|
|
409
|
+
observedWinner: candidateModels[0],
|
|
410
|
+
predictedScoreGap: 0.1,
|
|
411
|
+
observedScoreGap: 0.2,
|
|
412
|
+
passed: true
|
|
413
|
+
}))
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
ledger: summarizeEvalRunLedger(comparisons, plan.expectedCallCount, observations),
|
|
417
|
+
activation: {
|
|
418
|
+
version: 2,
|
|
419
|
+
generatedAt: "2026-08-18T00:02:00.000Z",
|
|
420
|
+
basisDigest: plan.basisDigest,
|
|
421
|
+
evidenceDigest: "evidence-digest",
|
|
422
|
+
classifierModel: plan.classifierModel,
|
|
423
|
+
objective: { kind: "highest-quality" },
|
|
424
|
+
maximumUnknownWeight: 0.2,
|
|
425
|
+
dimensions: reviewedDimensions,
|
|
426
|
+
candidateModels,
|
|
427
|
+
evidence: candidateModels.flatMap((model) => reviewedDimensions.map((dimension) => ({
|
|
428
|
+
model,
|
|
429
|
+
dimensionId: dimension.id,
|
|
430
|
+
suiteDigest: `suite-${dimension.id}`,
|
|
431
|
+
evidenceDigest: `${model}-${dimension.id}`,
|
|
432
|
+
quality: {
|
|
433
|
+
passRate: 1,
|
|
434
|
+
lowerConfidenceBound: 0.56,
|
|
435
|
+
sampleCount: plan.scope === "pilot" ? 5 : 20
|
|
436
|
+
},
|
|
437
|
+
failureRate: 0,
|
|
438
|
+
averageJudgeScore: 0.9,
|
|
439
|
+
p95DurationMs: 250,
|
|
440
|
+
unpricedCalls: plan.scope === "pilot" ? 5 : 20
|
|
441
|
+
})))
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
async function createPlan(root, scope = "pilot") {
|
|
446
|
+
await completeProjectSetup(root);
|
|
447
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
448
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
449
|
+
const basis = yield* workflow.proposeDimensions(root, reviewedDimensions);
|
|
450
|
+
yield* workflow.approveDimensions(root, basis.artifacts.basisProposalDigest);
|
|
451
|
+
const evaluations = yield* workflow.proposeEvaluations(root, reviewedEvaluationInput);
|
|
452
|
+
yield* workflow.approveEvaluations(root, evaluations.artifacts.evaluationProposalDigest);
|
|
453
|
+
return yield* workflow.createPlan(root, scope);
|
|
454
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
455
|
+
}
|
|
456
|
+
test("run lifecycle retains complete sanitized evidence and activates only after cleanup", async () => {
|
|
457
|
+
const root = await makeRepository();
|
|
458
|
+
const plan = await createPlan(root, "full");
|
|
459
|
+
const outcome = await Effect.runPromise(Effect.gen(function* () {
|
|
460
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
461
|
+
const started = yield* workflow.startRun(root, plan.planId);
|
|
462
|
+
const current = yield* workflow.status(root);
|
|
463
|
+
assert.equal(current?.state.stage, "running");
|
|
464
|
+
const report = qualifiedReport(plan, started.runId, current.state.projectId);
|
|
465
|
+
const qualified = yield* workflow.finishRun(root, report);
|
|
466
|
+
const loaded = yield* workflow.result(root, started.runId);
|
|
467
|
+
const activated = yield* workflow.markActivated(root, started.runId, "target:test");
|
|
468
|
+
return { started, qualified, loaded, activated };
|
|
469
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
470
|
+
assert.equal(outcome.qualified.state.stage, "qualified");
|
|
471
|
+
assert.equal(outcome.loaded?.status, "passed");
|
|
472
|
+
assert.equal(outcome.loaded?.ledger.observedCalls, 740);
|
|
473
|
+
assert.equal(outcome.loaded?.ledger.observedCandidateRows, 360);
|
|
474
|
+
assert.equal(outcome.loaded?.ledger.unpricedCalls, 740);
|
|
475
|
+
assert.equal(outcome.activated.state.stage, "activated");
|
|
476
|
+
const reportPath = path.join(root, ".routekit", "evals", "runs", outcome.started.runId, "report.json");
|
|
477
|
+
assert.equal((await stat(reportPath)).mode & 0o777, 0o600);
|
|
478
|
+
});
|
|
479
|
+
test("incomplete evidence and external qualification fail closed", async () => {
|
|
480
|
+
const incompleteRoot = await makeRepository();
|
|
481
|
+
const incompletePlan = await createPlan(incompleteRoot, "full");
|
|
482
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
483
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
484
|
+
const started = yield* workflow.startRun(incompleteRoot, incompletePlan.planId);
|
|
485
|
+
const status = yield* workflow.status(incompleteRoot);
|
|
486
|
+
const report = qualifiedReport(incompletePlan, started.runId, status.state.projectId);
|
|
487
|
+
assert.equal(report.status, "passed");
|
|
488
|
+
if (report.status !== "passed")
|
|
489
|
+
return;
|
|
490
|
+
const [first, ...rest] = report.comparisons;
|
|
491
|
+
assert.ok(first !== undefined);
|
|
492
|
+
const incomplete = {
|
|
493
|
+
...report,
|
|
494
|
+
comparisons: [
|
|
495
|
+
{
|
|
496
|
+
...first,
|
|
497
|
+
models: first.models.map((model, index) => index === 0 ? { ...model, cases: model.cases.slice(0, -1) } : model)
|
|
498
|
+
},
|
|
499
|
+
...rest
|
|
500
|
+
]
|
|
501
|
+
};
|
|
502
|
+
const exit = yield* Effect.exit(workflow.finishRun(incompleteRoot, incomplete));
|
|
503
|
+
assert.equal(exit._tag, "Failure");
|
|
504
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
505
|
+
const externalRoot = await makeRepository();
|
|
506
|
+
const externalPlan = await createPlan(externalRoot, "full");
|
|
507
|
+
await Effect.runPromise(Effect.gen(function* () {
|
|
508
|
+
const workflow = yield* EvalProjectWorkflow;
|
|
509
|
+
const started = yield* workflow.startRun(externalRoot, externalPlan.planId);
|
|
510
|
+
const status = yield* workflow.status(externalRoot);
|
|
511
|
+
const report = qualifiedReport(externalPlan, started.runId, status.state.projectId, false);
|
|
512
|
+
yield* workflow.finishRun(externalRoot, report);
|
|
513
|
+
const exit = yield* Effect.exit(workflow.markActivated(externalRoot, started.runId, "target:test"));
|
|
514
|
+
assert.equal(exit._tag, "Failure");
|
|
515
|
+
}).pipe(Effect.provide(ProjectWorkflowTestLive)));
|
|
516
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { questionForStage } from "../questions.js";
|
|
4
|
+
test("setup exposes exactly one question for each unresolved stage", () => {
|
|
5
|
+
const stages = [
|
|
6
|
+
"surface",
|
|
7
|
+
"data",
|
|
8
|
+
"criteria",
|
|
9
|
+
"constraints",
|
|
10
|
+
"candidates",
|
|
11
|
+
"spend-approval",
|
|
12
|
+
"publish"
|
|
13
|
+
];
|
|
14
|
+
for (const stage of stages) {
|
|
15
|
+
const question = questionForStage(stage);
|
|
16
|
+
assert.equal(question?.id, stage);
|
|
17
|
+
assert.equal(question?.options.length, 3);
|
|
18
|
+
}
|
|
19
|
+
assert.equal(questionForStage("completed"), undefined);
|
|
20
|
+
});
|
|
21
|
+
test("repository findings become workspace-specific setup options", () => {
|
|
22
|
+
const question = questionForStage("surface", {
|
|
23
|
+
repositoryRoot: "/repo",
|
|
24
|
+
materials: [],
|
|
25
|
+
summary: {
|
|
26
|
+
entriesVisited: 2,
|
|
27
|
+
textFilesConsidered: 2,
|
|
28
|
+
filesRead: 2,
|
|
29
|
+
bytesRead: 100,
|
|
30
|
+
skippedOversizedFiles: 0,
|
|
31
|
+
truncated: false
|
|
32
|
+
},
|
|
33
|
+
surfaces: [
|
|
34
|
+
{ name: "support", path: "src/support.ts", model: "openai/current" },
|
|
35
|
+
{ name: "triage", path: "src/triage.ts" }
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(question?.options, [
|
|
39
|
+
"support on openai/current",
|
|
40
|
+
"triage (src/triage.ts)",
|
|
41
|
+
"Stop setup"
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
test("candidate question requires two candidates and a distinct judge", () => {
|
|
45
|
+
const question = questionForStage("candidates");
|
|
46
|
+
assert.match(question?.prompt ?? "", /exactly three unique provider\/model IDs/iu);
|
|
47
|
+
assert.doesNotMatch(question?.options.join("\n") ?? "", /save without choosing|current model,/iu);
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
7
|
+
test("onboarding skill uses the public CLI and preserves approval boundaries", async () => {
|
|
8
|
+
const skill = await readFile(path.join(packageRoot, "skills", "setup-eval-routing", "SKILL.md"), "utf8");
|
|
9
|
+
assert.match(skill, /setup-eval-routing/u);
|
|
10
|
+
assert.match(skill, /one question per turn/iu);
|
|
11
|
+
assert.match(skill, /Never spend or publish silently/u);
|
|
12
|
+
assert.match(skill, /public `routekit eval` CLI/u);
|
|
13
|
+
assert.match(skill, /\$ROUTEKIT eval --help/u);
|
|
14
|
+
for (const command of ["setup", "status", "answer", "validate", "estimate", "run", "publish"]) {
|
|
15
|
+
assert.match(skill, new RegExp(`eval ${command}`, "u"));
|
|
16
|
+
}
|
|
17
|
+
for (const term of [
|
|
18
|
+
"routing basis",
|
|
19
|
+
"workload dimension",
|
|
20
|
+
"request decomposition",
|
|
21
|
+
"evidence matrix",
|
|
22
|
+
"routing activation"
|
|
23
|
+
]) {
|
|
24
|
+
assert.match(skill, new RegExp(term, "iu"));
|
|
25
|
+
}
|
|
26
|
+
assert.doesNotMatch(skill, /\beval prepare\b/u);
|
|
27
|
+
assert.doesNotMatch(skill, /\barea catalog\b/iu);
|
|
28
|
+
assert.doesNotMatch(skill, /Use RouteKit's `EvalSetup` operations/u);
|
|
29
|
+
assert.doesNotMatch(skill, /test:e2e:eval-routing/u);
|
|
30
|
+
assert.doesNotMatch(skill, /OPENROUTER_API_KEY/u);
|
|
31
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|