@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,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
|
+
};
|
package/dist/runner.d.ts
ADDED
|
@@ -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 };
|
package/dist/service.js
ADDED
|
@@ -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 {};
|