@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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/effect-api.d.ts +12 -0
  4. package/dist/effect-api.js +9 -0
  5. package/dist/errors.d.ts +91 -0
  6. package/dist/errors.js +46 -0
  7. package/dist/host-metadata.d.ts +32 -0
  8. package/dist/host-metadata.js +46 -0
  9. package/dist/index.d.ts +20 -0
  10. package/dist/index.js +13 -0
  11. package/dist/inspection.d.ts +24 -0
  12. package/dist/inspection.js +261 -0
  13. package/dist/model-selection.d.ts +6 -0
  14. package/dist/model-selection.js +37 -0
  15. package/dist/ori-authoring.d.ts +16 -0
  16. package/dist/ori-authoring.js +17 -0
  17. package/dist/ori-result.d.ts +45 -0
  18. package/dist/ori-result.js +1 -0
  19. package/dist/project-artifacts.d.ts +31 -0
  20. package/dist/project-artifacts.js +353 -0
  21. package/dist/project-authoring.d.ts +68 -0
  22. package/dist/project-authoring.js +431 -0
  23. package/dist/project-contracts.d.ts +1197 -0
  24. package/dist/project-contracts.js +396 -0
  25. package/dist/project-store.d.ts +13 -0
  26. package/dist/project-store.js +53 -0
  27. package/dist/project-workflow.d.ts +33 -0
  28. package/dist/project-workflow.js +904 -0
  29. package/dist/questions.d.ts +7 -0
  30. package/dist/questions.js +67 -0
  31. package/dist/runner.d.ts +8 -0
  32. package/dist/runner.js +16 -0
  33. package/dist/service.d.ts +24 -0
  34. package/dist/service.js +279 -0
  35. package/dist/state-store.d.ts +21 -0
  36. package/dist/state-store.js +86 -0
  37. package/dist/test/inspection.test.d.ts +1 -0
  38. package/dist/test/inspection.test.js +68 -0
  39. package/dist/test/model-selection.test.d.ts +1 -0
  40. package/dist/test/model-selection.test.js +15 -0
  41. package/dist/test/project-authoring.test.d.ts +1 -0
  42. package/dist/test/project-authoring.test.js +67 -0
  43. package/dist/test/project-workflow.test.d.ts +1 -0
  44. package/dist/test/project-workflow.test.js +516 -0
  45. package/dist/test/questions.test.d.ts +1 -0
  46. package/dist/test/questions.test.js +48 -0
  47. package/dist/test/skill.test.d.ts +1 -0
  48. package/dist/test/skill.test.js +31 -0
  49. package/dist/test/state-store.test.d.ts +1 -0
  50. package/dist/test/state-store.test.js +30 -0
  51. package/dist/test/workflow.test.d.ts +1 -0
  52. package/dist/test/workflow.test.js +167 -0
  53. package/dist/types.d.ts +77 -0
  54. package/dist/types.js +1 -0
  55. package/package.json +52 -0
  56. package/skills/setup-eval-routing/SKILL.md +149 -0
@@ -0,0 +1,7 @@
1
+ import type { EvalSetupStage, EvalSetupState } from "@velum-labs/routekit-eval-contracts";
2
+ import type { RepositoryInspection, SetupQuestion } from "./types.js";
3
+ export declare const questionForStage: (stage: EvalSetupStage, inspection?: RepositoryInspection) => SetupQuestion | undefined;
4
+ export declare const withOpenQuestion: (state: EvalSetupState, inspection?: RepositoryInspection) => {
5
+ readonly state: EvalSetupState;
6
+ readonly question?: SetupQuestion;
7
+ };
@@ -0,0 +1,67 @@
1
+ const firstThree = (values, fallback) => {
2
+ const unique = [...new Set(values.filter((value) => value.trim().length > 0))].slice(0, 3);
3
+ return [unique[0] ?? fallback[0], unique[1] ?? fallback[1], unique[2] ?? fallback[2]];
4
+ };
5
+ export const questionForStage = (stage, inspection) => {
6
+ switch (stage) {
7
+ case "surface":
8
+ return {
9
+ id: stage,
10
+ prompt: "Which model-backed workflow should RouteKit optimize first?",
11
+ options: firstThree(inspection?.surfaces.map((surface) => surface.model === undefined
12
+ ? `${surface.name} (${surface.path})`
13
+ : `${surface.name} on ${surface.model}`) ?? [], ["Repository default", "The primary user-facing flow", "Stop setup"])
14
+ };
15
+ case "data":
16
+ return {
17
+ id: stage,
18
+ prompt: "Which representative inputs should this eval measure?",
19
+ options: firstThree(inspection?.materials.map((material) => `${material.path} (${material.kind})`) ?? [], ["Existing tests and fixtures", "Sanitized real examples", "Generate seed cases"])
20
+ };
21
+ case "criteria":
22
+ return {
23
+ id: stage,
24
+ prompt: "What makes an answer acceptable for this workflow?",
25
+ options: ["Correct and complete", "Valid structured output", "Correct tool behavior"]
26
+ };
27
+ case "constraints":
28
+ return {
29
+ id: stage,
30
+ prompt: "What should RouteKit optimize after candidates meet the quality floor?",
31
+ options: ["Lowest cost", "Lowest latency", "Highest quality"]
32
+ };
33
+ case "candidates":
34
+ return {
35
+ id: stage,
36
+ prompt: "Enter exactly three unique provider/model IDs: two candidates, then a distinct judge.",
37
+ options: [
38
+ "Compare my current model with one cheaper candidate",
39
+ "Compare two models I name with a separate judge",
40
+ "Help me find three explicit model IDs"
41
+ ]
42
+ };
43
+ case "spend-approval":
44
+ return {
45
+ id: stage,
46
+ prompt: "The suite is validated. How should RouteKit proceed with paid model calls?",
47
+ options: ["Run a three-case pilot", "Run the full comparison", "Save without running"]
48
+ };
49
+ case "publish":
50
+ return {
51
+ id: stage,
52
+ prompt: "Should RouteKit publish the proposed winner and fallbacks for this profile?",
53
+ options: ["Publish this policy", "Keep the proposal unpublished", "Run another comparison"]
54
+ };
55
+ case "completed":
56
+ return undefined;
57
+ }
58
+ };
59
+ export const withOpenQuestion = (state, inspection) => {
60
+ const question = questionForStage(state.stage, inspection);
61
+ if (question === undefined)
62
+ return { state };
63
+ return {
64
+ state: { ...state, openQuestion: question.prompt },
65
+ question
66
+ };
67
+ };
@@ -0,0 +1,8 @@
1
+ import { Context, Layer } from "effect";
2
+ import type { EvalSetupRunnerShape } from "./types.js";
3
+ declare const EvalSetupRunner_base: Context.ServiceClass<EvalSetupRunner, "@velum-labs/routekit-eval-setup/EvalSetupRunner", EvalSetupRunnerShape>;
4
+ export declare class EvalSetupRunner extends EvalSetupRunner_base {
5
+ static layer(service: EvalSetupRunnerShape): Layer.Layer<EvalSetupRunner, never, never>;
6
+ }
7
+ export declare const EvalSetupRunnerNoop: Layer.Layer<EvalSetupRunner, never, never>;
8
+ export {};
package/dist/runner.js ADDED
@@ -0,0 +1,16 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { EvalSetupRunnerError } from "./errors.js";
3
+ export class EvalSetupRunner extends Context.Service()("@velum-labs/routekit-eval-setup/EvalSetupRunner") {
4
+ static layer(service) {
5
+ return Layer.succeed(EvalSetupRunner, EvalSetupRunner.of(service));
6
+ }
7
+ }
8
+ const unavailable = (operation) => Effect.fail(new EvalSetupRunnerError({
9
+ operation,
10
+ detail: "EvalSetupRunner is not configured"
11
+ }));
12
+ export const EvalSetupRunnerNoop = EvalSetupRunner.layer({
13
+ validate: () => Effect.void,
14
+ estimate: () => Effect.succeed({ callCount: 0, pricingKnown: false }),
15
+ publish: () => unavailable("publishing an activation")
16
+ });
@@ -0,0 +1,24 @@
1
+ import { EVAL_SETUP_VERSION } from "@velum-labs/routekit-eval-contracts";
2
+ import { Context, Effect, Layer, Path } from "effect";
3
+ import { type EvalSetupRunnerError, EvalSetupTransitionError } from "./errors.js";
4
+ import { OriEvalAuthoring } from "./ori-authoring.js";
5
+ import { EvalSetupRunner } from "./runner.js";
6
+ import type { SetupAnswerResult, SetupEstimate, SetupRunResult, SetupStatus } from "./types.js";
7
+ export type EvalSetupError = EvalSetupRunnerError | EvalSetupTransitionError;
8
+ export type EvalSetupShape = {
9
+ readonly prepare: (repositoryRoot: string, profileId: string, options?: Readonly<{
10
+ description?: string;
11
+ }>) => Effect.Effect<SetupAnswerResult, EvalSetupError>;
12
+ readonly status: (repositoryRoot: string, profileId: string) => Effect.Effect<SetupStatus | undefined, EvalSetupError>;
13
+ readonly answer: (repositoryRoot: string, profileId: string, answer: string) => Effect.Effect<SetupAnswerResult, EvalSetupError>;
14
+ readonly validate: (repositoryRoot: string, profileId: string) => Effect.Effect<SetupAnswerResult, EvalSetupError>;
15
+ readonly estimate: (repositoryRoot: string, profileId: string, _mode?: "pilot" | "full") => Effect.Effect<SetupEstimate, EvalSetupError>;
16
+ readonly runApproved: (repositoryRoot: string, profileId: string) => Effect.Effect<SetupRunResult, EvalSetupError>;
17
+ readonly publishApproved: (repositoryRoot: string, profileId: string) => Effect.Effect<SetupRunResult, EvalSetupError>;
18
+ };
19
+ declare const EvalSetup_base: Context.ServiceClass<EvalSetup, "@velum-labs/routekit-eval-setup/EvalSetup", EvalSetupShape>;
20
+ export declare class EvalSetup extends EvalSetup_base {
21
+ }
22
+ export declare const makeEvalSetup: Effect.Effect<EvalSetupShape, never, Path.Path | OriEvalAuthoring | EvalSetupRunner>;
23
+ export declare const EvalSetupLive: Layer.Layer<EvalSetup, never, Path.Path | OriEvalAuthoring | EvalSetupRunner>;
24
+ export { EVAL_SETUP_VERSION };
@@ -0,0 +1,279 @@
1
+ import { EVAL_SETUP_VERSION } from "@velum-labs/routekit-eval-contracts";
2
+ import { Clock, Context, Effect, Layer, Path } from "effect";
3
+ import { EvalSetupTransitionError } from "./errors.js";
4
+ import { authoringRequest, initialHostMetadata, loadHostMetadata, saveHostMetadata } from "./host-metadata.js";
5
+ import { OriEvalAuthoring } from "./ori-authoring.js";
6
+ import { EvalSetupRunner } from "./runner.js";
7
+ const isoNow = Effect.map(Clock.currentTimeMillis, (millis) => new Date(millis).toISOString());
8
+ const asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
9
+ ? value
10
+ : undefined;
11
+ const asString = (value) => typeof value === "string" && value.trim().length > 0 ? value : undefined;
12
+ const asStringArray = (value) => Array.isArray(value)
13
+ ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
14
+ : [];
15
+ const questionFromResult = (result) => {
16
+ if (result.status !== "waiting")
17
+ return undefined;
18
+ const tag = asString(result.tag) ?? "untagged";
19
+ const prompt = asString(result.prompt) ?? asString(result.question);
20
+ if (prompt === undefined)
21
+ return undefined;
22
+ return {
23
+ id: tag,
24
+ prompt,
25
+ options: asStringArray(result.options),
26
+ ...(asString(result.context) === undefined ? {} : { context: asString(result.context) })
27
+ };
28
+ };
29
+ const stageFromResult = (result, fallback) => {
30
+ if (result === undefined)
31
+ return fallback;
32
+ if (result.status === "waiting")
33
+ return asString(result.tag) ?? "waiting";
34
+ return asString(result.status) ?? fallback;
35
+ };
36
+ const stateView = (host, result) => ({
37
+ profileId: host.profileId,
38
+ repositoryRoot: host.repositoryRoot,
39
+ stage: stageFromResult(result ?? host.lastResult, host.lastResult === undefined ? "prepared" : "unknown"),
40
+ revision: host.revision,
41
+ updatedAt: host.updatedAt,
42
+ answers: host.answers,
43
+ ...(host.runDirectory === undefined ? {} : { runDirectory: host.runDirectory }),
44
+ ...(host.scratchWorkspace === undefined ? {} : { scratchWorkspace: host.scratchWorkspace }),
45
+ ...(host.publishApproved === undefined ? {} : { publishApproved: host.publishApproved })
46
+ });
47
+ const statusOf = (host, result) => {
48
+ const current = result ?? host.lastResult;
49
+ const question = current === undefined ? undefined : questionFromResult(current);
50
+ return {
51
+ state: stateView(host, current),
52
+ ...(question === undefined ? {} : { question }),
53
+ ...(current === undefined ? {} : { result: current })
54
+ };
55
+ };
56
+ const eventsFor = (host, result) => {
57
+ const question = result === undefined ? undefined : questionFromResult(result);
58
+ const events = [];
59
+ if (question !== undefined) {
60
+ events.push({
61
+ type: "question",
62
+ stage: "surface",
63
+ prompt: question.prompt
64
+ });
65
+ }
66
+ if (result?.status === "completed") {
67
+ events.push({ type: "completed", profileId: host.profileId });
68
+ }
69
+ return events;
70
+ };
71
+ const mergeHost = (host, result, now, answer) => {
72
+ const state = asRecord(result.state);
73
+ const tag = asString(result.tag) ?? asString(host.lastResult?.tag);
74
+ const rejected = result.status === "waiting" && result.accepted === false;
75
+ return {
76
+ ...host,
77
+ revision: host.revision + (answer === undefined || rejected ? 0 : 1),
78
+ updatedAt: now,
79
+ answers: answer === undefined || tag === undefined || rejected
80
+ ? host.answers
81
+ : { ...host.answers, [tag]: answer },
82
+ runDirectory: asString(result.runDirectory) ?? host.runDirectory,
83
+ scratchWorkspace: asString(result.scratchWorkspace) ??
84
+ asString(state?.scratchWorkspace) ??
85
+ host.scratchWorkspace,
86
+ lastResult: result
87
+ };
88
+ };
89
+ const requireHost = (host, profileId) => {
90
+ if (host === undefined) {
91
+ throw new EvalSetupTransitionError({
92
+ stage: "absent",
93
+ detail: `no setup exists for profile ${JSON.stringify(profileId)}`
94
+ });
95
+ }
96
+ return host;
97
+ };
98
+ const requireResult = (host, detail) => {
99
+ if (host.lastResult === undefined) {
100
+ throw new EvalSetupTransitionError({
101
+ stage: host.lastResult === undefined ? "prepared" : stageFromResult(host.lastResult, "unknown"),
102
+ detail
103
+ });
104
+ }
105
+ return host.lastResult;
106
+ };
107
+ export class EvalSetup extends Context.Service()("@velum-labs/routekit-eval-setup/EvalSetup") {
108
+ }
109
+ export const makeEvalSetup = Effect.gen(function* () {
110
+ const authoring = yield* OriEvalAuthoring;
111
+ const runner = yield* EvalSetupRunner;
112
+ const paths = yield* Path.Path;
113
+ const loadHost = (repositoryRoot, profileId) => Effect.tryPromise({
114
+ try: () => loadHostMetadata(repositoryRoot, profileId),
115
+ catch: (cause) => new EvalSetupTransitionError({
116
+ stage: "absent",
117
+ detail: cause instanceof Error ? cause.message : String(cause)
118
+ })
119
+ });
120
+ const persist = (metadata) => Effect.tryPromise({
121
+ try: () => saveHostMetadata(metadata),
122
+ catch: (cause) => new EvalSetupTransitionError({
123
+ stage: metadata.lastResult === undefined ? "prepared" : "unknown",
124
+ detail: cause instanceof Error ? cause.message : String(cause)
125
+ })
126
+ });
127
+ const prepare = (repositoryRoot, profileId, options) => Effect.gen(function* () {
128
+ const root = paths.resolve(repositoryRoot);
129
+ const now = yield* isoNow;
130
+ const existing = yield* loadHost(root, profileId);
131
+ const description = options?.description?.trim();
132
+ const host = existing ??
133
+ initialHostMetadata({
134
+ profileId,
135
+ repositoryRoot: root,
136
+ now,
137
+ ...(description === undefined || description.length === 0
138
+ ? {}
139
+ : { description: description.slice(0, 1_024) })
140
+ });
141
+ const result = yield* authoring.withAuthoring({ profileId, repositoryRoot: root }, (api) => api.prepare({
142
+ existing: existing === undefined
143
+ ? undefined
144
+ : existing.lastResult?.status === "completed" &&
145
+ existing.scratchWorkspace === undefined &&
146
+ (!Array.isArray(existing.lastResult.evalRuns) ||
147
+ existing.lastResult.evalRuns.length === 0)
148
+ ? "archive"
149
+ : "resume",
150
+ repository: root,
151
+ request: authoringRequest(profileId, host.objective)
152
+ }));
153
+ const next = mergeHost(host, result, now);
154
+ yield* persist(next);
155
+ return { ...statusOf(next, result), events: eventsFor(next, result) };
156
+ });
157
+ const status = (repositoryRoot, profileId) => Effect.gen(function* () {
158
+ const root = paths.resolve(repositoryRoot);
159
+ const host = yield* loadHost(root, profileId);
160
+ if (host === undefined)
161
+ return undefined;
162
+ if (host.runDirectory === undefined)
163
+ return statusOf(host);
164
+ const result = yield* authoring.withAuthoring({ profileId, repositoryRoot: root }, (api) => api.status({ runDirectory: host.runDirectory, repository: root }));
165
+ const next = mergeHost(host, result, yield* isoNow);
166
+ yield* persist(next);
167
+ return statusOf(next, result);
168
+ });
169
+ const answer = (repositoryRoot, profileId, answerText) => Effect.gen(function* () {
170
+ const root = paths.resolve(repositoryRoot);
171
+ const host = requireHost(yield* loadHost(root, profileId), profileId);
172
+ const current = requireResult(host, "setup has no Ori run to answer; call prepare then run");
173
+ if (current.status === "completed") {
174
+ return yield* new EvalSetupTransitionError({
175
+ stage: "completed",
176
+ detail: "setup is already completed"
177
+ });
178
+ }
179
+ if (answerText.trim().length === 0) {
180
+ return yield* new EvalSetupTransitionError({
181
+ stage: stageFromResult(current, "waiting"),
182
+ detail: "an answer must not be empty"
183
+ });
184
+ }
185
+ const result = yield* authoring.withAuthoring({ profileId, repositoryRoot: root }, (api) => api.answer({
186
+ answer: answerText,
187
+ repository: root,
188
+ ...(host.runDirectory === undefined ? {} : { runDirectory: host.runDirectory })
189
+ }));
190
+ const next = mergeHost(host, result, yield* isoNow, answerText.trim());
191
+ yield* persist(next);
192
+ return { ...statusOf(next, result), events: eventsFor(next, result) };
193
+ });
194
+ const validate = (repositoryRoot, profileId) => Effect.gen(function* () {
195
+ const root = paths.resolve(repositoryRoot);
196
+ const host = requireHost(yield* loadHost(root, profileId), profileId);
197
+ const result = requireResult(host, "validation uses Ori's dry-run after an artifact exists");
198
+ yield* runner.validate(result);
199
+ return { ...statusOf(host, result), events: [] };
200
+ });
201
+ const estimate = (repositoryRoot, profileId, mode) => Effect.gen(function* () {
202
+ const root = paths.resolve(repositoryRoot);
203
+ const host = requireHost(yield* loadHost(root, profileId), profileId);
204
+ return yield* runner.estimate(requireResult(host, "estimates require an authored eval manifest"), mode ?? "pilot");
205
+ });
206
+ const runApproved = (repositoryRoot, profileId) => Effect.gen(function* () {
207
+ const root = paths.resolve(repositoryRoot);
208
+ const host = requireHost(yield* loadHost(root, profileId), profileId);
209
+ if (host.lastResult?.status === "completed" &&
210
+ Array.isArray(host.lastResult.evalRuns) &&
211
+ host.lastResult.evalRuns.length > 0) {
212
+ return yield* new EvalSetupTransitionError({
213
+ stage: "completed",
214
+ detail: "the Ori run is already completed"
215
+ });
216
+ }
217
+ if (host.lastResult?.status === "waiting") {
218
+ return yield* new EvalSetupTransitionError({
219
+ stage: stageFromResult(host.lastResult, "waiting"),
220
+ detail: "the run is waiting for a user answer; use eval answer"
221
+ });
222
+ }
223
+ const result = yield* authoring.withAuthoring({ profileId, repositoryRoot: root }, (api) => api.run({
224
+ repository: root,
225
+ ...(host.runDirectory === undefined ? {} : { runDirectory: host.runDirectory })
226
+ }));
227
+ const next = mergeHost(host, result, yield* isoNow);
228
+ yield* persist(next);
229
+ return { ...statusOf(next, result), events: eventsFor(next, result) };
230
+ });
231
+ const publishApproved = (repositoryRoot, profileId) => Effect.gen(function* () {
232
+ const root = paths.resolve(repositoryRoot);
233
+ const host = requireHost(yield* loadHost(root, profileId), profileId);
234
+ const result = requireResult(host, "publication requires a completed Ori run");
235
+ if (result.status !== "completed" ||
236
+ result.ok !== true ||
237
+ !Array.isArray(result.evalRuns) ||
238
+ result.evalRuns.length === 0) {
239
+ return yield* new EvalSetupTransitionError({
240
+ stage: stageFromResult(result, "unknown"),
241
+ detail: "publication requires a successful completed Ori run with eval evidence"
242
+ });
243
+ }
244
+ const published = yield* runner.publish({
245
+ profileId,
246
+ description: host.description ?? `${profileId} routing profile`,
247
+ repositoryRoot: root,
248
+ objective: host.objective,
249
+ result
250
+ });
251
+ const next = {
252
+ ...host,
253
+ publishApproved: true,
254
+ revision: host.revision + 1,
255
+ updatedAt: yield* isoNow
256
+ };
257
+ yield* persist(next);
258
+ return {
259
+ ...statusOf(next, result),
260
+ comparison: published.comparison,
261
+ activation: published.activation,
262
+ events: [
263
+ { type: "publish-approved", profileId },
264
+ { type: "completed", profileId }
265
+ ]
266
+ };
267
+ });
268
+ return EvalSetup.of({
269
+ prepare,
270
+ status,
271
+ answer,
272
+ validate,
273
+ estimate,
274
+ runApproved,
275
+ publishApproved
276
+ });
277
+ });
278
+ export const EvalSetupLive = Layer.effect(EvalSetup, makeEvalSetup);
279
+ export { EVAL_SETUP_VERSION };
@@ -0,0 +1,21 @@
1
+ import { type EvalSetupState } from "@velum-labs/routekit-eval-contracts";
2
+ import { Context, Effect, FileSystem, Layer, Path } from "effect";
3
+ import { EvalSetupStateError } from "./errors.js";
4
+ import type { EvalSetupRunCheckpoint } from "./types.js";
5
+ export type EvalSetupStateStoreShape = {
6
+ readonly load: (repositoryRoot: string, profileId: string) => Effect.Effect<EvalSetupState | undefined, EvalSetupStateError>;
7
+ readonly save: (state: EvalSetupState) => Effect.Effect<void, EvalSetupStateError>;
8
+ readonly loadRun: (repositoryRoot: string, profileId: string) => Effect.Effect<EvalSetupRunCheckpoint | undefined, EvalSetupStateError>;
9
+ readonly saveRun: (repositoryRoot: string, profileId: string, checkpoint: EvalSetupRunCheckpoint) => Effect.Effect<void, EvalSetupStateError>;
10
+ };
11
+ declare const EvalSetupStateStore_base: Context.ServiceClass<EvalSetupStateStore, "@velum-labs/routekit-eval-setup/EvalSetupStateStore", EvalSetupStateStoreShape>;
12
+ export declare class EvalSetupStateStore extends EvalSetupStateStore_base {
13
+ }
14
+ export declare const makeFileEvalSetupStateStore: Effect.Effect<EvalSetupStateStoreShape, never, Path.Path | FileSystem.FileSystem>;
15
+ export declare const EvalSetupStateStoreLive: Layer.Layer<EvalSetupStateStore, never, Path.Path | FileSystem.FileSystem>;
16
+ export declare const initialSetupState: (input: {
17
+ readonly profileId: string;
18
+ readonly repositoryRoot: string;
19
+ readonly now: string;
20
+ }) => EvalSetupState;
21
+ export {};
@@ -0,0 +1,86 @@
1
+ import { EVAL_SETUP_VERSION, EvalComparisonResult, EvalSetupState as EvalSetupStateSchema, PublishedRoutingActivation } from "@velum-labs/routekit-eval-contracts";
2
+ import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
3
+ import { EvalSetupStateError } from "./errors.js";
4
+ const EvalSetupRunCheckpointSchema = Schema.Struct({
5
+ comparison: EvalComparisonResult,
6
+ activation: PublishedRoutingActivation
7
+ });
8
+ export class EvalSetupStateStore extends Context.Service()("@velum-labs/routekit-eval-setup/EvalSetupStateStore") {
9
+ }
10
+ const safeProfileId = (profileId) => /^[a-z0-9](?:[a-z0-9-]{0,62})$/u.test(profileId);
11
+ const setupDirectory = (paths, root, profileId) => paths.join(root, ".routekit", "eval-setup", profileId);
12
+ const statePath = (paths, root, profileId) => paths.join(setupDirectory(paths, root, profileId), "state.json");
13
+ const runPath = (paths, root, profileId) => paths.join(setupDirectory(paths, root, profileId), "run.json");
14
+ const stateFailure = (operation, cause) => new EvalSetupStateError({
15
+ operation,
16
+ detail: cause instanceof Error ? cause.message : String(cause),
17
+ cause
18
+ });
19
+ const validateProfileId = (profileId) => safeProfileId(profileId)
20
+ ? Effect.void
21
+ : Effect.fail(new EvalSetupStateError({
22
+ operation: "resolving setup state",
23
+ detail: `invalid profile id ${JSON.stringify(profileId)}`
24
+ }));
25
+ export const makeFileEvalSetupStateStore = Effect.gen(function* () {
26
+ const fs = yield* FileSystem.FileSystem;
27
+ const paths = yield* Path.Path;
28
+ const loadDocument = (target, decode) => Effect.gen(function* () {
29
+ if (!(yield* fs
30
+ .exists(target)
31
+ .pipe(Effect.mapError((cause) => stateFailure("checking setup document", cause))))) {
32
+ return undefined;
33
+ }
34
+ const raw = yield* fs
35
+ .readFileString(target)
36
+ .pipe(Effect.mapError((cause) => stateFailure("reading setup document", cause)));
37
+ const json = yield* Effect.try({
38
+ try: () => JSON.parse(raw),
39
+ catch: (cause) => stateFailure("parsing setup document", cause)
40
+ });
41
+ return yield* decode(json).pipe(Effect.mapError((cause) => stateFailure("decoding setup document", cause)));
42
+ });
43
+ const saveDocument = (target, value, revision) => Effect.gen(function* () {
44
+ const directory = paths.dirname(target);
45
+ const temporary = paths.join(directory, `setup.${revision}.${crypto.randomUUID()}.tmp`);
46
+ yield* fs
47
+ .makeDirectory(directory, { recursive: true, mode: 0o700 })
48
+ .pipe(Effect.mapError((cause) => stateFailure("creating setup state directory", cause)));
49
+ yield* Effect.ensuring(Effect.gen(function* () {
50
+ yield* fs
51
+ .writeFileString(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
52
+ .pipe(Effect.mapError((cause) => stateFailure("writing setup document", cause)));
53
+ yield* fs
54
+ .rename(temporary, target)
55
+ .pipe(Effect.mapError((cause) => stateFailure("committing setup document", cause)));
56
+ }), fs.remove(temporary, { force: true }).pipe(Effect.ignore));
57
+ });
58
+ return EvalSetupStateStore.of({
59
+ load: (repositoryRoot, profileId) => Effect.gen(function* () {
60
+ yield* validateProfileId(profileId);
61
+ return yield* loadDocument(statePath(paths, repositoryRoot, profileId), Schema.decodeUnknownEffect(EvalSetupStateSchema));
62
+ }),
63
+ save: (state) => Effect.gen(function* () {
64
+ yield* validateProfileId(state.profileId);
65
+ yield* saveDocument(statePath(paths, state.repositoryRoot, state.profileId), state, state.revision);
66
+ }),
67
+ loadRun: (repositoryRoot, profileId) => Effect.gen(function* () {
68
+ yield* validateProfileId(profileId);
69
+ return yield* loadDocument(runPath(paths, repositoryRoot, profileId), Schema.decodeUnknownEffect(EvalSetupRunCheckpointSchema));
70
+ }),
71
+ saveRun: (repositoryRoot, profileId, checkpoint) => Effect.gen(function* () {
72
+ yield* validateProfileId(profileId);
73
+ yield* saveDocument(runPath(paths, repositoryRoot, profileId), checkpoint, checkpoint.comparison.comparisonId);
74
+ })
75
+ });
76
+ });
77
+ export const EvalSetupStateStoreLive = Layer.effect(EvalSetupStateStore, makeFileEvalSetupStateStore);
78
+ export const initialSetupState = (input) => ({
79
+ version: EVAL_SETUP_VERSION,
80
+ profileId: input.profileId,
81
+ repositoryRoot: input.repositoryRoot,
82
+ stage: "surface",
83
+ revision: 0,
84
+ updatedAt: input.now,
85
+ answers: {}
86
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, symlink, 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 } from "effect";
8
+ import { inspectRepository } from "../inspection.js";
9
+ const roots = [];
10
+ after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
11
+ test("repository inspection finds model surfaces and useful material without build trees", async () => {
12
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-inspect-"));
13
+ roots.push(root);
14
+ await mkdir(path.join(root, "src"), { recursive: true });
15
+ await mkdir(path.join(root, "test", "fixtures"), { recursive: true });
16
+ await mkdir(path.join(root, "node_modules", "ignored"), { recursive: true });
17
+ await writeFile(path.join(root, "src", "support.ts"), 'client.responses.create({ model: "openai/gpt-test" });\n');
18
+ await writeFile(path.join(root, "src", "system-prompt.md"), "Be accurate.\n");
19
+ await writeFile(path.join(root, "test", "fixtures", "cases.json"), "[]\n");
20
+ await writeFile(path.join(root, "node_modules", "ignored", "model.ts"), "model = 'bad/id'\n");
21
+ const inspection = await Effect.runPromise(inspectRepository(root).pipe(Effect.provide(NodeServicesLayer)));
22
+ assert.equal(inspection.surfaces.length, 1);
23
+ assert.equal(inspection.surfaces[0]?.path, "src/support.ts");
24
+ assert.equal(inspection.surfaces[0]?.model, "openai/gpt-test");
25
+ assert.equal(inspection.materials.some((item) => item.path === "src/system-prompt.md"), true);
26
+ assert.equal(inspection.materials.some((item) => item.path.includes("node_modules")), false);
27
+ assert.equal(inspection.summary.filesRead, 3);
28
+ assert.equal(inspection.summary.truncated, false);
29
+ });
30
+ test("repository inspection skips oversized files and symlinks that escape the repository", async () => {
31
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-bounds-"));
32
+ const outside = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-outside-"));
33
+ roots.push(root, outside);
34
+ await mkdir(path.join(root, "src"), { recursive: true });
35
+ await writeFile(path.join(root, "src", "oversized.ts"), `client.responses.create({ model: "openai/oversized" });${"x".repeat(300_000)}`);
36
+ await writeFile(path.join(outside, "secret.ts"), 'client.responses.create({ model: "openai/outside" });\n');
37
+ await symlink(outside, path.join(root, "src", "outside-link"));
38
+ const inspection = await Effect.runPromise(inspectRepository(root).pipe(Effect.provide(NodeServicesLayer)));
39
+ assert.equal(inspection.surfaces.length, 0);
40
+ assert.equal(inspection.summary.skippedOversizedFiles, 1);
41
+ assert.equal(inspection.summary.filesRead, 0);
42
+ assert.equal(inspection.summary.truncated, false);
43
+ });
44
+ test("repository inspection ranks README docs and skips test-file model surfaces", async () => {
45
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-docs-"));
46
+ roots.push(root);
47
+ await mkdir(path.join(root, "src"), { recursive: true });
48
+ await mkdir(path.join(root, "docs"), { recursive: true });
49
+ await writeFile(path.join(root, "README.md"), "# RouteKit\n\nOne gateway.\n");
50
+ await writeFile(path.join(root, "docs", "guide.md"), "# Guide\n");
51
+ await writeFile(path.join(root, "src", "foo.test.ts"), 'client.responses.create({ model: "openai/hidden" });\n');
52
+ await writeFile(path.join(root, "src", "support.ts"), 'client.responses.create({ model: "openai/gpt-test" });\n');
53
+ const inspection = await Effect.runPromise(inspectRepository(root).pipe(Effect.provide(NodeServicesLayer)));
54
+ assert.equal(inspection.materials[0]?.path, "README.md");
55
+ assert.equal(inspection.materials[0]?.kind, "doc");
56
+ assert.equal(inspection.materials.some((item) => item.path === "docs/guide.md" && item.kind === "doc"), true);
57
+ assert.equal(inspection.surfaces.some((item) => item.path.includes("foo.test.ts")), false);
58
+ assert.equal(inspection.surfaces[0]?.path, "src/support.ts");
59
+ });
60
+ test("repository inspection synthesizes a docs surface when only README remains", async () => {
61
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-readme-only-"));
62
+ roots.push(root);
63
+ await mkdir(path.join(root, "src"), { recursive: true });
64
+ await writeFile(path.join(root, "README.md"), "# RouteKit\n");
65
+ await writeFile(path.join(root, "src", "foo.test.ts"), 'client.responses.create({ model: "openai/hidden" });\n');
66
+ const inspection = await Effect.runPromise(inspectRepository(root).pipe(Effect.provide(NodeServicesLayer)));
67
+ assert.deepEqual(inspection.surfaces, [{ name: "repository-docs", path: "README.md" }]);
68
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { modelSelectionFromAnswer } from "../model-selection.js";
4
+ test("model selection accepts exactly two candidates followed by a distinct judge", () => {
5
+ assert.deepEqual(modelSelectionFromAnswer("openai/gpt-a anthropic/claude-b google/gemini-judge"), {
6
+ candidates: ["openai/gpt-a", "anthropic/claude-b"],
7
+ judgeModel: "google/gemini-judge"
8
+ });
9
+ });
10
+ test("model selection rejects canned answers, duplicates, aliases, and extra ids", () => {
11
+ assert.throws(() => modelSelectionFromAnswer("Current model, a cheaper candidate, and a stronger candidate"), /exactly three explicit provider\/model ids/iu);
12
+ assert.throws(() => modelSelectionFromAnswer("openai/a openai/a openai/judge"), /three unique model ids/iu);
13
+ assert.throws(() => modelSelectionFromAnswer("openai/a anthropic/b routekit/auto"), /concrete provider\/model id/iu);
14
+ assert.throws(() => modelSelectionFromAnswer("openai/a anthropic/b google/judge mistral/extra"), /exactly three explicit provider\/model ids/iu);
15
+ });
@@ -0,0 +1 @@
1
+ export {};