@evo-dev/core 0.0.1-alpha.11 → 0.0.1-alpha.12

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.
@@ -0,0 +1,435 @@
1
+ import { createStableId } from "../../utils/index.ts";
2
+ import {
3
+ readSessionEvidenceSegment,
4
+ resolveSessionMemoryPaths,
5
+ writeHistoricalSessionEvidenceSegment,
6
+ writeHistoricalSessionStateAndIndex,
7
+ } from "../evidence/session-memory/index.ts";
8
+ import type { SegmentEvolutionTriggerRecord } from "../schema.ts";
9
+ import {
10
+ enqueueSegmentEvolutionTrigger,
11
+ listSegmentEvolutionTriggers,
12
+ resolveSegmentEvolutionTriggerPath,
13
+ } from "../triggers/index.ts";
14
+ import {
15
+ type TrajectoryImportMaterializationPlan,
16
+ createHistoricalSessionEvidenceSegments,
17
+ } from "./materialize.ts";
18
+ import { resolveTrajectoryImportPaths, resolveTrajectoryImportSourcePaths } from "./paths.ts";
19
+ import { stageTrajectoryImportWithLock } from "./stage.ts";
20
+ import {
21
+ assertTrajectoryImportSourceLockOwned,
22
+ readTrajectoryImportMaterializationProgress,
23
+ readTrajectoryImportReceipt,
24
+ withTrajectoryImportSourceLock,
25
+ writeImmutableTrajectoryImportReceipt,
26
+ writeTrajectoryImportManifest,
27
+ writeTrajectoryImportMaterializationProgress,
28
+ } from "./storage.ts";
29
+ import type {
30
+ TrajectoryImportManifestV1,
31
+ TrajectoryImportMaterializationProgressV1,
32
+ TrajectoryImportReceiptV1,
33
+ } from "./types.ts";
34
+
35
+ export interface ApplyTrajectoryImportInput {
36
+ homeDir: string;
37
+ plan: TrajectoryImportMaterializationPlan;
38
+ expectedPreviewId: string;
39
+ now?: string;
40
+ }
41
+
42
+ export interface ApplyTrajectoryImportResult {
43
+ status: "applied" | "unchanged";
44
+ previewId: string;
45
+ generationId: string;
46
+ snapshotId: string;
47
+ segmentIds: string[];
48
+ triggerIds: string[];
49
+ materializationReceipt: TrajectoryImportReceiptV1;
50
+ stateWrites: string[];
51
+ }
52
+
53
+ export async function applyTrajectoryImport(
54
+ input: ApplyTrajectoryImportInput,
55
+ ): Promise<ApplyTrajectoryImportResult> {
56
+ validateApplyInput(input);
57
+ const projectKey = input.plan.preview.projectKey;
58
+ if (projectKey === null) {
59
+ throw new Error("Trajectory import apply requires a projectKey.");
60
+ }
61
+ const sourcePaths = resolveTrajectoryImportSourcePaths({
62
+ homeDir: input.homeDir,
63
+ projectKey,
64
+ sourceKey: input.plan.preview.sourceKey,
65
+ });
66
+
67
+ return withTrajectoryImportSourceLock(sourcePaths, async (lock) => {
68
+ const staged = await stageTrajectoryImportWithLock(
69
+ {
70
+ homeDir: input.homeDir,
71
+ preview: input.plan.preview,
72
+ comparisonSnapshot: input.plan.comparisonSnapshot,
73
+ now: input.now,
74
+ },
75
+ lock,
76
+ );
77
+ await assertTrajectoryImportSourceLockOwned(lock);
78
+
79
+ if (
80
+ staged.manifest.applyState === "completed" &&
81
+ staged.manifest.latestMaterializedSnapshotId === staged.manifest.currentSnapshotId
82
+ ) {
83
+ return readCompletedApplyResult(input, staged.manifest);
84
+ }
85
+ if (
86
+ staged.manifest.currentPreviewId !== input.plan.preview.id ||
87
+ staged.manifest.currentSnapshotId !== input.plan.preview.snapshotId
88
+ ) {
89
+ throw new Error("Trajectory import staged state does not match the apply preview.");
90
+ }
91
+
92
+ const paths = resolveTrajectoryImportPaths({
93
+ homeDir: input.homeDir,
94
+ projectKey,
95
+ sourceKey: input.plan.preview.sourceKey,
96
+ generationId: input.plan.preview.generationId,
97
+ snapshotId: input.plan.preview.snapshotId,
98
+ previewId: input.plan.preview.id,
99
+ });
100
+ const materializingManifest: TrajectoryImportManifestV1 = {
101
+ ...staged.manifest,
102
+ applyState: "materializing",
103
+ };
104
+ await writeTrajectoryImportManifest(sourcePaths, materializingManifest);
105
+ await assertTrajectoryImportSourceLockOwned(lock);
106
+
107
+ const segments = createHistoricalSessionEvidenceSegments({
108
+ plan: input.plan,
109
+ createdAt: staged.receipt.createdAt,
110
+ });
111
+ const sessionKey = `historical-${input.plan.preview.sourceKey}`;
112
+ const sessionPaths = resolveSessionMemoryPaths({
113
+ homeDir: input.homeDir,
114
+ projectKey,
115
+ sessionKey,
116
+ });
117
+ const expectedTriggerIds = segments.map((segment) =>
118
+ createStableId("segment-trigger", [projectKey, sessionKey, segment.id]),
119
+ );
120
+ let progress = await resolveMaterializationProgress({
121
+ path: paths.materializationPath,
122
+ previewId: input.plan.preview.id,
123
+ sourceKey: input.plan.preview.sourceKey,
124
+ generationId: input.plan.preview.generationId,
125
+ snapshotId: input.plan.preview.snapshotId,
126
+ expectedSegmentIds: segments.map((segment) => segment.id),
127
+ expectedTriggerIds,
128
+ updatedAt: staged.receipt.createdAt,
129
+ });
130
+ await writeTrajectoryImportMaterializationProgress(paths, progress);
131
+ const stateWrites = [
132
+ ...staged.stateWrites,
133
+ `materializations/${input.plan.preview.snapshotId}.json`,
134
+ ];
135
+
136
+ for (const segment of segments) {
137
+ const written = await writeHistoricalSessionEvidenceSegment({
138
+ homeDir: input.homeDir,
139
+ paths: sessionPaths,
140
+ segment,
141
+ });
142
+ if (written.created) stateWrites.push(sessionPaths.segmentPath(segment.id));
143
+ progress = addCompletedProgress(progress, "segment", segment.id);
144
+ await writeTrajectoryImportMaterializationProgress(paths, progress);
145
+ await assertTrajectoryImportSourceLockOwned(lock);
146
+ }
147
+
148
+ if (segments.length > 0) {
149
+ await writeHistoricalSessionStateAndIndex({
150
+ homeDir: input.homeDir,
151
+ paths: sessionPaths,
152
+ projectKey,
153
+ sessionKey,
154
+ target: segments[0]?.target ?? "unknown",
155
+ roleId: input.plan.preview.scope.roleId,
156
+ policy: input.plan.policy,
157
+ now: staged.receipt.createdAt,
158
+ });
159
+ stateWrites.push(sessionPaths.statePath, sessionPaths.indexPath);
160
+ }
161
+
162
+ const triggerIds: string[] = [];
163
+ for (const [index, segment] of segments.entries()) {
164
+ const trigger = await enqueueSegmentEvolutionTrigger({
165
+ homeDir: input.homeDir,
166
+ projectKey,
167
+ sessionKey,
168
+ runId: null,
169
+ roleId: input.plan.preview.scope.roleId,
170
+ segmentId: segment.id,
171
+ segmentPath: sessionPaths.segmentPath(segment.id),
172
+ strength: "normal",
173
+ reason: "historical-import",
174
+ summary: segment.normalized.summary,
175
+ now: staged.receipt.createdAt,
176
+ });
177
+ assertHistoricalTriggerCompatible(trigger, {
178
+ segmentId: segment.id,
179
+ expectedId: expectedTriggerIds[index] ?? "",
180
+ segmentPath: sessionPaths.segmentPath(segment.id),
181
+ summary: segment.normalized.summary,
182
+ });
183
+ triggerIds.push(trigger.id);
184
+ progress = addCompletedProgress(progress, "trigger", trigger.id);
185
+ await writeTrajectoryImportMaterializationProgress(paths, progress);
186
+ await assertTrajectoryImportSourceLockOwned(lock);
187
+ stateWrites.push(resolveSegmentEvolutionTriggerPath(input.homeDir, trigger));
188
+ }
189
+
190
+ await verifyMaterializedSegments({
191
+ homeDir: input.homeDir,
192
+ projectKey,
193
+ sessionKey,
194
+ segmentIds: progress.expectedSegmentIds,
195
+ });
196
+ if (
197
+ progress.completedSegmentIds.length !== progress.expectedSegmentIds.length ||
198
+ progress.completedTriggerIds.length !== progress.expectedTriggerIds.length
199
+ ) {
200
+ throw new Error("Trajectory import materialization did not complete all expected writes.");
201
+ }
202
+
203
+ const materializationReceipt: TrajectoryImportReceiptV1 = {
204
+ schemaVersion: 1,
205
+ kind: "trajectory-import-receipt",
206
+ previewId: input.plan.preview.id,
207
+ sourceKey: input.plan.preview.sourceKey,
208
+ generationId: input.plan.preview.generationId,
209
+ snapshotId: input.plan.preview.snapshotId,
210
+ operation: "materialize",
211
+ outcome: "completed",
212
+ counts: {
213
+ total: input.plan.preview.records.total,
214
+ new: input.plan.preview.records.new,
215
+ unchanged: input.plan.preview.records.unchanged,
216
+ changed: input.plan.preview.records.changed,
217
+ missing: input.plan.preview.records.missing,
218
+ },
219
+ createdAt: staged.receipt.createdAt,
220
+ rawContentStored: false,
221
+ };
222
+ const receiptWrite = await writeImmutableTrajectoryImportReceipt(
223
+ paths.materializeReceiptPath,
224
+ materializationReceipt,
225
+ );
226
+ if (receiptWrite.created) {
227
+ stateWrites.push(`receipts/${input.plan.preview.id}/materialize.json`);
228
+ }
229
+ await assertTrajectoryImportSourceLockOwned(lock);
230
+
231
+ const completedManifest: TrajectoryImportManifestV1 = {
232
+ ...materializingManifest,
233
+ applyState: "completed",
234
+ latestMaterializedSnapshotId: input.plan.preview.snapshotId,
235
+ };
236
+ await writeTrajectoryImportManifest(sourcePaths, completedManifest);
237
+ await assertTrajectoryImportSourceLockOwned(lock);
238
+ stateWrites.push("manifest.json");
239
+
240
+ return {
241
+ status: "applied",
242
+ previewId: input.plan.preview.id,
243
+ generationId: input.plan.preview.generationId,
244
+ snapshotId: input.plan.preview.snapshotId,
245
+ segmentIds: segments.map((segment) => segment.id),
246
+ triggerIds,
247
+ materializationReceipt,
248
+ stateWrites: [...new Set(stateWrites)],
249
+ };
250
+ });
251
+ }
252
+
253
+ async function readCompletedApplyResult(
254
+ input: ApplyTrajectoryImportInput,
255
+ manifest: TrajectoryImportManifestV1,
256
+ ): Promise<ApplyTrajectoryImportResult> {
257
+ const projectKey = input.plan.preview.projectKey;
258
+ if (projectKey === null) throw new Error("Trajectory import apply requires a projectKey.");
259
+ const paths = resolveTrajectoryImportPaths({
260
+ homeDir: input.homeDir,
261
+ projectKey,
262
+ sourceKey: manifest.sourceKey,
263
+ generationId: manifest.currentGenerationId,
264
+ snapshotId: manifest.currentSnapshotId,
265
+ previewId: manifest.currentPreviewId,
266
+ });
267
+ const receipt = await readTrajectoryImportReceipt(paths.materializeReceiptPath);
268
+ if (
269
+ receipt === null ||
270
+ receipt.operation !== "materialize" ||
271
+ receipt.outcome !== "completed" ||
272
+ receipt.snapshotId !== manifest.currentSnapshotId ||
273
+ receipt.previewId !== manifest.currentPreviewId
274
+ ) {
275
+ throw new Error("Completed trajectory import is missing its materialization receipt.");
276
+ }
277
+ const progress = await readTrajectoryImportMaterializationProgress(paths.materializationPath);
278
+ if (progress === null) {
279
+ throw new Error("Completed trajectory import is missing materialization progress.");
280
+ }
281
+ if (
282
+ progress.completedSegmentIds.length !== progress.expectedSegmentIds.length ||
283
+ progress.completedTriggerIds.length !== progress.expectedTriggerIds.length
284
+ ) {
285
+ throw new Error("Completed trajectory import has incomplete materialization progress.");
286
+ }
287
+ const sessionKey = `historical-${manifest.sourceKey}`;
288
+ await verifyMaterializedSegments({
289
+ homeDir: input.homeDir,
290
+ projectKey,
291
+ sessionKey,
292
+ segmentIds: progress.expectedSegmentIds,
293
+ });
294
+ const triggerIds = new Set(
295
+ (
296
+ await listSegmentEvolutionTriggers({
297
+ homeDir: input.homeDir,
298
+ projectKey,
299
+ })
300
+ )
301
+ .filter(
302
+ (trigger) =>
303
+ trigger.sessionKey === sessionKey &&
304
+ progress.expectedSegmentIds.includes(trigger.segmentId),
305
+ )
306
+ .map((trigger) => trigger.id),
307
+ );
308
+ if (progress.expectedTriggerIds.some((id) => !triggerIds.has(id))) {
309
+ throw new Error("Completed trajectory import is missing a materialized trigger.");
310
+ }
311
+ return {
312
+ status: "unchanged",
313
+ previewId: input.plan.preview.id,
314
+ generationId: manifest.currentGenerationId,
315
+ snapshotId: manifest.currentSnapshotId,
316
+ segmentIds: progress.expectedSegmentIds,
317
+ triggerIds: progress.expectedTriggerIds,
318
+ materializationReceipt: receipt,
319
+ stateWrites: [],
320
+ };
321
+ }
322
+
323
+ async function resolveMaterializationProgress(input: {
324
+ path: string;
325
+ previewId: string;
326
+ sourceKey: string;
327
+ generationId: string;
328
+ snapshotId: string;
329
+ expectedSegmentIds: string[];
330
+ expectedTriggerIds: string[];
331
+ updatedAt: string;
332
+ }): Promise<TrajectoryImportMaterializationProgressV1> {
333
+ const existing = await readTrajectoryImportMaterializationProgress(input.path);
334
+ if (existing === null) {
335
+ return {
336
+ schemaVersion: 1,
337
+ kind: "trajectory-import-materialization-progress",
338
+ previewId: input.previewId,
339
+ sourceKey: input.sourceKey,
340
+ generationId: input.generationId,
341
+ snapshotId: input.snapshotId,
342
+ expectedSegmentIds: input.expectedSegmentIds,
343
+ completedSegmentIds: [],
344
+ expectedTriggerIds: input.expectedTriggerIds,
345
+ completedTriggerIds: [],
346
+ updatedAt: input.updatedAt,
347
+ rawContentStored: false,
348
+ };
349
+ }
350
+ if (
351
+ existing.previewId !== input.previewId ||
352
+ existing.sourceKey !== input.sourceKey ||
353
+ existing.generationId !== input.generationId ||
354
+ existing.snapshotId !== input.snapshotId ||
355
+ JSON.stringify(existing.expectedSegmentIds) !== JSON.stringify(input.expectedSegmentIds) ||
356
+ JSON.stringify(existing.expectedTriggerIds) !== JSON.stringify(input.expectedTriggerIds)
357
+ ) {
358
+ throw new Error("Trajectory import materialization progress conflicts with its preview.");
359
+ }
360
+ return existing;
361
+ }
362
+
363
+ function addCompletedProgress(
364
+ progress: TrajectoryImportMaterializationProgressV1,
365
+ kind: "segment" | "trigger",
366
+ id: string,
367
+ ): TrajectoryImportMaterializationProgressV1 {
368
+ return {
369
+ ...progress,
370
+ completedSegmentIds:
371
+ kind === "segment"
372
+ ? [...new Set([...progress.completedSegmentIds, id])]
373
+ : progress.completedSegmentIds,
374
+ completedTriggerIds:
375
+ kind === "trigger"
376
+ ? [...new Set([...progress.completedTriggerIds, id])]
377
+ : progress.completedTriggerIds,
378
+ };
379
+ }
380
+
381
+ async function verifyMaterializedSegments(input: {
382
+ homeDir: string;
383
+ projectKey: string;
384
+ sessionKey: string;
385
+ segmentIds: string[];
386
+ }): Promise<void> {
387
+ for (const segmentId of input.segmentIds) {
388
+ const segment = await readSessionEvidenceSegment({ ...input, segmentId });
389
+ if (segment.origin?.kind !== "historical-import") {
390
+ throw new Error("Trajectory import materialized segment origin is invalid.");
391
+ }
392
+ }
393
+ }
394
+
395
+ function assertHistoricalTriggerCompatible(
396
+ trigger: SegmentEvolutionTriggerRecord,
397
+ expected: {
398
+ segmentId: string;
399
+ expectedId: string;
400
+ segmentPath: string;
401
+ summary: string;
402
+ },
403
+ ): void {
404
+ if (
405
+ trigger.id !== expected.expectedId ||
406
+ trigger.segmentId !== expected.segmentId ||
407
+ trigger.segmentPath !== expected.segmentPath ||
408
+ trigger.reason !== "historical-import" ||
409
+ trigger.strength !== "normal" ||
410
+ trigger.summary !== expected.summary
411
+ ) {
412
+ throw new Error("Historical segment trigger immutable content conflict.");
413
+ }
414
+ }
415
+
416
+ function validateApplyInput(input: ApplyTrajectoryImportInput): void {
417
+ if (input.expectedPreviewId !== input.plan.preview.id) {
418
+ throw new Error("Trajectory import preview changed; run --dry-run again.");
419
+ }
420
+ if (!input.plan.preview.canApply || input.plan.preview.blockers.length > 0) {
421
+ throw new Error(
422
+ `Trajectory import cannot apply${
423
+ input.plan.preview.blockers.length === 0
424
+ ? "."
425
+ : `: ${input.plan.preview.blockers.join(", ")}.`
426
+ }`,
427
+ );
428
+ }
429
+ if (
430
+ input.now !== undefined &&
431
+ (!Number.isFinite(Date.parse(input.now)) || new Date(input.now).toISOString() !== input.now)
432
+ ) {
433
+ throw new Error("Trajectory import apply now must be an ISO timestamp.");
434
+ }
435
+ }