@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,30 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm, stat } 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 { initialSetupState, makeFileEvalSetupStateStore } from "../state-store.js";
9
+ const roots = [];
10
+ after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
11
+ test("setup state and run checkpoints survive process interruption", async () => {
12
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-state-"));
13
+ roots.push(root);
14
+ await Effect.runPromise(Effect.gen(function* () {
15
+ const store = yield* makeFileEvalSetupStateStore;
16
+ const state = {
17
+ ...initialSetupState({
18
+ profileId: "support",
19
+ repositoryRoot: root,
20
+ now: "2026-08-15T00:00:00.000Z"
21
+ }),
22
+ openQuestion: "Which workflow?"
23
+ };
24
+ yield* store.save(state);
25
+ assert.deepEqual(yield* store.load(root, "support"), state);
26
+ }).pipe(Effect.provide(NodeServicesLayer)));
27
+ const mode = (await stat(path.join(root, ".routekit", "eval-setup", "support", "state.json")))
28
+ .mode;
29
+ assert.equal(mode & 0o777, 0o600);
30
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,167 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, 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 { EvalSetup, EvalSetupLive, EvalSetupRunner, OriEvalAuthoring, oriAuthoringFromApi } from "../index.js";
9
+ const roots = [];
10
+ after(async () => Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))));
11
+ const waiting = (tag, prompt) => ({
12
+ ok: true,
13
+ status: "waiting",
14
+ runDirectory: "/tmp/ori-run",
15
+ tag,
16
+ prompt,
17
+ question: prompt,
18
+ options: ["Support replies", "Documentation", "Routing"],
19
+ context: "Inspected the repository."
20
+ });
21
+ const fakeAuthoring = (turns) => {
22
+ let index = 0;
23
+ const next = () => turns[Math.min(index++, turns.length - 1)] ?? waiting("surface", "Which surface?");
24
+ return {
25
+ prepare: async () => ({ ok: true, status: "prepared", runDirectory: "/tmp/ori-run" }),
26
+ run: async () => next(),
27
+ answer: async (input) => {
28
+ if (/clarif|what do you mean/iu.test(input.answer)) {
29
+ return { ...waiting("surface", "Which surface?"), accepted: false };
30
+ }
31
+ return next();
32
+ },
33
+ status: async () => turns[Math.max(0, index - 1)] ?? {
34
+ ok: true,
35
+ status: "prepared",
36
+ runDirectory: "/tmp/ori-run"
37
+ }
38
+ };
39
+ };
40
+ const makeLayer = (api, publishes) => EvalSetupLive.pipe(Layer.provide(Layer.mergeAll(OriEvalAuthoring.layer(oriAuthoringFromApi(api)), EvalSetupRunner.layer({
41
+ validate: () => Effect.void,
42
+ estimate: () => Effect.succeed({ callCount: 12, maximumCostUsd: 0.42, pricingKnown: true }),
43
+ publish: () => Effect.sync(() => {
44
+ publishes.count += 1;
45
+ return {
46
+ comparison: {
47
+ version: 1,
48
+ comparisonId: "comparison-1",
49
+ profileId: "support",
50
+ suiteDigest: "suite",
51
+ judgeModel: "openai/judge",
52
+ startedAt: "2026-08-16T00:00:00.000Z",
53
+ finishedAt: "2026-08-16T00:01:00.000Z",
54
+ models: []
55
+ },
56
+ activation: {
57
+ version: 2,
58
+ generatedAt: "2026-08-16T00:01:00.000Z",
59
+ basisDigest: "basis",
60
+ evidenceDigest: "evidence",
61
+ classifierModel: "openai/classifier",
62
+ objective: { kind: "lowest-cost", minimumQuality: 0.8 },
63
+ maximumUnknownWeight: 0.2,
64
+ dimensions: [
65
+ {
66
+ id: "support",
67
+ description: "Support requests",
68
+ includes: ["support"],
69
+ excludes: ["other"]
70
+ }
71
+ ],
72
+ candidateModels: ["openai/cheap", "anthropic/strong"],
73
+ evidence: [
74
+ {
75
+ model: "openai/cheap",
76
+ dimensionId: "support",
77
+ suiteDigest: "suite",
78
+ evidenceDigest: "cheap-evidence",
79
+ quality: {
80
+ passRate: 1,
81
+ lowerConfidenceBound: 0.8,
82
+ sampleCount: 1
83
+ },
84
+ failureRate: 0,
85
+ unpricedCalls: 1
86
+ },
87
+ {
88
+ model: "anthropic/strong",
89
+ dimensionId: "support",
90
+ suiteDigest: "suite",
91
+ evidenceDigest: "strong-evidence",
92
+ quality: {
93
+ passRate: 1,
94
+ lowerConfidenceBound: 0.8,
95
+ sampleCount: 1
96
+ },
97
+ failureRate: 0,
98
+ unpricedCalls: 1
99
+ }
100
+ ]
101
+ }
102
+ };
103
+ })
104
+ }))), Layer.provide(NodeServicesLayer));
105
+ test("prepare, run, answer, and status relay Ori questions and resume durable host metadata", async () => {
106
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-ori-"));
107
+ roots.push(root);
108
+ await writeFile(path.join(root, "source.txt"), "unchanged\n");
109
+ const publishes = { count: 0 };
110
+ const api = fakeAuthoring([
111
+ waiting("surface", "Which model call should we evaluate?"),
112
+ {
113
+ ok: true,
114
+ status: "completed",
115
+ runDirectory: "/tmp/ori-run",
116
+ scratchWorkspace: path.join(root, "scratch"),
117
+ evalRuns: []
118
+ }
119
+ ]);
120
+ const first = await Effect.runPromise(Effect.gen(function* () {
121
+ const setup = yield* EvalSetup;
122
+ const prepared = yield* setup.prepare(root, "support");
123
+ assert.equal(prepared.state.stage, "prepared");
124
+ const running = yield* setup.runApproved(root, "support");
125
+ assert.equal(running.state.stage, "surface");
126
+ assert.equal(running.question?.prompt, "Which model call should we evaluate?");
127
+ const clarification = yield* setup.answer(root, "support", "Can you clarify?");
128
+ assert.equal(clarification.state.stage, "surface");
129
+ return yield* setup.answer(root, "support", "1");
130
+ }).pipe(Effect.provide(makeLayer(api, publishes))));
131
+ assert.equal(first.state.stage, "completed");
132
+ assert.equal(first.state.answers.surface, "1");
133
+ const resumed = await Effect.runPromise(Effect.gen(function* () {
134
+ const setup = yield* EvalSetup;
135
+ return yield* setup.prepare(root, "support");
136
+ }).pipe(Effect.provide(makeLayer(api, publishes))));
137
+ assert.equal(resumed.state.answers.surface, "1");
138
+ assert.equal(await readFile(path.join(root, "source.txt"), "utf8"), "unchanged\n");
139
+ });
140
+ test("publication requires a completed Ori run and runs exactly once", async () => {
141
+ const root = await mkdtemp(path.join(os.tmpdir(), "routekit-eval-setup-publish-"));
142
+ roots.push(root);
143
+ const publishes = { count: 0 };
144
+ const completed = {
145
+ ok: true,
146
+ status: "completed",
147
+ runDirectory: "/tmp/ori-run",
148
+ scratchWorkspace: path.join(root, "scratch"),
149
+ evalRuns: [{ ok: true, model: "openai/cheap" }]
150
+ };
151
+ const api = fakeAuthoring([waiting("surface", "Which surface?"), completed]);
152
+ const outcome = await Effect.runPromise(Effect.gen(function* () {
153
+ const setup = yield* EvalSetup;
154
+ yield* setup.prepare(root, "support");
155
+ yield* setup.runApproved(root, "support");
156
+ const early = yield* Effect.exit(setup.publishApproved(root, "support"));
157
+ assert.equal(early._tag, "Failure");
158
+ yield* setup.answer(root, "support", "1");
159
+ const published = yield* setup.publishApproved(root, "support");
160
+ const duplicateRun = yield* Effect.exit(setup.runApproved(root, "support"));
161
+ assert.equal(duplicateRun._tag, "Failure");
162
+ return published;
163
+ }).pipe(Effect.provide(makeLayer(api, publishes))));
164
+ assert.equal(outcome.state.stage, "completed");
165
+ assert.deepEqual(outcome.activation?.candidateModels, ["openai/cheap", "anthropic/strong"]);
166
+ assert.equal(publishes.count, 1);
167
+ });
@@ -0,0 +1,77 @@
1
+ import type { EvalComparisonResult, EvalSetupEvent, EvalSetupRunMode, PublishedRoutingActivation, RoutingObjective } from "@velum-labs/routekit-eval-contracts";
2
+ import type { OriEvalResult } from "./ori-result.js";
3
+ export type SetupQuestion = {
4
+ readonly id: string;
5
+ readonly prompt: string;
6
+ readonly context?: string;
7
+ readonly options: readonly string[];
8
+ };
9
+ export type RepositorySurface = {
10
+ readonly name: string;
11
+ readonly path: string;
12
+ readonly model?: string;
13
+ };
14
+ export type RepositoryMaterial = {
15
+ readonly kind: "doc" | "prompt" | "dataset" | "fixture" | "test" | "schema";
16
+ readonly path: string;
17
+ };
18
+ export type RepositoryInspection = {
19
+ readonly repositoryRoot: string;
20
+ readonly surfaces: readonly RepositorySurface[];
21
+ readonly materials: readonly RepositoryMaterial[];
22
+ readonly summary: {
23
+ readonly entriesVisited: number;
24
+ readonly textFilesConsidered: number;
25
+ readonly filesRead: number;
26
+ readonly bytesRead: number;
27
+ readonly skippedOversizedFiles: number;
28
+ readonly truncated: boolean;
29
+ };
30
+ };
31
+ export type SetupEstimate = {
32
+ readonly callCount: number;
33
+ readonly maximumCostUsd?: number;
34
+ readonly pricingKnown: boolean;
35
+ };
36
+ export type SetupStateView = {
37
+ readonly profileId: string;
38
+ readonly repositoryRoot: string;
39
+ readonly stage: string;
40
+ readonly revision: number;
41
+ readonly updatedAt: string;
42
+ readonly answers: Record<string, string>;
43
+ readonly runDirectory?: string;
44
+ readonly scratchWorkspace?: string;
45
+ readonly publishApproved?: boolean;
46
+ };
47
+ export type SetupStatus = {
48
+ readonly state: SetupStateView;
49
+ readonly question?: SetupQuestion;
50
+ readonly result?: OriEvalResult;
51
+ };
52
+ export type SetupAnswerResult = SetupStatus & {
53
+ readonly events: readonly EvalSetupEvent[];
54
+ };
55
+ export type SetupRunResult = SetupAnswerResult & {
56
+ readonly comparison?: EvalComparisonResult;
57
+ readonly activation?: PublishedRoutingActivation;
58
+ };
59
+ export type EvalSetupRunnerShape = {
60
+ readonly validate: (result: OriEvalResult) => import("effect").Effect.Effect<void, import("./errors.js").EvalSetupRunnerError>;
61
+ readonly estimate: (result: OriEvalResult, mode: EvalSetupRunMode) => import("effect").Effect.Effect<SetupEstimate, import("./errors.js").EvalSetupRunnerError>;
62
+ readonly publish: (input: {
63
+ readonly profileId: string;
64
+ readonly description: string;
65
+ readonly repositoryRoot: string;
66
+ readonly objective: RoutingObjective;
67
+ readonly result: OriEvalResult;
68
+ }) => import("effect").Effect.Effect<{
69
+ readonly comparison: EvalComparisonResult;
70
+ readonly activation: PublishedRoutingActivation;
71
+ }, import("./errors.js").EvalSetupRunnerError>;
72
+ };
73
+ export type EvalSetupRunCheckpoint = {
74
+ readonly comparison: EvalComparisonResult;
75
+ readonly activation: PublishedRoutingActivation;
76
+ };
77
+ export type { EvalSetupRunMode };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@velum-labs/routekit-eval-setup",
3
+ "private": false,
4
+ "version": "1.0.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/velum-labs/routekit.git",
8
+ "directory": "packages/eval-setup"
9
+ },
10
+ "description": "Durable Effect onboarding for compositional RouteKit eval routing.",
11
+ "license": "Apache-2.0",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./effect": {
19
+ "types": "./dist/effect-api.d.ts",
20
+ "default": "./dist/effect-api.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "skills",
26
+ "LICENSE"
27
+ ],
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org",
30
+ "access": "public",
31
+ "provenance": true
32
+ },
33
+ "dependencies": {
34
+ "effect": "4.0.0-rc.108",
35
+ "@velum-labs/routekit-eval-contracts": "1.0.0",
36
+ "@velum-labs/routekit-runtime": "1.0.0"
37
+ },
38
+ "keywords": [
39
+ "routekit",
40
+ "eval",
41
+ "onboarding",
42
+ "routing"
43
+ ],
44
+ "devDependencies": {
45
+ "@effect/platform-node": "4.0.0-rc.108"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -b",
49
+ "clean": "tsc -b --clean",
50
+ "test": "node --test \"dist/test/*.test.js\""
51
+ }
52
+ }
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: setup-eval-routing
3
+ description: >-
4
+ Onboard or maintain a repository's compositional RouteKit eval routing through
5
+ the public routekit eval CLI. Use when the user wants to define a routing
6
+ basis, review workload dimensions, author or approve evaluations, validate or
7
+ estimate a billed plan, run model evidence, inspect results, activate
8
+ model:auto routing, or resume an interrupted eval project.
9
+ ---
10
+
11
+ # Set Up Eval Routing
12
+
13
+ Use the public `routekit eval` CLI as the product boundary. Do not substitute
14
+ internal services, a standalone eval executable, or testkit qualification
15
+ commands for missing CLI functionality.
16
+
17
+ Inside the RouteKit source checkout, build first and use:
18
+
19
+ ```text
20
+ node packages/cli/dist/index.js
21
+ ```
22
+
23
+ For an installed release, use `routekit`. Refer to either form as `$ROUTEKIT`.
24
+
25
+ ## Discover the available interface
26
+
27
+ Run `$ROUTEKIT eval --help` and the relevant subcommand help before acting. Use
28
+ only commands and flags exposed by that CLI version. Prefer `--json` for state,
29
+ plans, estimates, and results.
30
+
31
+ Normal model-backed commands use RouteKit's standard target resolution:
32
+
33
+ 1. an explicit global `--remote <name>` or `--local` selection;
34
+ 2. the active remote, when configured; otherwise
35
+ 3. the local daemon.
36
+
37
+ Do not ask for a gateway URL or credential when that configured target works.
38
+ An explicitly supported external gateway mode may use `--gateway-url` and one
39
+ private credential source, but it is for qualification only and must never
40
+ publish a routing activation.
41
+
42
+ ## Resume or initialize
43
+
44
+ 1. Run `$ROUTEKIT --json eval status` from the repository root.
45
+ 2. If no eval project exists, run `$ROUTEKIT --json eval setup`.
46
+ 3. Follow `nextAction` and the returned artifact paths. Durable state lives
47
+ under `.routekit/evals`; resume it rather than starting over after an
48
+ interruption.
49
+ 4. When setup returns a question, relay exactly that question and its context.
50
+ Ask one question per turn and never answer it for the user.
51
+ 5. Submit the answer unchanged with `eval answer`. Prefer a private temporary
52
+ answer file for multiline text when the CLI supports one, then remove it.
53
+
54
+ Candidate, classifier, author, and judge roles must use explicit
55
+ `provider/model` IDs. Eval traffic must never use `model: auto`.
56
+
57
+ ## Build and review the routing basis
58
+
59
+ Use the CLI workflow in this order, following the current `nextAction`:
60
+
61
+ 1. `eval propose dimensions`
62
+ 2. review the generated routing basis and every workload dimension;
63
+ 3. `eval approve dimensions`
64
+ 4. `eval propose evaluations`
65
+ 5. review every dimension suite, case identity, rubric, manifest, and source
66
+ boundary;
67
+ 6. `eval approve evaluations`
68
+ 7. `eval validate`
69
+
70
+ Treat proposals as review material, not activation evidence. Approval is bound
71
+ to the exact artifact digest. If an artifact changes, validate and approve the
72
+ new digest rather than reusing an old approval.
73
+
74
+ A useful routing basis normally contains 5–10 separable workload dimensions.
75
+ Each definition should include positive scope, exclusions, and boundary
76
+ examples. Request-envelope capabilities such as tools, vision, context, and
77
+ maximum output are hard requirements, not semantic workload dimensions.
78
+
79
+ Keep generated evaluations and sanitized structured results reviewable in the
80
+ repository when the user approves committing them. Do not hand-edit immutable
81
+ plans or measured run records.
82
+
83
+ ## Preserve the classifier boundary
84
+
85
+ The decomposition classifier receives only:
86
+
87
+ - the request; and
88
+ - the reviewed workload-dimension definitions.
89
+
90
+ It emits one weight per dimension plus an unknown weight, normalized to sum to
91
+ one. It must not receive candidate models, evidence, prices, objectives,
92
+ selected models, fallbacks, or previous routing decisions.
93
+
94
+ Model selection is deterministic. It combines the request decomposition, hard
95
+ requirements, the approved objective and constraints, and the published
96
+ model-by-dimension evidence matrix.
97
+
98
+ ## Estimate and run
99
+
100
+ Before every billed step:
101
+
102
+ 1. explain what will call models and show the resolved target and explicit model
103
+ roles;
104
+ 2. run `eval estimate` for the intended scope;
105
+ 3. report exact call and token limits and the CLI's pricing status—missing
106
+ pricing is unknown, never zero; and
107
+ 4. obtain explicit user approval for that plan.
108
+
109
+ Run the immutable plan with `eval run` using the plan identifier returned by
110
+ the CLI. Do not silently reduce case counts, change candidates, replace failed
111
+ rows, or retry a completed paid plan. After an interruption or ambiguous
112
+ result, use `eval status` and `eval results` before deciding whether work
113
+ remains.
114
+
115
+ Never expose credentials, prompts, responses, headers, or raw child output.
116
+ Never recursively evaluate through `model: auto`.
117
+
118
+ ## Review results and activate
119
+
120
+ Use `eval results` to review the decomposition benchmark, every dimension
121
+ suite, the composition benchmark, the complete evidence matrix, accounting,
122
+ and cleanup outcome.
123
+
124
+ Run `eval publish` only when all of these are true:
125
+
126
+ - dimensions and evaluations were approved at their current digests;
127
+ - validation passed and the immutable plan is still fresh;
128
+ - every configured candidate has exactly one judged result for every expected
129
+ case in every dimension;
130
+ - the decomposition and composition benchmarks passed;
131
+ - the run has zero active reservations and zero unknown measurements;
132
+ - the user reviewed the results and explicitly approved activation; and
133
+ - the run used a configured local or remote RouteKit target, not an external
134
+ gateway.
135
+
136
+ Publication installs already-measured evidence atomically; it must not perform
137
+ another billed run. After publication, check `eval status`, then verify an
138
+ ordinary headerless `model: auto` request. Use `routekit calls inspect <call-id>`
139
+ to inspect sanitized routing provenance when needed.
140
+
141
+ ## Safety
142
+
143
+ - Never spend or publish silently.
144
+ - Never send repository material before approval for the model-backed step.
145
+ - Never log, echo, or commit credentials.
146
+ - Never describe unknown cost as zero.
147
+ - Never publish incomplete, stale, mismatched, duplicated, or cutoff evidence.
148
+ - Never claim that a passing pilot or classifier-only run is production routing
149
+ qualification.