@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.
Files changed (38) hide show
  1. package/dist/adapters/git-basis-snapshot.d.ts +27 -0
  2. package/dist/adapters/git-basis-snapshot.js +178 -0
  3. package/dist/basis-candidates.d.ts +72 -0
  4. package/dist/basis-candidates.js +157 -0
  5. package/dist/basis-projection.d.ts +9 -0
  6. package/dist/basis-projection.js +30 -0
  7. package/dist/basis-validation.d.ts +38 -0
  8. package/dist/basis-validation.js +187 -0
  9. package/dist/effect-api.d.ts +8 -0
  10. package/dist/effect-api.js +5 -0
  11. package/dist/errors.d.ts +11 -1
  12. package/dist/errors.js +5 -0
  13. package/dist/index.d.ts +15 -3
  14. package/dist/index.js +10 -2
  15. package/dist/project-artifacts.d.ts +26 -1
  16. package/dist/project-artifacts.js +52 -1
  17. package/dist/project-contracts.d.ts +3594 -0
  18. package/dist/project-contracts.js +344 -1
  19. package/dist/project-store.d.ts +3 -1
  20. package/dist/project-store.js +39 -33
  21. package/dist/project-workflow.d.ts +2 -2
  22. package/dist/services/basis-authoring/service.d.ts +253 -0
  23. package/dist/services/basis-authoring/service.js +547 -0
  24. package/dist/services/basis-onboarding/service.d.ts +48 -0
  25. package/dist/services/basis-onboarding/service.js +491 -0
  26. package/dist/services/basis-repair/service.d.ts +25 -0
  27. package/dist/services/basis-repair/service.js +249 -0
  28. package/dist/test/basis-authoring.test.d.ts +1 -0
  29. package/dist/test/basis-authoring.test.js +194 -0
  30. package/dist/test/basis-onboarding.test.d.ts +1 -0
  31. package/dist/test/basis-onboarding.test.js +385 -0
  32. package/dist/test/basis-repair.test.d.ts +1 -0
  33. package/dist/test/basis-repair.test.js +338 -0
  34. package/dist/test/basis-v3.test.d.ts +1 -0
  35. package/dist/test/basis-v3.test.js +262 -0
  36. package/dist/test/project-v2.test.d.ts +1 -0
  37. package/dist/test/project-v2.test.js +99 -0
  38. 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 {};