@velum-labs/routekit-eval-setup 1.0.26 → 1.1.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/dist/adapters/git-basis-snapshot.d.ts +27 -0
- package/dist/adapters/git-basis-snapshot.js +178 -0
- package/dist/basis-candidates.d.ts +72 -0
- package/dist/basis-candidates.js +157 -0
- package/dist/basis-projection.d.ts +9 -0
- package/dist/basis-projection.js +30 -0
- package/dist/basis-validation.d.ts +38 -0
- package/dist/basis-validation.js +187 -0
- package/dist/effect-api.d.ts +8 -0
- package/dist/effect-api.js +5 -0
- package/dist/errors.d.ts +11 -1
- package/dist/errors.js +5 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +10 -2
- package/dist/project-artifacts.d.ts +26 -1
- package/dist/project-artifacts.js +52 -1
- package/dist/project-contracts.d.ts +3594 -0
- package/dist/project-contracts.js +344 -1
- package/dist/project-store.d.ts +3 -1
- package/dist/project-store.js +39 -33
- package/dist/project-workflow.d.ts +2 -2
- package/dist/services/basis-authoring/service.d.ts +253 -0
- package/dist/services/basis-authoring/service.js +547 -0
- package/dist/services/basis-onboarding/service.d.ts +48 -0
- package/dist/services/basis-onboarding/service.js +491 -0
- package/dist/services/basis-repair/service.d.ts +25 -0
- package/dist/services/basis-repair/service.js +249 -0
- package/dist/test/basis-authoring.test.d.ts +1 -0
- package/dist/test/basis-authoring.test.js +194 -0
- package/dist/test/basis-onboarding.test.d.ts +1 -0
- package/dist/test/basis-onboarding.test.js +385 -0
- package/dist/test/basis-repair.test.d.ts +1 -0
- package/dist/test/basis-repair.test.js +338 -0
- package/dist/test/basis-v3.test.d.ts +1 -0
- package/dist/test/basis-v3.test.js +262 -0
- package/dist/test/project-v2.test.d.ts +1 -0
- package/dist/test/project-v2.test.js +99 -0
- package/package.json +4 -3
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { routingBasisApprovalDigestV3, routingContractDigest } from "@velum-labs/routekit-eval-contracts";
|
|
2
|
+
import { Clock, Context, Effect, Layer } from "effect";
|
|
3
|
+
import { BASIS_AUTHOR_INPUT_TOKENS_PER_ATTEMPT, BASIS_AUTHOR_OUTPUT_TOKENS_PER_ATTEMPT, DEFAULT_BASIS_CANDIDATE_RECIPES_V3, splitBasisTasksChronologicallyV3 } from "../../basis-candidates.js";
|
|
4
|
+
import { approveSelectedBasisV3, inspectBasisCandidatesV3, selectValidatedBasisCandidatesV3 } from "../../basis-validation.js";
|
|
5
|
+
import { projectRoutingBasisV3 } from "../../basis-projection.js";
|
|
6
|
+
import { captureFrozenRepositorySnapshotV3 } from "../../adapters/git-basis-snapshot.js";
|
|
7
|
+
import { EVAL_PROJECT_V2_VERSION } from "../../project-contracts.js";
|
|
8
|
+
import { EvalProjectTransitionError } from "../../errors.js";
|
|
9
|
+
import { EvalProjectArtifacts } from "../../project-artifacts.js";
|
|
10
|
+
import { EvalProjectStore } from "../../project-store.js";
|
|
11
|
+
export class BasisOnboarding extends Context.Service()("@velum-labs/routekit-eval-setup/BasisOnboarding") {
|
|
12
|
+
}
|
|
13
|
+
const transitionError = (state, detail) => new EvalProjectTransitionError({ state, detail });
|
|
14
|
+
const sameIds = (left, right) => left.length === right.length &&
|
|
15
|
+
left.every((value, index) => value === right[index]);
|
|
16
|
+
const sameValue = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
17
|
+
const sameValidationResults = (left, right) => left.length === right.length &&
|
|
18
|
+
left.every((result, index) => {
|
|
19
|
+
const other = right[index];
|
|
20
|
+
return (other !== undefined &&
|
|
21
|
+
result.candidateId === other.candidateId &&
|
|
22
|
+
result.basisDigest === other.basisDigest &&
|
|
23
|
+
result.validationDatasetDigest === other.validationDatasetDigest &&
|
|
24
|
+
result.classifierConfigurationDigest ===
|
|
25
|
+
other.classifierConfigurationDigest &&
|
|
26
|
+
routingContractDigest("routekit.basis-validation-result.v1", "result", JSON.parse(JSON.stringify(result))) ===
|
|
27
|
+
routingContractDigest("routekit.basis-validation-result.v1", "result", JSON.parse(JSON.stringify(other))));
|
|
28
|
+
});
|
|
29
|
+
const verifyDigest = (domain, field, value, digestField, expected) => {
|
|
30
|
+
const semantic = { ...value };
|
|
31
|
+
delete semantic[digestField];
|
|
32
|
+
return routingContractDigest(domain, field, semantic) === expected;
|
|
33
|
+
};
|
|
34
|
+
const requireV2 = (document) => document === undefined
|
|
35
|
+
? Effect.fail(transitionError("missing", "initialize the V3 basis project first"))
|
|
36
|
+
: document.version === EVAL_PROJECT_V2_VERSION
|
|
37
|
+
? Effect.succeed(document)
|
|
38
|
+
: Effect.fail(transitionError(document.stage, "legacy V1 projects are not implicitly converted to Classification V3"));
|
|
39
|
+
export const makeBasisOnboarding = Effect.gen(function* () {
|
|
40
|
+
const store = yield* EvalProjectStore;
|
|
41
|
+
const artifacts = yield* EvalProjectArtifacts;
|
|
42
|
+
const load = (repositoryRoot) => store.loadDocument(repositoryRoot).pipe(Effect.flatMap((document) => document === undefined
|
|
43
|
+
? Effect.void.pipe(Effect.as(undefined))
|
|
44
|
+
: document.version === EVAL_PROJECT_V2_VERSION
|
|
45
|
+
? Effect.succeed(document)
|
|
46
|
+
: Effect.fail(transitionError(document.stage, "legacy V1 projects are not implicitly converted to Classification V3"))));
|
|
47
|
+
const now = Clock.currentTimeMillis.pipe(Effect.map((milliseconds) => new Date(milliseconds).toISOString()));
|
|
48
|
+
const transition = (state, operation) => Effect.try({
|
|
49
|
+
try: operation,
|
|
50
|
+
catch: (cause) => transitionError(state, cause instanceof Error ? cause.message : String(cause))
|
|
51
|
+
});
|
|
52
|
+
const validatePreparedEvidence = (input) => {
|
|
53
|
+
const eligibleCandidateIds = input.structureReports
|
|
54
|
+
.filter((report) => report.eligible)
|
|
55
|
+
.map((report) => report.candidateId);
|
|
56
|
+
const datasetCandidateIds = input.datasets.map((dataset) => {
|
|
57
|
+
const candidate = input.candidates.find((entry) => entry.basisDigest === dataset.basisDigest);
|
|
58
|
+
if (candidate === undefined) {
|
|
59
|
+
throw new Error("validation dataset refers to an unknown basis");
|
|
60
|
+
}
|
|
61
|
+
if (dataset.splitDigest !== input.state.split.splitDigest ||
|
|
62
|
+
!sameIds(dataset.tasks.map((task) => task.caseId), input.state.split.validation.map((task) => task.caseId))) {
|
|
63
|
+
throw new Error(`validation dataset for ${JSON.stringify(candidate.candidateId)} does not use the frozen validation cohort`);
|
|
64
|
+
}
|
|
65
|
+
return candidate.candidateId;
|
|
66
|
+
});
|
|
67
|
+
if (input.validationPlan.repositoryDigest !==
|
|
68
|
+
input.state.snapshot.repositoryDigest ||
|
|
69
|
+
input.validationPlan.splitDigest !== input.state.split.splitDigest ||
|
|
70
|
+
!verifyDigest("routekit.basis-validation-plan.v1", "plan", input.validationPlan, "planDigest", input.validationPlan.planDigest) ||
|
|
71
|
+
!sameIds(input.validationPlan.candidateIds, eligibleCandidateIds) ||
|
|
72
|
+
!sameIds(input.validationPlan.classifierBindings.map((binding) => binding.candidateId), eligibleCandidateIds) ||
|
|
73
|
+
!sameIds(datasetCandidateIds, eligibleCandidateIds) ||
|
|
74
|
+
!sameIds(input.validationPlan.datasetDigests, input.datasets.map((dataset) => dataset.datasetDigest))) {
|
|
75
|
+
throw new Error("validation plan must bind every structurally eligible candidate and dataset");
|
|
76
|
+
}
|
|
77
|
+
for (const binding of input.validationPlan.classifierBindings) {
|
|
78
|
+
const candidate = input.candidates.find((entry) => entry.candidateId === binding.candidateId);
|
|
79
|
+
if (candidate === undefined ||
|
|
80
|
+
binding.basisDigest !== candidate.basisDigest) {
|
|
81
|
+
throw new Error("validation classifier bindings must match each exact candidate basis");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const plannedPrimaryCalls = input.datasets.reduce((total, dataset) => total + dataset.tasks.length, 0) * input.validationPlan.repetitions;
|
|
85
|
+
if (input.validationPlan.repetitions < 1 ||
|
|
86
|
+
input.validationPlan.plannedPrimaryCalls !== plannedPrimaryCalls ||
|
|
87
|
+
input.validationPlan.maximumAttempts <
|
|
88
|
+
input.validationPlan.plannedPrimaryCalls ||
|
|
89
|
+
input.validationPlan.requiredTotalInputTokens < 1 ||
|
|
90
|
+
input.validationPlan.requiredTotalOutputTokens < 1 ||
|
|
91
|
+
(input.validationPlan.maximumPricedExposureUsd === undefined &&
|
|
92
|
+
!input.validationPlan.acknowledgeUnpricedExposure)) {
|
|
93
|
+
throw new Error("validation requires exact calls and approved attempt/token/pricing ceilings");
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return BasisOnboarding.of({
|
|
97
|
+
load,
|
|
98
|
+
initialize: (input) => Effect.gen(function* () {
|
|
99
|
+
const existing = yield* store.loadDocument(input.repositoryRoot);
|
|
100
|
+
if (existing !== undefined) {
|
|
101
|
+
if (existing.version === EVAL_PROJECT_V2_VERSION) {
|
|
102
|
+
if (existing.stage === "basis-generation-ready") {
|
|
103
|
+
yield* artifacts.saveBasisGenerationV3(input.repositoryRoot, {
|
|
104
|
+
version: 1,
|
|
105
|
+
sourceInventory: existing.sourceInventory,
|
|
106
|
+
snapshot: existing.snapshot,
|
|
107
|
+
split: existing.split,
|
|
108
|
+
generationPlan: existing.generationPlan,
|
|
109
|
+
generationLedger: existing.generationLedger
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return existing;
|
|
113
|
+
}
|
|
114
|
+
return yield* transitionError(existing.stage, "legacy V1 projects are not implicitly converted to Classification V3");
|
|
115
|
+
}
|
|
116
|
+
const capturedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
|
|
117
|
+
const captured = yield* captureFrozenRepositorySnapshotV3({
|
|
118
|
+
repositoryRoot: input.repositoryRoot,
|
|
119
|
+
requestedRef: input.requestedRef,
|
|
120
|
+
allowDirty: input.allowDirty,
|
|
121
|
+
capturedAt
|
|
122
|
+
});
|
|
123
|
+
const split = splitBasisTasksChronologicallyV3(input.tasks);
|
|
124
|
+
const paidRecipes = DEFAULT_BASIS_CANDIDATE_RECIPES_V3.filter((recipe) => !recipe.deterministic);
|
|
125
|
+
const maximumAttempts = paidRecipes.length * 2;
|
|
126
|
+
const generationSemantic = {
|
|
127
|
+
version: 1,
|
|
128
|
+
repositoryDigest: captured.snapshot.repositoryDigest,
|
|
129
|
+
splitDigest: split.splitDigest,
|
|
130
|
+
authorModel: input.configuration.authorModel,
|
|
131
|
+
recipes: DEFAULT_BASIS_CANDIDATE_RECIPES_V3,
|
|
132
|
+
plannedPrimaryCalls: paidRecipes.length,
|
|
133
|
+
maximumAttempts,
|
|
134
|
+
requiredTotalInputTokens: maximumAttempts *
|
|
135
|
+
BASIS_AUTHOR_INPUT_TOKENS_PER_ATTEMPT,
|
|
136
|
+
requiredTotalOutputTokens: maximumAttempts *
|
|
137
|
+
BASIS_AUTHOR_OUTPUT_TOKENS_PER_ATTEMPT
|
|
138
|
+
};
|
|
139
|
+
const generationPlan = {
|
|
140
|
+
...generationSemantic,
|
|
141
|
+
planDigest: routingContractDigest("routekit.basis-generation-plan.v1", "plan", generationSemantic)
|
|
142
|
+
};
|
|
143
|
+
const ledgerSemantic = {
|
|
144
|
+
version: 1,
|
|
145
|
+
planDigest: generationPlan.planDigest,
|
|
146
|
+
jobs: paidRecipes.map((recipe) => ({
|
|
147
|
+
recipeId: recipe.id,
|
|
148
|
+
status: "pending",
|
|
149
|
+
attemptCount: 0,
|
|
150
|
+
inputTokens: 0,
|
|
151
|
+
outputTokens: 0,
|
|
152
|
+
callIds: []
|
|
153
|
+
}))
|
|
154
|
+
};
|
|
155
|
+
const state = {
|
|
156
|
+
version: EVAL_PROJECT_V2_VERSION,
|
|
157
|
+
projectId: crypto.randomUUID(),
|
|
158
|
+
revision: 0,
|
|
159
|
+
createdAt: capturedAt,
|
|
160
|
+
updatedAt: capturedAt,
|
|
161
|
+
sourceInventory: captured.sourceInventory,
|
|
162
|
+
configuration: input.configuration,
|
|
163
|
+
stage: "basis-generation-ready",
|
|
164
|
+
snapshot: captured.snapshot,
|
|
165
|
+
split,
|
|
166
|
+
generationPlan,
|
|
167
|
+
generationLedger: {
|
|
168
|
+
...ledgerSemantic,
|
|
169
|
+
ledgerDigest: routingContractDigest("routekit.basis-generation-ledger.v1", "ledger", ledgerSemantic)
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
yield* store.saveDocument(input.repositoryRoot, state);
|
|
173
|
+
yield* artifacts.saveBasisGenerationV3(input.repositoryRoot, {
|
|
174
|
+
version: 1,
|
|
175
|
+
sourceInventory: state.sourceInventory,
|
|
176
|
+
snapshot: state.snapshot,
|
|
177
|
+
split: state.split,
|
|
178
|
+
generationPlan: state.generationPlan,
|
|
179
|
+
generationLedger: state.generationLedger
|
|
180
|
+
});
|
|
181
|
+
return state;
|
|
182
|
+
}),
|
|
183
|
+
recordCandidates: (input) => Effect.gen(function* () {
|
|
184
|
+
const state = yield* store
|
|
185
|
+
.loadDocument(input.repositoryRoot)
|
|
186
|
+
.pipe(Effect.flatMap(requireV2));
|
|
187
|
+
if (state.stage !== "basis-generation-ready") {
|
|
188
|
+
if ("candidates" in state &&
|
|
189
|
+
sameValue(state.candidates, input.candidates) &&
|
|
190
|
+
sameValue(state.generationLedger, input.generationLedger)) {
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
return yield* transitionError(state.stage, "candidate evidence differs from the durable transition");
|
|
194
|
+
}
|
|
195
|
+
const structureReports = yield* transition(state.stage, () => {
|
|
196
|
+
if (input.generationLedger.planDigest !==
|
|
197
|
+
state.generationPlan.planDigest ||
|
|
198
|
+
!verifyDigest("routekit.basis-generation-ledger.v1", "ledger", input.generationLedger, "ledgerDigest", input.generationLedger.ledgerDigest)) {
|
|
199
|
+
throw new Error("generation ledger does not match its immutable plan");
|
|
200
|
+
}
|
|
201
|
+
const paidRecipeIds = state.generationPlan.recipes
|
|
202
|
+
.filter((recipe) => !recipe.deterministic)
|
|
203
|
+
.map((recipe) => recipe.id);
|
|
204
|
+
if (!sameIds(input.generationLedger.jobs.map((job) => job.recipeId), paidRecipeIds) ||
|
|
205
|
+
input.generationLedger.jobs.some((job) => job.status !== "completed" ||
|
|
206
|
+
job.candidateId === undefined ||
|
|
207
|
+
job.attemptCount < 1 ||
|
|
208
|
+
job.attemptCount > 2) ||
|
|
209
|
+
input.generationLedger.jobs.reduce((total, job) => total + job.attemptCount, 0) > state.generationPlan.maximumAttempts ||
|
|
210
|
+
input.generationLedger.jobs.reduce((total, job) => total + job.inputTokens, 0) > state.generationPlan.requiredTotalInputTokens ||
|
|
211
|
+
input.generationLedger.jobs.reduce((total, job) => total + job.outputTokens, 0) > state.generationPlan.requiredTotalOutputTokens) {
|
|
212
|
+
throw new Error("all seven bounded authoring jobs must complete");
|
|
213
|
+
}
|
|
214
|
+
const recipeIds = state.generationPlan.recipes.map((recipe) => recipe.id);
|
|
215
|
+
if (!sameIds(input.candidates.map((candidate) => candidate.recipeId), recipeIds) ||
|
|
216
|
+
new Set(input.candidates.map((candidate) => candidate.candidateId))
|
|
217
|
+
.size !== input.candidates.length) {
|
|
218
|
+
throw new Error("candidate portfolio must contain the incumbent and exact seven challengers");
|
|
219
|
+
}
|
|
220
|
+
const candidateByRecipe = new Map(input.candidates.map((candidate) => [candidate.recipeId, candidate]));
|
|
221
|
+
for (const job of input.generationLedger.jobs) {
|
|
222
|
+
if (candidateByRecipe.get(job.recipeId)?.candidateId !==
|
|
223
|
+
job.candidateId) {
|
|
224
|
+
throw new Error(`generation job ${JSON.stringify(job.recipeId)} does not identify its candidate`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const candidate of input.candidates) {
|
|
228
|
+
if (candidate.basis.repositoryDigest !==
|
|
229
|
+
state.snapshot.repositoryDigest ||
|
|
230
|
+
candidate.basisDigest !== candidate.basis.basisDigest ||
|
|
231
|
+
candidate.candidateId !== candidate.basis.basisId) {
|
|
232
|
+
throw new Error(`candidate ${JSON.stringify(candidate.candidateId)} has stale repository or basis identity`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return inspectBasisCandidatesV3(input.candidates);
|
|
236
|
+
});
|
|
237
|
+
const updatedAt = yield* now;
|
|
238
|
+
const next = {
|
|
239
|
+
...state,
|
|
240
|
+
revision: state.revision + 1,
|
|
241
|
+
updatedAt,
|
|
242
|
+
stage: "basis-candidates-review",
|
|
243
|
+
generationLedger: input.generationLedger,
|
|
244
|
+
candidates: [...input.candidates],
|
|
245
|
+
structureReports: [...structureReports]
|
|
246
|
+
};
|
|
247
|
+
yield* artifacts.saveBasisCandidatesV3(input.repositoryRoot, {
|
|
248
|
+
version: 1,
|
|
249
|
+
generationPlanDigest: state.generationPlan.planDigest,
|
|
250
|
+
generationLedger: input.generationLedger,
|
|
251
|
+
candidates: input.candidates,
|
|
252
|
+
structureReports
|
|
253
|
+
});
|
|
254
|
+
yield* store.saveDocument(input.repositoryRoot, next);
|
|
255
|
+
return next;
|
|
256
|
+
}),
|
|
257
|
+
prepareValidation: (input) => Effect.gen(function* () {
|
|
258
|
+
const state = yield* store
|
|
259
|
+
.loadDocument(input.repositoryRoot)
|
|
260
|
+
.pipe(Effect.flatMap(requireV2));
|
|
261
|
+
if (state.stage !== "basis-candidates-review") {
|
|
262
|
+
if ("validationPlan" in state &&
|
|
263
|
+
sameValue(state.datasets, input.datasets) &&
|
|
264
|
+
sameValue(state.validationPlan, input.validationPlan)) {
|
|
265
|
+
return state;
|
|
266
|
+
}
|
|
267
|
+
return yield* transitionError(state.stage, "validation evidence differs from the durable transition");
|
|
268
|
+
}
|
|
269
|
+
yield* transition(state.stage, () => validatePreparedEvidence({
|
|
270
|
+
state,
|
|
271
|
+
candidates: state.candidates,
|
|
272
|
+
structureReports: state.structureReports,
|
|
273
|
+
datasets: input.datasets,
|
|
274
|
+
validationPlan: input.validationPlan
|
|
275
|
+
}));
|
|
276
|
+
const updatedAt = yield* now;
|
|
277
|
+
const next = {
|
|
278
|
+
...state,
|
|
279
|
+
revision: state.revision + 1,
|
|
280
|
+
updatedAt,
|
|
281
|
+
stage: "basis-validation-ready",
|
|
282
|
+
datasets: [...input.datasets],
|
|
283
|
+
validationPlan: input.validationPlan
|
|
284
|
+
};
|
|
285
|
+
yield* artifacts.saveBasisValidationV3(input.repositoryRoot, {
|
|
286
|
+
version: 1,
|
|
287
|
+
generationPlanDigest: state.generationPlan.planDigest,
|
|
288
|
+
datasets: input.datasets,
|
|
289
|
+
validationPlan: input.validationPlan
|
|
290
|
+
});
|
|
291
|
+
yield* store.saveDocument(input.repositoryRoot, next);
|
|
292
|
+
return next;
|
|
293
|
+
}),
|
|
294
|
+
prepareRepairValidation: (input) => Effect.gen(function* () {
|
|
295
|
+
const state = yield* store
|
|
296
|
+
.loadDocument(input.repositoryRoot)
|
|
297
|
+
.pipe(Effect.flatMap(requireV2));
|
|
298
|
+
if (state.stage !== "basis-validation-review") {
|
|
299
|
+
return yield* transitionError(state.stage, "repair validation requires an unapproved validation review");
|
|
300
|
+
}
|
|
301
|
+
const source = state.candidates.find((candidate) => candidate.candidateId === state.selection.selectedCandidateId);
|
|
302
|
+
if (source === undefined ||
|
|
303
|
+
input.repairCandidate.basis.provenance.origin?.kind !== "repair" ||
|
|
304
|
+
input.repairCandidate.basis.provenance.origin.sourceBasisId !==
|
|
305
|
+
source.basis.basisId ||
|
|
306
|
+
state.candidates.some((candidate) => candidate.candidateId === input.repairCandidate.candidateId)) {
|
|
307
|
+
return yield* transitionError(state.stage, "repair candidate does not derive from the selected original");
|
|
308
|
+
}
|
|
309
|
+
const candidates = [...state.candidates, input.repairCandidate];
|
|
310
|
+
const repairStructure = inspectBasisCandidatesV3([
|
|
311
|
+
input.repairCandidate
|
|
312
|
+
])[0];
|
|
313
|
+
if (!repairStructure.eligible) {
|
|
314
|
+
return yield* transitionError(state.stage, "repair candidate must pass the same structural and projection gates before validation");
|
|
315
|
+
}
|
|
316
|
+
const structureReports = [
|
|
317
|
+
...state.structureReports,
|
|
318
|
+
repairStructure
|
|
319
|
+
];
|
|
320
|
+
yield* transition(state.stage, () => validatePreparedEvidence({
|
|
321
|
+
state,
|
|
322
|
+
candidates,
|
|
323
|
+
structureReports,
|
|
324
|
+
datasets: input.datasets,
|
|
325
|
+
validationPlan: input.validationPlan
|
|
326
|
+
}));
|
|
327
|
+
const updatedAt = yield* now;
|
|
328
|
+
const next = {
|
|
329
|
+
version: state.version,
|
|
330
|
+
projectId: state.projectId,
|
|
331
|
+
revision: state.revision + 1,
|
|
332
|
+
createdAt: state.createdAt,
|
|
333
|
+
updatedAt,
|
|
334
|
+
sourceInventory: state.sourceInventory,
|
|
335
|
+
configuration: state.configuration,
|
|
336
|
+
stage: "basis-validation-ready",
|
|
337
|
+
snapshot: state.snapshot,
|
|
338
|
+
split: state.split,
|
|
339
|
+
generationPlan: state.generationPlan,
|
|
340
|
+
generationLedger: state.generationLedger,
|
|
341
|
+
candidates,
|
|
342
|
+
structureReports,
|
|
343
|
+
datasets: [...input.datasets],
|
|
344
|
+
validationPlan: input.validationPlan
|
|
345
|
+
};
|
|
346
|
+
yield* artifacts.saveBasisRepairValidationV3(input.repositoryRoot, {
|
|
347
|
+
version: 1,
|
|
348
|
+
generationPlanDigest: state.generationPlan.planDigest,
|
|
349
|
+
datasets: input.datasets,
|
|
350
|
+
validationPlan: input.validationPlan
|
|
351
|
+
});
|
|
352
|
+
yield* store.saveDocument(input.repositoryRoot, next);
|
|
353
|
+
return next;
|
|
354
|
+
}),
|
|
355
|
+
recordValidation: (input) => Effect.gen(function* () {
|
|
356
|
+
const state = yield* store
|
|
357
|
+
.loadDocument(input.repositoryRoot)
|
|
358
|
+
.pipe(Effect.flatMap(requireV2));
|
|
359
|
+
if (state.stage !== "basis-validation-ready") {
|
|
360
|
+
if ((state.stage === "basis-validation-review" ||
|
|
361
|
+
state.stage === "basis-qualified") &&
|
|
362
|
+
sameValidationResults(state.validationResults, input.validationResults)) {
|
|
363
|
+
yield* artifacts.saveBasisValidationResultsV3(input.repositoryRoot, {
|
|
364
|
+
version: 1,
|
|
365
|
+
validationPlanDigest: state.validationPlan.planDigest,
|
|
366
|
+
validationResults: state.validationResults,
|
|
367
|
+
selection: state.selection
|
|
368
|
+
});
|
|
369
|
+
return state;
|
|
370
|
+
}
|
|
371
|
+
return yield* transitionError(state.stage, "validation evidence differs from the durable transition");
|
|
372
|
+
}
|
|
373
|
+
const selection = yield* transition(state.stage, () => {
|
|
374
|
+
if (!sameIds(input.validationResults.map((result) => result.candidateId), state.validationPlan.candidateIds) ||
|
|
375
|
+
input.validationResults.some((result, index) => result.validationDatasetDigest !==
|
|
376
|
+
state.datasets[index]?.datasetDigest ||
|
|
377
|
+
result.classifierConfigurationDigest !==
|
|
378
|
+
state.validationPlan.classifierBindings[index]
|
|
379
|
+
?.classifierConfigurationDigest)) {
|
|
380
|
+
throw new Error("validation results must exactly cover the immutable validation plan");
|
|
381
|
+
}
|
|
382
|
+
return selectValidatedBasisCandidatesV3({
|
|
383
|
+
repositoryDigest: state.snapshot.repositoryDigest,
|
|
384
|
+
validationPlan: state.validationPlan,
|
|
385
|
+
candidates: state.candidates,
|
|
386
|
+
structureReports: state.structureReports,
|
|
387
|
+
validationResults: input.validationResults
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
const updatedAt = yield* now;
|
|
391
|
+
const next = {
|
|
392
|
+
...state,
|
|
393
|
+
revision: state.revision + 1,
|
|
394
|
+
updatedAt,
|
|
395
|
+
stage: "basis-validation-review",
|
|
396
|
+
validationResults: [...input.validationResults],
|
|
397
|
+
selection
|
|
398
|
+
};
|
|
399
|
+
yield* store.saveDocument(input.repositoryRoot, next);
|
|
400
|
+
const saveResults = state.candidates.some((candidate) => candidate.basis.provenance.origin?.kind === "repair")
|
|
401
|
+
? artifacts.saveBasisRepairValidationResultsV3
|
|
402
|
+
: artifacts.saveBasisValidationResultsV3;
|
|
403
|
+
yield* saveResults(input.repositoryRoot, {
|
|
404
|
+
version: 1,
|
|
405
|
+
validationPlanDigest: state.validationPlan.planDigest,
|
|
406
|
+
validationResults: input.validationResults,
|
|
407
|
+
selection
|
|
408
|
+
});
|
|
409
|
+
return next;
|
|
410
|
+
}),
|
|
411
|
+
qualify: (input) => Effect.gen(function* () {
|
|
412
|
+
const state = yield* store
|
|
413
|
+
.loadDocument(input.repositoryRoot)
|
|
414
|
+
.pipe(Effect.flatMap(requireV2));
|
|
415
|
+
if (state.stage !== "basis-validation-review") {
|
|
416
|
+
const sameCandidate = state.stage === "basis-qualified" &&
|
|
417
|
+
state.approval.candidateId === input.candidateId &&
|
|
418
|
+
state.approval.basisDigest === input.basisDigest;
|
|
419
|
+
const sameApprover = state.stage === "basis-qualified" &&
|
|
420
|
+
state.approval.approvedBy === input.approvedBy;
|
|
421
|
+
const sameAcknowledgement = state.stage === "basis-qualified" &&
|
|
422
|
+
state.approval.provisionalAcknowledgement ===
|
|
423
|
+
input.provisionalAcknowledgement;
|
|
424
|
+
if (state.stage === "basis-qualified" &&
|
|
425
|
+
sameCandidate &&
|
|
426
|
+
sameApprover &&
|
|
427
|
+
sameAcknowledgement) {
|
|
428
|
+
yield* artifacts.saveBasisQualificationV3(input.repositoryRoot, {
|
|
429
|
+
version: 1,
|
|
430
|
+
validationPlanDigest: state.validationPlan.planDigest,
|
|
431
|
+
validationResults: state.validationResults,
|
|
432
|
+
selection: state.selection,
|
|
433
|
+
approval: state.approval,
|
|
434
|
+
approvalDigest: routingBasisApprovalDigestV3(state.approval)
|
|
435
|
+
});
|
|
436
|
+
return state;
|
|
437
|
+
}
|
|
438
|
+
return yield* transitionError(state.stage, `approval differs from the durable transition (candidate=${String(sameCandidate)}, approver=${String(sameApprover)}, acknowledgement=${String(sameAcknowledgement)})`);
|
|
439
|
+
}
|
|
440
|
+
const selectedCandidate = state.candidates.find((candidate) => candidate.candidateId === state.selection.selectedCandidateId);
|
|
441
|
+
if (selectedCandidate === undefined ||
|
|
442
|
+
input.candidateId !== state.selection.selectedCandidateId ||
|
|
443
|
+
input.basisDigest !== selectedCandidate.basisDigest) {
|
|
444
|
+
return yield* transitionError(state.stage, "approval must pin the exact selected candidate id and basis digest");
|
|
445
|
+
}
|
|
446
|
+
const approvedAt = yield* now;
|
|
447
|
+
const qualified = yield* transition(state.stage, () => {
|
|
448
|
+
const structure = state.structureReports.find((report) => report.candidateId === state.selection.selectedCandidateId);
|
|
449
|
+
const validation = state.validationResults.find((result) => result.candidateId === state.selection.selectedCandidateId);
|
|
450
|
+
if (structure === undefined ||
|
|
451
|
+
validation === undefined) {
|
|
452
|
+
throw new Error("selected basis evidence is incomplete");
|
|
453
|
+
}
|
|
454
|
+
return approveSelectedBasisV3({
|
|
455
|
+
projectId: state.projectId,
|
|
456
|
+
approvedBy: input.approvedBy,
|
|
457
|
+
approvedAt,
|
|
458
|
+
snapshot: state.snapshot,
|
|
459
|
+
candidate: selectedCandidate,
|
|
460
|
+
structure,
|
|
461
|
+
validation,
|
|
462
|
+
selection: state.selection,
|
|
463
|
+
...(input.provisionalAcknowledgement === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: {
|
|
466
|
+
provisionalAcknowledgement: input.provisionalAcknowledgement
|
|
467
|
+
})
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
const next = {
|
|
471
|
+
...state,
|
|
472
|
+
revision: state.revision + 1,
|
|
473
|
+
updatedAt: qualified.approval.approvedAt,
|
|
474
|
+
stage: "basis-qualified",
|
|
475
|
+
approval: qualified.approval
|
|
476
|
+
};
|
|
477
|
+
yield* store.saveDocument(input.repositoryRoot, next);
|
|
478
|
+
yield* artifacts.saveBasisQualificationV3(input.repositoryRoot, {
|
|
479
|
+
version: 1,
|
|
480
|
+
validationPlanDigest: state.validationPlan.planDigest,
|
|
481
|
+
validationResults: state.validationResults,
|
|
482
|
+
selection: state.selection,
|
|
483
|
+
approval: qualified.approval,
|
|
484
|
+
approvalDigest: qualified.approvalDigest
|
|
485
|
+
});
|
|
486
|
+
yield* artifacts.saveBasisEvaluationProjectionV3(input.repositoryRoot, projectRoutingBasisV3(selectedCandidate.basis));
|
|
487
|
+
return next;
|
|
488
|
+
})
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
export const BasisOnboardingLive = Layer.effect(BasisOnboarding, makeBasisOnboarding);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { EvalProjectAuthoringError, EvalProjectTransitionError } from "../../errors.js";
|
|
3
|
+
import { EvalProjectArtifacts } from "../../project-artifacts.js";
|
|
4
|
+
import { EvalAuthoringTransport } from "../../project-authoring.js";
|
|
5
|
+
import type { BasisRepairArtifactsV3, BasisRepairPlanV3, BasisRepairSummaryV3 } from "../../project-contracts.js";
|
|
6
|
+
import { EvalProjectStore } from "../../project-store.js";
|
|
7
|
+
export type BasisRepairShape = {
|
|
8
|
+
readonly plan: (input: {
|
|
9
|
+
readonly repositoryRoot: string;
|
|
10
|
+
readonly sourceCandidateId: string;
|
|
11
|
+
readonly authorModel: string;
|
|
12
|
+
readonly summary: BasisRepairSummaryV3;
|
|
13
|
+
}) => Effect.Effect<BasisRepairPlanV3, EvalProjectTransitionError | import("../../errors.js").EvalProjectStoreError>;
|
|
14
|
+
readonly execute: (input: {
|
|
15
|
+
readonly repositoryRoot: string;
|
|
16
|
+
readonly plan: BasisRepairPlanV3;
|
|
17
|
+
readonly summary: BasisRepairSummaryV3;
|
|
18
|
+
}) => Effect.Effect<BasisRepairArtifactsV3, EvalProjectAuthoringError | EvalProjectTransitionError | import("../../errors.js").EvalBasisSnapshotError | import("../../errors.js").EvalProjectArtifactError | import("../../errors.js").EvalProjectStoreError, EvalAuthoringTransport>;
|
|
19
|
+
};
|
|
20
|
+
declare const BasisRepair_base: Context.ServiceClass<BasisRepair, "@velum-labs/routekit-eval-setup/BasisRepair", BasisRepairShape>;
|
|
21
|
+
export declare class BasisRepair extends BasisRepair_base {
|
|
22
|
+
}
|
|
23
|
+
export declare const makeBasisRepair: Effect.Effect<BasisRepairShape, never, EvalProjectArtifacts | EvalProjectStore>;
|
|
24
|
+
export declare const BasisRepairLive: Layer.Layer<BasisRepair, never, EvalProjectArtifacts | EvalProjectStore>;
|
|
25
|
+
export {};
|