| author | |
| committer | |
| log | 7118e43d16b157dd3079f354bd66ca5d772a9a93 |
| tree | 7c8a186196469b91ddbcbbdf8871bff4fda077ec |
| parent | 4e8ee13d8f54e9aa84f1b15f9d802c30650217ea |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>19 files changed, 1102 insertions(+), 1141 deletions(-)
apps/server/integration/OrchestrationEngineHarness.integration.ts+4-5| ... | @@ -22,8 +22,7 @@ import * as Schema from "effect/Schema"; | ... | @@ -22,8 +22,7 @@ import * as Schema from "effect/Schema"; |
| 22 | import * as Scope from "effect/Scope"; | 22 | import * as Scope from "effect/Scope"; |
| 23 | import * as Stream from "effect/Stream"; | 23 | import * as Stream from "effect/Stream"; |
| 24 | 24 | ||
| 25 | import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts"; | 25 | import * as CheckpointStore from "../src/checkpointing/CheckpointStore.ts"; |
| 26 | import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts"; | ||
| 27 | import { TextGeneration, type TextGenerationShape } from "../src/textGeneration/TextGeneration.ts"; | 26 | import { TextGeneration, type TextGenerationShape } from "../src/textGeneration/TextGeneration.ts"; |
| 28 | import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; | 27 | import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/Layers/OrchestrationCommandReceipts.ts"; |
| 29 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; | 28 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; |
| ... | @@ -180,7 +179,7 @@ export interface OrchestrationIntegrationHarness { | ... | @@ -180,7 +179,7 @@ export interface OrchestrationIntegrationHarness { |
| 180 | readonly engine: OrchestrationEngineShape; | 179 | readonly engine: OrchestrationEngineShape; |
| 181 | readonly snapshotQuery: ProjectionSnapshotQuery["Service"]; | 180 | readonly snapshotQuery: ProjectionSnapshotQuery["Service"]; |
| 182 | readonly providerService: ProviderService["Service"]; | 181 | readonly providerService: ProviderService["Service"]; |
| 183 | readonly checkpointStore: CheckpointStore["Service"]; | 182 | readonly checkpointStore: CheckpointStore.CheckpointStore["Service"]; |
| 184 | readonly checkpointRepository: ProjectionCheckpointRepository["Service"]; | 183 | readonly checkpointRepository: ProjectionCheckpointRepository["Service"]; |
| 185 | readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"]; | 184 | readonly pendingApprovalRepository: ProjectionPendingApprovalRepository["Service"]; |
| 186 | readonly waitForThread: ( | 185 | readonly waitForThread: ( |
| ... | @@ -296,7 +295,7 @@ export const makeOrchestrationIntegrationHarness = ( | ... | @@ -296,7 +295,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 296 | ); | 295 | ); |
| 297 | const providerRegistryLayer = makeProviderRegistryLayer(); | 296 | const providerRegistryLayer = makeProviderRegistryLayer(); |
| 298 | 297 | ||
| 299 | const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistry.layer)); | 298 | const checkpointStoreLayer = CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer)); |
| 300 | const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive; | 299 | const projectionSnapshotQueryLayer = OrchestrationProjectionSnapshotQueryLive; |
| 301 | const runtimeServicesLayer = Layer.mergeAll( | 300 | const runtimeServicesLayer = Layer.mergeAll( |
| 302 | projectionSnapshotQueryLayer, | 301 | projectionSnapshotQueryLayer, |
| ... | @@ -399,7 +398,7 @@ export const makeOrchestrationIntegrationHarness = ( | ... | @@ -399,7 +398,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 399 | runtime.runPromise(Effect.service(ProviderService)), | 398 | runtime.runPromise(Effect.service(ProviderService)), |
| 400 | ).pipe(Effect.orDie); | 399 | ).pipe(Effect.orDie); |
| 401 | const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => | 400 | const checkpointStore = yield* tryRuntimePromise("load CheckpointStore service", () => |
| 402 | runtime.runPromise(Effect.service(CheckpointStore)), | 401 | runtime.runPromise(Effect.service(CheckpointStore.CheckpointStore)), |
| 403 | ).pipe(Effect.orDie); | 402 | ).pipe(Effect.orDie); |
| 404 | const checkpointRepository = yield* tryRuntimePromise( | 403 | const checkpointRepository = yield* tryRuntimePromise( |
| 405 | "load ProjectionCheckpointRepository service", | 404 | "load ProjectionCheckpointRepository service", |
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts created+418| ... | @@ -0,0 +1,418 @@ | ||
| 1 | import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; | ||
| 2 | import { it } from "@effect/vitest"; | ||
| 3 | import * as Effect from "effect/Effect"; | ||
| 4 | import * as Layer from "effect/Layer"; | ||
| 5 | import * as Option from "effect/Option"; | ||
| 6 | import { describe, expect } from "vite-plus/test"; | ||
| 7 | |||
| 8 | import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; | ||
| 9 | import { checkpointRefForThreadTurn } from "./Utils.ts"; | ||
| 10 | import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts"; | ||
| 11 | import * as CheckpointStore from "./CheckpointStore.ts"; | ||
| 12 | |||
| 13 | function makeThreadCheckpointContext(input: { | ||
| 14 | readonly projectId: ProjectId; | ||
| 15 | readonly threadId: ThreadId; | ||
| 16 | readonly workspaceRoot: string; | ||
| 17 | readonly worktreePath: string | null; | ||
| 18 | readonly checkpointTurnCount: number; | ||
| 19 | readonly checkpointRef: CheckpointRef; | ||
| 20 | }): ProjectionSnapshotQuery.ProjectionThreadCheckpointContext { | ||
| 21 | return { | ||
| 22 | threadId: input.threadId, | ||
| 23 | projectId: input.projectId, | ||
| 24 | workspaceRoot: input.workspaceRoot, | ||
| 25 | worktreePath: input.worktreePath, | ||
| 26 | checkpoints: [ | ||
| 27 | { | ||
| 28 | turnId: TurnId.make("turn-1"), | ||
| 29 | checkpointTurnCount: input.checkpointTurnCount, | ||
| 30 | checkpointRef: input.checkpointRef, | ||
| 31 | status: "ready", | ||
| 32 | files: [], | ||
| 33 | assistantMessageId: null, | ||
| 34 | completedAt: "2026-01-01T00:00:00.000Z", | ||
| 35 | }, | ||
| 36 | ], | ||
| 37 | }; | ||
| 38 | } | ||
| 39 | |||
| 40 | describe("CheckpointDiffQuery.layer", () => { | ||
| 41 | it.effect("uses the narrow full-thread context lookup for all-turns diffs", () => | ||
| 42 | Effect.gen(function* () { | ||
| 43 | const projectId = ProjectId.make("project-full-thread"); | ||
| 44 | const threadId = ThreadId.make("thread-full-thread"); | ||
| 45 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 4); | ||
| 46 | let getThreadCheckpointContextCalls = 0; | ||
| 47 | let getFullThreadDiffContextCalls = 0; | ||
| 48 | const diffCheckpointsCalls: Array<{ | ||
| 49 | readonly fromCheckpointRef: CheckpointRef; | ||
| 50 | readonly toCheckpointRef: CheckpointRef; | ||
| 51 | readonly cwd: string; | ||
| 52 | readonly ignoreWhitespace: boolean; | ||
| 53 | }> = []; | ||
| 54 | |||
| 55 | const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { | ||
| 56 | isGitRepository: () => Effect.succeed(true), | ||
| 57 | captureCheckpoint: () => Effect.void, | ||
| 58 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 59 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 60 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => | ||
| 61 | Effect.sync(() => { | ||
| 62 | diffCheckpointsCalls.push({ | ||
| 63 | fromCheckpointRef, | ||
| 64 | toCheckpointRef, | ||
| 65 | cwd, | ||
| 66 | ignoreWhitespace, | ||
| 67 | }); | ||
| 68 | return "full thread diff patch"; | ||
| 69 | }), | ||
| 70 | deleteCheckpointRefs: () => Effect.void, | ||
| 71 | }; | ||
| 72 | |||
| 73 | const layer = CheckpointDiffQuery.layer.pipe( | ||
| 74 | Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), | ||
| 75 | Layer.provideMerge( | ||
| 76 | Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | ||
| 77 | getCommandReadModel: () => | ||
| 78 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 79 | getSnapshot: () => | ||
| 80 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 81 | getShellSnapshot: () => | ||
| 82 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 83 | getArchivedShellSnapshot: () => | ||
| 84 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 85 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 86 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 87 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 88 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 89 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 90 | getThreadCheckpointContext: () => | ||
| 91 | Effect.sync(() => { | ||
| 92 | getThreadCheckpointContextCalls += 1; | ||
| 93 | return Option.none(); | ||
| 94 | }), | ||
| 95 | getFullThreadDiffContext: () => | ||
| 96 | Effect.sync(() => { | ||
| 97 | getFullThreadDiffContextCalls += 1; | ||
| 98 | return Option.some({ | ||
| 99 | threadId, | ||
| 100 | projectId, | ||
| 101 | workspaceRoot: "/tmp/workspace", | ||
| 102 | worktreePath: "/tmp/worktree", | ||
| 103 | latestCheckpointTurnCount: 4, | ||
| 104 | toCheckpointRef, | ||
| 105 | }); | ||
| 106 | }), | ||
| 107 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 108 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 109 | }), | ||
| 110 | ), | ||
| 111 | ); | ||
| 112 | |||
| 113 | const result = yield* Effect.gen(function* () { | ||
| 114 | const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; | ||
| 115 | return yield* query.getFullThreadDiff({ | ||
| 116 | threadId, | ||
| 117 | toTurnCount: 4, | ||
| 118 | ignoreWhitespace: true, | ||
| 119 | }); | ||
| 120 | }).pipe(Effect.provide(layer)); | ||
| 121 | |||
| 122 | expect(getThreadCheckpointContextCalls).toBe(0); | ||
| 123 | expect(getFullThreadDiffContextCalls).toBe(1); | ||
| 124 | expect(diffCheckpointsCalls).toEqual([ | ||
| 125 | { | ||
| 126 | cwd: "/tmp/worktree", | ||
| 127 | fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0), | ||
| 128 | toCheckpointRef, | ||
| 129 | ignoreWhitespace: true, | ||
| 130 | }, | ||
| 131 | ]); | ||
| 132 | expect(result).toEqual({ | ||
| 133 | threadId, | ||
| 134 | fromTurnCount: 0, | ||
| 135 | toTurnCount: 4, | ||
| 136 | diff: "full thread diff patch", | ||
| 137 | }); | ||
| 138 | }), | ||
| 139 | ); | ||
| 140 | |||
| 141 | it.effect("computes diffs using canonical turn-0 checkpoint refs", () => | ||
| 142 | Effect.gen(function* () { | ||
| 143 | const projectId = ProjectId.make("project-1"); | ||
| 144 | const threadId = ThreadId.make("thread-1"); | ||
| 145 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 146 | const diffCheckpointsCalls: Array<{ | ||
| 147 | readonly fromCheckpointRef: CheckpointRef; | ||
| 148 | readonly toCheckpointRef: CheckpointRef; | ||
| 149 | readonly cwd: string; | ||
| 150 | readonly ignoreWhitespace: boolean; | ||
| 151 | }> = []; | ||
| 152 | |||
| 153 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 154 | projectId, | ||
| 155 | threadId, | ||
| 156 | workspaceRoot: "/tmp/workspace", | ||
| 157 | worktreePath: null, | ||
| 158 | checkpointTurnCount: 1, | ||
| 159 | checkpointRef: toCheckpointRef, | ||
| 160 | }); | ||
| 161 | |||
| 162 | const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { | ||
| 163 | isGitRepository: () => Effect.succeed(true), | ||
| 164 | captureCheckpoint: () => Effect.void, | ||
| 165 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 166 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 167 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => | ||
| 168 | Effect.sync(() => { | ||
| 169 | diffCheckpointsCalls.push({ | ||
| 170 | fromCheckpointRef, | ||
| 171 | toCheckpointRef, | ||
| 172 | cwd, | ||
| 173 | ignoreWhitespace, | ||
| 174 | }); | ||
| 175 | return "diff patch"; | ||
| 176 | }), | ||
| 177 | deleteCheckpointRefs: () => Effect.void, | ||
| 178 | }; | ||
| 179 | |||
| 180 | const layer = CheckpointDiffQuery.layer.pipe( | ||
| 181 | Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), | ||
| 182 | Layer.provideMerge( | ||
| 183 | Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | ||
| 184 | getCommandReadModel: () => | ||
| 185 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 186 | getSnapshot: () => | ||
| 187 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 188 | getShellSnapshot: () => | ||
| 189 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 190 | getArchivedShellSnapshot: () => | ||
| 191 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 192 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 193 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 194 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 195 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 196 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 197 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 198 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 199 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 200 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 201 | }), | ||
| 202 | ), | ||
| 203 | ); | ||
| 204 | |||
| 205 | const result = yield* Effect.gen(function* () { | ||
| 206 | const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; | ||
| 207 | return yield* query.getTurnDiff({ | ||
| 208 | threadId, | ||
| 209 | fromTurnCount: 0, | ||
| 210 | toTurnCount: 1, | ||
| 211 | ignoreWhitespace: true, | ||
| 212 | }); | ||
| 213 | }).pipe(Effect.provide(layer)); | ||
| 214 | |||
| 215 | const expectedFromRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 216 | expect(diffCheckpointsCalls).toEqual([ | ||
| 217 | { | ||
| 218 | cwd: "/tmp/workspace", | ||
| 219 | fromCheckpointRef: expectedFromRef, | ||
| 220 | toCheckpointRef, | ||
| 221 | ignoreWhitespace: true, | ||
| 222 | }, | ||
| 223 | ]); | ||
| 224 | expect(result).toEqual({ | ||
| 225 | threadId, | ||
| 226 | fromTurnCount: 0, | ||
| 227 | toTurnCount: 1, | ||
| 228 | diff: "diff patch", | ||
| 229 | }); | ||
| 230 | }), | ||
| 231 | ); | ||
| 232 | |||
| 233 | it.effect("defaults to hide whitespace changes", () => | ||
| 234 | Effect.gen(function* () { | ||
| 235 | const projectId = ProjectId.make("project-default-whitespace"); | ||
| 236 | const threadId = ThreadId.make("thread-default-whitespace"); | ||
| 237 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 238 | const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = []; | ||
| 239 | |||
| 240 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 241 | projectId, | ||
| 242 | threadId, | ||
| 243 | workspaceRoot: "/tmp/workspace", | ||
| 244 | worktreePath: null, | ||
| 245 | checkpointTurnCount: 1, | ||
| 246 | checkpointRef: toCheckpointRef, | ||
| 247 | }); | ||
| 248 | |||
| 249 | const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { | ||
| 250 | isGitRepository: () => Effect.succeed(true), | ||
| 251 | captureCheckpoint: () => Effect.void, | ||
| 252 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 253 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 254 | diffCheckpoints: ({ ignoreWhitespace }) => | ||
| 255 | Effect.sync(() => { | ||
| 256 | diffCheckpointsCalls.push({ ignoreWhitespace }); | ||
| 257 | return "diff patch"; | ||
| 258 | }), | ||
| 259 | deleteCheckpointRefs: () => Effect.void, | ||
| 260 | }; | ||
| 261 | |||
| 262 | const layer = CheckpointDiffQuery.layer.pipe( | ||
| 263 | Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), | ||
| 264 | Layer.provideMerge( | ||
| 265 | Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | ||
| 266 | getCommandReadModel: () => | ||
| 267 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 268 | getSnapshot: () => | ||
| 269 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 270 | getShellSnapshot: () => | ||
| 271 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 272 | getArchivedShellSnapshot: () => | ||
| 273 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 274 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 275 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 276 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 277 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 278 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 279 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 280 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 281 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 282 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 283 | }), | ||
| 284 | ), | ||
| 285 | ); | ||
| 286 | |||
| 287 | yield* Effect.gen(function* () { | ||
| 288 | const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; | ||
| 289 | return yield* query.getTurnDiff({ | ||
| 290 | threadId, | ||
| 291 | fromTurnCount: 0, | ||
| 292 | toTurnCount: 1, | ||
| 293 | }); | ||
| 294 | }).pipe(Effect.provide(layer)); | ||
| 295 | |||
| 296 | expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]); | ||
| 297 | }), | ||
| 298 | ); | ||
| 299 | |||
| 300 | it.effect("does not preflight checkpoint refs before diffing", () => | ||
| 301 | Effect.gen(function* () { | ||
| 302 | const projectId = ProjectId.make("project-no-preflight"); | ||
| 303 | const threadId = ThreadId.make("thread-no-preflight"); | ||
| 304 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 305 | let hasCheckpointRefCallCount = 0; | ||
| 306 | |||
| 307 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 308 | projectId, | ||
| 309 | threadId, | ||
| 310 | workspaceRoot: "/tmp/workspace", | ||
| 311 | worktreePath: null, | ||
| 312 | checkpointTurnCount: 1, | ||
| 313 | checkpointRef: toCheckpointRef, | ||
| 314 | }); | ||
| 315 | |||
| 316 | const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { | ||
| 317 | isGitRepository: () => Effect.succeed(true), | ||
| 318 | captureCheckpoint: () => Effect.void, | ||
| 319 | hasCheckpointRef: () => | ||
| 320 | Effect.sync(() => { | ||
| 321 | hasCheckpointRefCallCount += 1; | ||
| 322 | return true; | ||
| 323 | }), | ||
| 324 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 325 | diffCheckpoints: () => Effect.succeed("diff patch"), | ||
| 326 | deleteCheckpointRefs: () => Effect.void, | ||
| 327 | }; | ||
| 328 | |||
| 329 | const layer = CheckpointDiffQuery.layer.pipe( | ||
| 330 | Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), | ||
| 331 | Layer.provideMerge( | ||
| 332 | Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | ||
| 333 | getCommandReadModel: () => | ||
| 334 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 335 | getSnapshot: () => | ||
| 336 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 337 | getShellSnapshot: () => | ||
| 338 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 339 | getArchivedShellSnapshot: () => | ||
| 340 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 341 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 342 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 343 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 344 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 345 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 346 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 347 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 348 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 349 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 350 | }), | ||
| 351 | ), | ||
| 352 | ); | ||
| 353 | |||
| 354 | yield* Effect.gen(function* () { | ||
| 355 | const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; | ||
| 356 | return yield* query.getTurnDiff({ | ||
| 357 | threadId, | ||
| 358 | fromTurnCount: 0, | ||
| 359 | toTurnCount: 1, | ||
| 360 | ignoreWhitespace: true, | ||
| 361 | }); | ||
| 362 | }).pipe(Effect.provide(layer)); | ||
| 363 | |||
| 364 | expect(hasCheckpointRefCallCount).toBe(0); | ||
| 365 | }), | ||
| 366 | ); | ||
| 367 | |||
| 368 | it.effect("fails when the thread is missing from the snapshot", () => | ||
| 369 | Effect.gen(function* () { | ||
| 370 | const threadId = ThreadId.make("thread-missing"); | ||
| 371 | |||
| 372 | const checkpointStore: CheckpointStore.CheckpointStore["Service"] = { | ||
| 373 | isGitRepository: () => Effect.succeed(true), | ||
| 374 | captureCheckpoint: () => Effect.void, | ||
| 375 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 376 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 377 | diffCheckpoints: () => Effect.succeed(""), | ||
| 378 | deleteCheckpointRefs: () => Effect.void, | ||
| 379 | }; | ||
| 380 | |||
| 381 | const layer = CheckpointDiffQuery.layer.pipe( | ||
| 382 | Layer.provideMerge(Layer.succeed(CheckpointStore.CheckpointStore, checkpointStore)), | ||
| 383 | Layer.provideMerge( | ||
| 384 | Layer.succeed(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | ||
| 385 | getCommandReadModel: () => | ||
| 386 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 387 | getSnapshot: () => | ||
| 388 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 389 | getShellSnapshot: () => | ||
| 390 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 391 | getArchivedShellSnapshot: () => | ||
| 392 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 393 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 394 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 395 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 396 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 397 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 398 | getThreadCheckpointContext: () => Effect.succeed(Option.none()), | ||
| 399 | getFullThreadDiffContext: () => Effect.succeed(Option.none()), | ||
| 400 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 401 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 402 | }), | ||
| 403 | ), | ||
| 404 | ); | ||
| 405 | |||
| 406 | const error = yield* Effect.gen(function* () { | ||
| 407 | const query = yield* CheckpointDiffQuery.CheckpointDiffQuery; | ||
| 408 | return yield* query.getTurnDiff({ | ||
| 409 | threadId, | ||
| 410 | fromTurnCount: 0, | ||
| 411 | toTurnCount: 1, | ||
| 412 | }); | ||
| 413 | }).pipe(Effect.provide(layer), Effect.flip); | ||
| 414 | |||
| 415 | expect(error.message).toContain("Thread 'thread-missing' not found."); | ||
| 416 | }), | ||
| 417 | ); | ||
| 418 | }); | ||
apps/server/src/checkpointing/CheckpointDiffQuery.ts created+280| ... | @@ -0,0 +1,280 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointDiffQuery - Query interface for computed checkpoint diffs. | ||
| 3 | * | ||
| 4 | * Provides read-only diff operations across checkpoint snapshots used by | ||
| 5 | * orchestration APIs. | ||
| 6 | * | ||
| 7 | * @module CheckpointDiffQuery | ||
| 8 | */ | ||
| 9 | import { | ||
| 10 | type CheckpointRef, | ||
| 11 | OrchestrationGetTurnDiffResult, | ||
| 12 | type OrchestrationGetFullThreadDiffInput, | ||
| 13 | type OrchestrationGetFullThreadDiffResult, | ||
| 14 | type OrchestrationGetTurnDiffInput, | ||
| 15 | type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType, | ||
| 16 | type ThreadId, | ||
| 17 | } from "@t3tools/contracts"; | ||
| 18 | import * as Context from "effect/Context"; | ||
| 19 | import * as Effect from "effect/Effect"; | ||
| 20 | import * as Layer from "effect/Layer"; | ||
| 21 | import * as Option from "effect/Option"; | ||
| 22 | import * as Schema from "effect/Schema"; | ||
| 23 | |||
| 24 | import * as ProjectionSnapshotQuery from "../orchestration/Services/ProjectionSnapshotQuery.ts"; | ||
| 25 | import { CheckpointInvariantError, CheckpointUnavailableError } from "./Errors.ts"; | ||
| 26 | import type { CheckpointServiceError } from "./Errors.ts"; | ||
| 27 | import { checkpointRefForThreadTurn } from "./Utils.ts"; | ||
| 28 | import * as CheckpointStore from "./CheckpointStore.ts"; | ||
| 29 | |||
| 30 | /** Service tag for checkpoint diff queries. */ | ||
| 31 | export class CheckpointDiffQuery extends Context.Service< | ||
| 32 | CheckpointDiffQuery, | ||
| 33 | { | ||
| 34 | /** | ||
| 35 | * Read the patch diff for a single turn checkpoint transition. | ||
| 36 | * | ||
| 37 | * Verifies checkpoint availability in both projection state and filesystem. | ||
| 38 | */ | ||
| 39 | readonly getTurnDiff: ( | ||
| 40 | input: OrchestrationGetTurnDiffInput, | ||
| 41 | ) => Effect.Effect<OrchestrationGetTurnDiffResultType, CheckpointServiceError>; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Read the full patch diff across a thread range of checkpoints. | ||
| 45 | * | ||
| 46 | * Uses turn-diff semantics with `fromTurnCount = 0`. | ||
| 47 | */ | ||
| 48 | readonly getFullThreadDiff: ( | ||
| 49 | input: OrchestrationGetFullThreadDiffInput, | ||
| 50 | ) => Effect.Effect<OrchestrationGetFullThreadDiffResult, CheckpointServiceError>; | ||
| 51 | } | ||
| 52 | >()("t3/checkpointing/CheckpointDiffQuery") {} | ||
| 53 | |||
| 54 | const isTurnDiffResult = Schema.is(OrchestrationGetTurnDiffResult); | ||
| 55 | |||
| 56 | function buildTurnDiffResult( | ||
| 57 | input: { | ||
| 58 | readonly threadId: ThreadId; | ||
| 59 | readonly fromTurnCount: number; | ||
| 60 | readonly toTurnCount: number; | ||
| 61 | }, | ||
| 62 | diff: string, | ||
| 63 | ): OrchestrationGetTurnDiffResultType { | ||
| 64 | return { | ||
| 65 | threadId: input.threadId, | ||
| 66 | fromTurnCount: input.fromTurnCount, | ||
| 67 | toTurnCount: input.toTurnCount, | ||
| 68 | diff, | ||
| 69 | }; | ||
| 70 | } | ||
| 71 | |||
| 72 | export const make = Effect.gen(function* () { | ||
| 73 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; | ||
| 74 | const checkpointStore = yield* CheckpointStore.CheckpointStore; | ||
| 75 | |||
| 76 | const getTurnDiff: CheckpointDiffQuery["Service"]["getTurnDiff"] = Effect.fn("getTurnDiff")( | ||
| 77 | function* (input) { | ||
| 78 | const operation = "CheckpointDiffQuery.getTurnDiff"; | ||
| 79 | const ignoreWhitespace = input.ignoreWhitespace ?? true; | ||
| 80 | yield* Effect.annotateCurrentSpan({ | ||
| 81 | "checkpoint.thread_id": input.threadId, | ||
| 82 | "checkpoint.from_turn_count": input.fromTurnCount, | ||
| 83 | "checkpoint.to_turn_count": input.toTurnCount, | ||
| 84 | "checkpoint.ignore_whitespace": ignoreWhitespace, | ||
| 85 | }); | ||
| 86 | |||
| 87 | if (input.fromTurnCount === input.toTurnCount) { | ||
| 88 | const emptyDiff: OrchestrationGetTurnDiffResultType = { | ||
| 89 | threadId: input.threadId, | ||
| 90 | fromTurnCount: input.fromTurnCount, | ||
| 91 | toTurnCount: input.toTurnCount, | ||
| 92 | diff: "", | ||
| 93 | }; | ||
| 94 | if (!isTurnDiffResult(emptyDiff)) { | ||
| 95 | return yield* new CheckpointInvariantError({ | ||
| 96 | operation, | ||
| 97 | detail: "Computed turn diff result does not satisfy contract schema.", | ||
| 98 | }); | ||
| 99 | } | ||
| 100 | return emptyDiff; | ||
| 101 | } | ||
| 102 | |||
| 103 | const threadContext = yield* projectionSnapshotQuery | ||
| 104 | .getThreadCheckpointContext(input.threadId) | ||
| 105 | .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext")); | ||
| 106 | if (Option.isNone(threadContext)) { | ||
| 107 | return yield* new CheckpointInvariantError({ | ||
| 108 | operation, | ||
| 109 | detail: `Thread '${input.threadId}' not found.`, | ||
| 110 | }); | ||
| 111 | } | ||
| 112 | |||
| 113 | const maxTurnCount = threadContext.value.checkpoints.reduce( | ||
| 114 | (max, checkpoint) => Math.max(max, checkpoint.checkpointTurnCount), | ||
| 115 | 0, | ||
| 116 | ); | ||
| 117 | if (input.toTurnCount > maxTurnCount) { | ||
| 118 | return yield* new CheckpointUnavailableError({ | ||
| 119 | threadId: input.threadId, | ||
| 120 | turnCount: input.toTurnCount, | ||
| 121 | detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`, | ||
| 122 | }); | ||
| 123 | } | ||
| 124 | |||
| 125 | const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; | ||
| 126 | if (!workspaceCwd) { | ||
| 127 | return yield* new CheckpointInvariantError({ | ||
| 128 | operation, | ||
| 129 | detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`, | ||
| 130 | }); | ||
| 131 | } | ||
| 132 | |||
| 133 | const fromCheckpointRef = | ||
| 134 | input.fromTurnCount === 0 | ||
| 135 | ? checkpointRefForThreadTurn(input.threadId, 0) | ||
| 136 | : threadContext.value.checkpoints.find( | ||
| 137 | (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount, | ||
| 138 | )?.checkpointRef; | ||
| 139 | if (!fromCheckpointRef) { | ||
| 140 | return yield* new CheckpointUnavailableError({ | ||
| 141 | threadId: input.threadId, | ||
| 142 | turnCount: input.fromTurnCount, | ||
| 143 | detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`, | ||
| 144 | }); | ||
| 145 | } | ||
| 146 | |||
| 147 | const toCheckpointRef = threadContext.value.checkpoints.find( | ||
| 148 | (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount, | ||
| 149 | )?.checkpointRef; | ||
| 150 | if (!toCheckpointRef) { | ||
| 151 | return yield* new CheckpointUnavailableError({ | ||
| 152 | threadId: input.threadId, | ||
| 153 | turnCount: input.toTurnCount, | ||
| 154 | detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | |||
| 158 | const diff = yield* checkpointStore | ||
| 159 | .diffCheckpoints({ | ||
| 160 | cwd: workspaceCwd, | ||
| 161 | fromCheckpointRef, | ||
| 162 | toCheckpointRef, | ||
| 163 | fallbackFromToHead: false, | ||
| 164 | ignoreWhitespace, | ||
| 165 | }) | ||
| 166 | .pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints")); | ||
| 167 | |||
| 168 | const turnDiff = buildTurnDiffResult(input, diff); | ||
| 169 | if (!isTurnDiffResult(turnDiff)) { | ||
| 170 | return yield* new CheckpointInvariantError({ | ||
| 171 | operation, | ||
| 172 | detail: "Computed turn diff result does not satisfy contract schema.", | ||
| 173 | }); | ||
| 174 | } | ||
| 175 | |||
| 176 | return turnDiff; | ||
| 177 | }, | ||
| 178 | ); | ||
| 179 | |||
| 180 | const getFullThreadDiff: CheckpointDiffQuery["Service"]["getFullThreadDiff"] = Effect.fn( | ||
| 181 | "CheckpointDiffQuery.getFullThreadDiff", | ||
| 182 | )(function* (input) { | ||
| 183 | const operation = "CheckpointDiffQuery.getFullThreadDiff"; | ||
| 184 | const ignoreWhitespace = input.ignoreWhitespace ?? true; | ||
| 185 | yield* Effect.annotateCurrentSpan({ | ||
| 186 | "checkpoint.thread_id": input.threadId, | ||
| 187 | "checkpoint.from_turn_count": 0, | ||
| 188 | "checkpoint.to_turn_count": input.toTurnCount, | ||
| 189 | "checkpoint.ignore_whitespace": ignoreWhitespace, | ||
| 190 | "checkpoint.diff_kind": "full-thread", | ||
| 191 | }); | ||
| 192 | |||
| 193 | if (input.toTurnCount === 0) { | ||
| 194 | const emptyDiff = buildTurnDiffResult( | ||
| 195 | { | ||
| 196 | threadId: input.threadId, | ||
| 197 | fromTurnCount: 0, | ||
| 198 | toTurnCount: 0, | ||
| 199 | }, | ||
| 200 | "", | ||
| 201 | ); | ||
| 202 | if (!isTurnDiffResult(emptyDiff)) { | ||
| 203 | return yield* new CheckpointInvariantError({ | ||
| 204 | operation, | ||
| 205 | detail: "Computed full thread diff result does not satisfy contract schema.", | ||
| 206 | }); | ||
| 207 | } | ||
| 208 | return emptyDiff satisfies OrchestrationGetFullThreadDiffResult; | ||
| 209 | } | ||
| 210 | |||
| 211 | const threadContext = yield* projectionSnapshotQuery | ||
| 212 | .getFullThreadDiffContext(input.threadId, input.toTurnCount) | ||
| 213 | .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext")); | ||
| 214 | |||
| 215 | if (Option.isNone(threadContext)) { | ||
| 216 | return yield* new CheckpointInvariantError({ | ||
| 217 | operation, | ||
| 218 | detail: `Thread '${input.threadId}' not found.`, | ||
| 219 | }); | ||
| 220 | } | ||
| 221 | |||
| 222 | if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) { | ||
| 223 | return yield* new CheckpointUnavailableError({ | ||
| 224 | threadId: input.threadId, | ||
| 225 | turnCount: input.toTurnCount, | ||
| 226 | detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`, | ||
| 227 | }); | ||
| 228 | } | ||
| 229 | |||
| 230 | const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; | ||
| 231 | if (!workspaceCwd) { | ||
| 232 | return yield* new CheckpointInvariantError({ | ||
| 233 | operation, | ||
| 234 | detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`, | ||
| 235 | }); | ||
| 236 | } | ||
| 237 | |||
| 238 | if (!threadContext.value.toCheckpointRef) { | ||
| 239 | return yield* new CheckpointUnavailableError({ | ||
| 240 | threadId: input.threadId, | ||
| 241 | turnCount: input.toTurnCount, | ||
| 242 | detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, | ||
| 243 | }); | ||
| 244 | } | ||
| 245 | |||
| 246 | const diff = yield* checkpointStore | ||
| 247 | .diffCheckpoints({ | ||
| 248 | cwd: workspaceCwd, | ||
| 249 | fromCheckpointRef: checkpointRefForThreadTurn(input.threadId, 0), | ||
| 250 | toCheckpointRef: threadContext.value.toCheckpointRef as CheckpointRef, | ||
| 251 | fallbackFromToHead: false, | ||
| 252 | ignoreWhitespace, | ||
| 253 | }) | ||
| 254 | .pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints")); | ||
| 255 | |||
| 256 | const turnDiff = buildTurnDiffResult( | ||
| 257 | { | ||
| 258 | threadId: input.threadId, | ||
| 259 | fromTurnCount: 0, | ||
| 260 | toTurnCount: input.toTurnCount, | ||
| 261 | }, | ||
| 262 | diff, | ||
| 263 | ); | ||
| 264 | if (!isTurnDiffResult(turnDiff)) { | ||
| 265 | return yield* new CheckpointInvariantError({ | ||
| 266 | operation, | ||
| 267 | detail: "Computed full thread diff result does not satisfy contract schema.", | ||
| 268 | }); | ||
| 269 | } | ||
| 270 | |||
| 271 | return turnDiff satisfies OrchestrationGetFullThreadDiffResult; | ||
| 272 | }); | ||
| 273 | |||
| 274 | return CheckpointDiffQuery.of({ | ||
| 275 | getTurnDiff, | ||
| 276 | getFullThreadDiff, | ||
| 277 | }); | ||
| 278 | }); | ||
| 279 | |||
| 280 | export const layer = Layer.effect(CheckpointDiffQuery, make); | ||
apps/server/src/checkpointing/CheckpointStore.test.ts created+204| ... | @@ -0,0 +1,204 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | import path from "node:path"; | ||
| 3 | |||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 5 | import { it } from "@effect/vitest"; | ||
| 6 | import { ThreadId, type VcsError } from "@t3tools/contracts"; | ||
| 7 | import * as Effect from "effect/Effect"; | ||
| 8 | import * as FileSystem from "effect/FileSystem"; | ||
| 9 | import * as Layer from "effect/Layer"; | ||
| 10 | import * as PlatformError from "effect/PlatformError"; | ||
| 11 | import * as Scope from "effect/Scope"; | ||
| 12 | import { describe, expect } from "vite-plus/test"; | ||
| 13 | |||
| 14 | import { checkpointRefForThreadTurn } from "./Utils.ts"; | ||
| 15 | import * as CheckpointStore from "./CheckpointStore.ts"; | ||
| 16 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | ||
| 17 | import * as VcsProcess from "../vcs/VcsProcess.ts"; | ||
| 18 | import * as ServerConfig from "../config.ts"; | ||
| 19 | |||
| 20 | const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { | ||
| 21 | prefix: "t3-checkpoint-store-test-", | ||
| 22 | }); | ||
| 23 | const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer)); | ||
| 24 | const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer)); | ||
| 25 | const CheckpointStoreTestLayer = CheckpointStore.layer.pipe( | ||
| 26 | Layer.provideMerge(VcsDriverTestLayer), | ||
| 27 | Layer.provideMerge(NodeServices.layer), | ||
| 28 | ); | ||
| 29 | const TestLayer = CheckpointStoreTestLayer.pipe( | ||
| 30 | Layer.provideMerge(VcsProcessTestLayer), | ||
| 31 | Layer.provideMerge(VcsDriverTestLayer), | ||
| 32 | Layer.provideMerge(ServerConfigLayer), | ||
| 33 | Layer.provideMerge(NodeServices.layer), | ||
| 34 | ); | ||
| 35 | |||
| 36 | function makeTmpDir( | ||
| 37 | prefix = "checkpoint-store-test-", | ||
| 38 | ): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> { | ||
| 39 | return Effect.gen(function* () { | ||
| 40 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 41 | return yield* fileSystem.makeTempDirectoryScoped({ prefix }); | ||
| 42 | }); | ||
| 43 | } | ||
| 44 | |||
| 45 | function writeTextFile( | ||
| 46 | filePath: string, | ||
| 47 | contents: string, | ||
| 48 | ): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> { | ||
| 49 | return Effect.gen(function* () { | ||
| 50 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 51 | yield* fileSystem.writeFileString(filePath, contents); | ||
| 52 | }); | ||
| 53 | } | ||
| 54 | |||
| 55 | function git( | ||
| 56 | cwd: string, | ||
| 57 | args: ReadonlyArray<string>, | ||
| 58 | ): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> { | ||
| 59 | return Effect.gen(function* () { | ||
| 60 | const process = yield* VcsProcess.VcsProcess; | ||
| 61 | const result = yield* process.run({ | ||
| 62 | operation: "CheckpointStore.test.git", | ||
| 63 | command: "git", | ||
| 64 | cwd, | ||
| 65 | args, | ||
| 66 | timeoutMs: 10_000, | ||
| 67 | }); | ||
| 68 | return result.stdout.trim(); | ||
| 69 | }); | ||
| 70 | } | ||
| 71 | |||
| 72 | function initRepoWithCommit( | ||
| 73 | cwd: string, | ||
| 74 | ): Effect.Effect< | ||
| 75 | void, | ||
| 76 | VcsError | PlatformError.PlatformError, | ||
| 77 | VcsProcess.VcsProcess | FileSystem.FileSystem | ||
| 78 | > { | ||
| 79 | return Effect.gen(function* () { | ||
| 80 | yield* git(cwd, ["init"]); | ||
| 81 | yield* git(cwd, ["config", "user.email", "test@test.com"]); | ||
| 82 | yield* git(cwd, ["config", "user.name", "Test"]); | ||
| 83 | yield* writeTextFile(path.join(cwd, "README.md"), "# test\n"); | ||
| 84 | yield* git(cwd, ["add", "."]); | ||
| 85 | yield* git(cwd, ["commit", "-m", "initial commit"]); | ||
| 86 | }); | ||
| 87 | } | ||
| 88 | |||
| 89 | function buildLargeText(lineCount = 5_000): string { | ||
| 90 | return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`) | ||
| 91 | .join("\n") | ||
| 92 | .concat("\n"); | ||
| 93 | } | ||
| 94 | |||
| 95 | it.layer(TestLayer)("CheckpointStore.layer", (it) => { | ||
| 96 | describe("diffCheckpoints", () => { | ||
| 97 | it.effect("returns full oversized checkpoint diffs without truncation", () => | ||
| 98 | Effect.gen(function* () { | ||
| 99 | const tmp = yield* makeTmpDir(); | ||
| 100 | yield* initRepoWithCommit(tmp); | ||
| 101 | const checkpointStore = yield* CheckpointStore.CheckpointStore; | ||
| 102 | const threadId = ThreadId.make("thread-checkpoint-store"); | ||
| 103 | const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 104 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 105 | |||
| 106 | yield* checkpointStore.captureCheckpoint({ | ||
| 107 | cwd: tmp, | ||
| 108 | checkpointRef: fromCheckpointRef, | ||
| 109 | }); | ||
| 110 | yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText()); | ||
| 111 | yield* checkpointStore.captureCheckpoint({ | ||
| 112 | cwd: tmp, | ||
| 113 | checkpointRef: toCheckpointRef, | ||
| 114 | }); | ||
| 115 | |||
| 116 | const diff = yield* checkpointStore.diffCheckpoints({ | ||
| 117 | cwd: tmp, | ||
| 118 | fromCheckpointRef, | ||
| 119 | toCheckpointRef, | ||
| 120 | ignoreWhitespace: true, | ||
| 121 | }); | ||
| 122 | |||
| 123 | expect(diff).toContain("diff --git"); | ||
| 124 | expect(diff).not.toContain("[truncated]"); | ||
| 125 | expect(diff).toContain("+line 04999"); | ||
| 126 | }), | ||
| 127 | ); | ||
| 128 | |||
| 129 | it.effect("can hide indentation churn when changes wrap existing lines", () => | ||
| 130 | Effect.gen(function* () { | ||
| 131 | const tmp = yield* makeTmpDir(); | ||
| 132 | yield* initRepoWithCommit(tmp); | ||
| 133 | const checkpointStore = yield* CheckpointStore.CheckpointStore; | ||
| 134 | const threadId = ThreadId.make("thread-checkpoint-store-whitespace"); | ||
| 135 | const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 136 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 137 | |||
| 138 | const componentPath = path.join(tmp, "Component.tsx"); | ||
| 139 | yield* writeTextFile( | ||
| 140 | componentPath, | ||
| 141 | [ | ||
| 142 | "export function View() {", | ||
| 143 | " return (", | ||
| 144 | " <section>", | ||
| 145 | " <h1>Title</h1>", | ||
| 146 | " <p>Body</p>", | ||
| 147 | " </section>", | ||
| 148 | " );", | ||
| 149 | "}", | ||
| 150 | "", | ||
| 151 | ].join("\n"), | ||
| 152 | ); | ||
| 153 | yield* checkpointStore.captureCheckpoint({ | ||
| 154 | cwd: tmp, | ||
| 155 | checkpointRef: fromCheckpointRef, | ||
| 156 | }); | ||
| 157 | yield* writeTextFile( | ||
| 158 | componentPath, | ||
| 159 | [ | ||
| 160 | "export function View() {", | ||
| 161 | " return (", | ||
| 162 | " <section>", | ||
| 163 | " {isReady ? (", | ||
| 164 | " <div>", | ||
| 165 | " <h1>Title</h1>", | ||
| 166 | " <p>Body</p>", | ||
| 167 | " </div>", | ||
| 168 | " ) : null}", | ||
| 169 | " </section>", | ||
| 170 | " );", | ||
| 171 | "}", | ||
| 172 | "", | ||
| 173 | ].join("\n"), | ||
| 174 | ); | ||
| 175 | yield* checkpointStore.captureCheckpoint({ | ||
| 176 | cwd: tmp, | ||
| 177 | checkpointRef: toCheckpointRef, | ||
| 178 | }); | ||
| 179 | |||
| 180 | const normalDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 181 | cwd: tmp, | ||
| 182 | fromCheckpointRef, | ||
| 183 | toCheckpointRef, | ||
| 184 | ignoreWhitespace: false, | ||
| 185 | }); | ||
| 186 | const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 187 | cwd: tmp, | ||
| 188 | fromCheckpointRef, | ||
| 189 | toCheckpointRef, | ||
| 190 | ignoreWhitespace: true, | ||
| 191 | }); | ||
| 192 | |||
| 193 | expect(normalDiff).toContain("diff --git"); | ||
| 194 | expect(normalDiff).toContain("- <h1>Title</h1>"); | ||
| 195 | expect(normalDiff).toContain("+ <h1>Title</h1>"); | ||
| 196 | expect(whitespaceIgnoredDiff).toContain("diff --git"); | ||
| 197 | expect(whitespaceIgnoredDiff).toContain("+ {isReady ? ("); | ||
| 198 | expect(whitespaceIgnoredDiff).toContain("+ <div>"); | ||
| 199 | expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>"); | ||
| 200 | expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>"); | ||
| 201 | }), | ||
| 202 | ); | ||
| 203 | }); | ||
| 204 | }); | ||
apps/server/src/checkpointing/CheckpointStore.ts created+171| ... | @@ -0,0 +1,171 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints. | ||
| 3 | * | ||
| 4 | * Owns hidden Git-ref checkpoint capture/restore and diff computation for a | ||
| 5 | * workspace thread timeline. It does not store user-facing checkpoint metadata | ||
| 6 | * and does not coordinate provider conversation rollback. | ||
| 7 | * | ||
| 8 | * The live adapter resolves the active VCS driver once per checkpoint operation | ||
| 9 | * and delegates to the driver's optional checkpoint capability. | ||
| 10 | * | ||
| 11 | * Uses Effect `Context.Service` for dependency injection and exposes typed | ||
| 12 | * domain errors for checkpoint storage operations. | ||
| 13 | * | ||
| 14 | * @module CheckpointStore | ||
| 15 | */ | ||
| 16 | import { VcsUnsupportedOperationError, type CheckpointRef } from "@t3tools/contracts"; | ||
| 17 | import * as Context from "effect/Context"; | ||
| 18 | import * as Effect from "effect/Effect"; | ||
| 19 | import * as Layer from "effect/Layer"; | ||
| 20 | |||
| 21 | import type { CheckpointStoreError } from "./Errors.ts"; | ||
| 22 | import type { VcsCheckpointOps } from "../vcs/VcsDriver.ts"; | ||
| 23 | import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts"; | ||
| 24 | |||
| 25 | export interface CaptureCheckpointInput { | ||
| 26 | readonly cwd: string; | ||
| 27 | readonly checkpointRef: CheckpointRef; | ||
| 28 | } | ||
| 29 | |||
| 30 | export interface RestoreCheckpointInput { | ||
| 31 | readonly cwd: string; | ||
| 32 | readonly checkpointRef: CheckpointRef; | ||
| 33 | readonly fallbackToHead?: boolean; | ||
| 34 | } | ||
| 35 | |||
| 36 | export interface DiffCheckpointsInput { | ||
| 37 | readonly cwd: string; | ||
| 38 | readonly fromCheckpointRef: CheckpointRef; | ||
| 39 | readonly toCheckpointRef: CheckpointRef; | ||
| 40 | readonly fallbackFromToHead?: boolean; | ||
| 41 | readonly ignoreWhitespace: boolean; | ||
| 42 | } | ||
| 43 | |||
| 44 | export interface DeleteCheckpointRefsInput { | ||
| 45 | readonly cwd: string; | ||
| 46 | readonly checkpointRefs: ReadonlyArray<CheckpointRef>; | ||
| 47 | } | ||
| 48 | |||
| 49 | /** Service tag for checkpoint persistence and restore operations. */ | ||
| 50 | export class CheckpointStore extends Context.Service< | ||
| 51 | CheckpointStore, | ||
| 52 | { | ||
| 53 | /** Check whether cwd is inside a Git worktree. */ | ||
| 54 | readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 55 | |||
| 56 | /** | ||
| 57 | * Capture a checkpoint commit and store it at the provided checkpoint ref. | ||
| 58 | * | ||
| 59 | * Uses an isolated temporary Git index and writes a hidden ref. | ||
| 60 | */ | ||
| 61 | readonly captureCheckpoint: ( | ||
| 62 | input: CaptureCheckpointInput, | ||
| 63 | ) => Effect.Effect<void, CheckpointStoreError>; | ||
| 64 | |||
| 65 | /** Check whether a checkpoint ref exists. */ | ||
| 66 | readonly hasCheckpointRef: ( | ||
| 67 | input: Omit<RestoreCheckpointInput, "fallbackToHead">, | ||
| 68 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 69 | |||
| 70 | /** | ||
| 71 | * Restore workspace and staging state to a checkpoint. | ||
| 72 | * | ||
| 73 | * Optionally falls back to current `HEAD` when the checkpoint ref is missing. | ||
| 74 | */ | ||
| 75 | readonly restoreCheckpoint: ( | ||
| 76 | input: RestoreCheckpointInput, | ||
| 77 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Compute a patch diff between two checkpoint refs. | ||
| 81 | * | ||
| 82 | * Can optionally treat a missing "from" ref as `HEAD`. | ||
| 83 | */ | ||
| 84 | readonly diffCheckpoints: ( | ||
| 85 | input: DiffCheckpointsInput, | ||
| 86 | ) => Effect.Effect<string, CheckpointStoreError>; | ||
| 87 | |||
| 88 | /** | ||
| 89 | * Delete the provided checkpoint refs. | ||
| 90 | * | ||
| 91 | * Best-effort delete: missing refs are tolerated. | ||
| 92 | */ | ||
| 93 | readonly deleteCheckpointRefs: ( | ||
| 94 | input: DeleteCheckpointRefsInput, | ||
| 95 | ) => Effect.Effect<void, CheckpointStoreError>; | ||
| 96 | } | ||
| 97 | >()("t3/checkpointing/CheckpointStore") {} | ||
| 98 | |||
| 99 | export const make = Effect.gen(function* () { | ||
| 100 | const vcsRegistry = yield* VcsDriverRegistry.VcsDriverRegistry; | ||
| 101 | |||
| 102 | const resolveCheckpoints = Effect.fn("CheckpointStore.resolveCheckpoints")(function* ( | ||
| 103 | operation: string, | ||
| 104 | cwd: string, | ||
| 105 | ) { | ||
| 106 | const handle = yield* vcsRegistry.resolve({ cwd }); | ||
| 107 | if (!handle.driver.checkpoints) { | ||
| 108 | return yield* new VcsUnsupportedOperationError({ | ||
| 109 | operation, | ||
| 110 | kind: handle.kind, | ||
| 111 | detail: `${handle.kind} driver does not implement checkpoint operations.`, | ||
| 112 | }); | ||
| 113 | } | ||
| 114 | return handle.driver.checkpoints satisfies VcsCheckpointOps; | ||
| 115 | }); | ||
| 116 | |||
| 117 | const isGitRepository: CheckpointStore["Service"]["isGitRepository"] = (cwd) => | ||
| 118 | vcsRegistry.resolve({ cwd, requestedKind: "git" }).pipe( | ||
| 119 | Effect.map(() => true), | ||
| 120 | Effect.orElseSucceed(() => false), | ||
| 121 | ); | ||
| 122 | |||
| 123 | const captureCheckpoint: CheckpointStore["Service"]["captureCheckpoint"] = Effect.fn( | ||
| 124 | "captureCheckpoint", | ||
| 125 | )(function* (input) { | ||
| 126 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.captureCheckpoint", input.cwd); | ||
| 127 | return yield* checkpoints.captureCheckpoint(input); | ||
| 128 | }); | ||
| 129 | |||
| 130 | const hasCheckpointRef: CheckpointStore["Service"]["hasCheckpointRef"] = Effect.fn( | ||
| 131 | "hasCheckpointRef", | ||
| 132 | )(function* (input) { | ||
| 133 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.hasCheckpointRef", input.cwd); | ||
| 134 | return yield* checkpoints.hasCheckpointRef(input); | ||
| 135 | }); | ||
| 136 | |||
| 137 | const restoreCheckpoint: CheckpointStore["Service"]["restoreCheckpoint"] = Effect.fn( | ||
| 138 | "restoreCheckpoint", | ||
| 139 | )(function* (input) { | ||
| 140 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.restoreCheckpoint", input.cwd); | ||
| 141 | return yield* checkpoints.restoreCheckpoint(input); | ||
| 142 | }); | ||
| 143 | |||
| 144 | const diffCheckpoints: CheckpointStore["Service"]["diffCheckpoints"] = Effect.fn( | ||
| 145 | "diffCheckpoints", | ||
| 146 | )(function* (input) { | ||
| 147 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.diffCheckpoints", input.cwd); | ||
| 148 | return yield* checkpoints.diffCheckpoints(input); | ||
| 149 | }); | ||
| 150 | |||
| 151 | const deleteCheckpointRefs: CheckpointStore["Service"]["deleteCheckpointRefs"] = Effect.fn( | ||
| 152 | "deleteCheckpointRefs", | ||
| 153 | )(function* (input) { | ||
| 154 | const checkpoints = yield* resolveCheckpoints( | ||
| 155 | "CheckpointStore.deleteCheckpointRefs", | ||
| 156 | input.cwd, | ||
| 157 | ); | ||
| 158 | return yield* checkpoints.deleteCheckpointRefs(input); | ||
| 159 | }); | ||
| 160 | |||
| 161 | return CheckpointStore.of({ | ||
| 162 | isGitRepository, | ||
| 163 | captureCheckpoint, | ||
| 164 | hasCheckpointRef, | ||
| 165 | restoreCheckpoint, | ||
| 166 | diffCheckpoints, | ||
| 167 | deleteCheckpointRefs, | ||
| 168 | }); | ||
| 169 | }); | ||
| 170 | |||
| 171 | export const layer = Layer.effect(CheckpointStore, make); | ||
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts deleted-421| ... | @@ -1,421 +0,0 @@ | ||
| 1 | import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import * as Option from "effect/Option"; | ||
| 5 | import { describe, expect, it } from "vite-plus/test"; | ||
| 6 | |||
| 7 | import { | ||
| 8 | ProjectionSnapshotQuery, | ||
| 9 | type ProjectionThreadCheckpointContext, | ||
| 10 | } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; | ||
| 11 | import { checkpointRefForThreadTurn } from "../Utils.ts"; | ||
| 12 | import { CheckpointDiffQueryLive } from "./CheckpointDiffQuery.ts"; | ||
| 13 | import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts"; | ||
| 14 | import { CheckpointDiffQuery } from "../Services/CheckpointDiffQuery.ts"; | ||
| 15 | |||
| 16 | function makeThreadCheckpointContext(input: { | ||
| 17 | readonly projectId: ProjectId; | ||
| 18 | readonly threadId: ThreadId; | ||
| 19 | readonly workspaceRoot: string; | ||
| 20 | readonly worktreePath: string | null; | ||
| 21 | readonly checkpointTurnCount: number; | ||
| 22 | readonly checkpointRef: CheckpointRef; | ||
| 23 | }): ProjectionThreadCheckpointContext { | ||
| 24 | return { | ||
| 25 | threadId: input.threadId, | ||
| 26 | projectId: input.projectId, | ||
| 27 | workspaceRoot: input.workspaceRoot, | ||
| 28 | worktreePath: input.worktreePath, | ||
| 29 | checkpoints: [ | ||
| 30 | { | ||
| 31 | turnId: TurnId.make("turn-1"), | ||
| 32 | checkpointTurnCount: input.checkpointTurnCount, | ||
| 33 | checkpointRef: input.checkpointRef, | ||
| 34 | status: "ready", | ||
| 35 | files: [], | ||
| 36 | assistantMessageId: null, | ||
| 37 | completedAt: "2026-01-01T00:00:00.000Z", | ||
| 38 | }, | ||
| 39 | ], | ||
| 40 | }; | ||
| 41 | } | ||
| 42 | |||
| 43 | describe("CheckpointDiffQueryLive", () => { | ||
| 44 | it("uses the narrow full-thread context lookup for all-turns diffs", async () => { | ||
| 45 | const projectId = ProjectId.make("project-full-thread"); | ||
| 46 | const threadId = ThreadId.make("thread-full-thread"); | ||
| 47 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 4); | ||
| 48 | let getThreadCheckpointContextCalls = 0; | ||
| 49 | let getFullThreadDiffContextCalls = 0; | ||
| 50 | const diffCheckpointsCalls: Array<{ | ||
| 51 | readonly fromCheckpointRef: CheckpointRef; | ||
| 52 | readonly toCheckpointRef: CheckpointRef; | ||
| 53 | readonly cwd: string; | ||
| 54 | readonly ignoreWhitespace: boolean; | ||
| 55 | }> = []; | ||
| 56 | |||
| 57 | const checkpointStore: CheckpointStoreShape = { | ||
| 58 | isGitRepository: () => Effect.succeed(true), | ||
| 59 | captureCheckpoint: () => Effect.void, | ||
| 60 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 61 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 62 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => | ||
| 63 | Effect.sync(() => { | ||
| 64 | diffCheckpointsCalls.push({ | ||
| 65 | fromCheckpointRef, | ||
| 66 | toCheckpointRef, | ||
| 67 | cwd, | ||
| 68 | ignoreWhitespace, | ||
| 69 | }); | ||
| 70 | return "full thread diff patch"; | ||
| 71 | }), | ||
| 72 | deleteCheckpointRefs: () => Effect.void, | ||
| 73 | }; | ||
| 74 | |||
| 75 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 76 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 77 | Layer.provideMerge( | ||
| 78 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 79 | getCommandReadModel: () => | ||
| 80 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 81 | getSnapshot: () => | ||
| 82 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 83 | getShellSnapshot: () => | ||
| 84 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 85 | getArchivedShellSnapshot: () => | ||
| 86 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 87 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 88 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 89 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 90 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 91 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 92 | getThreadCheckpointContext: () => | ||
| 93 | Effect.sync(() => { | ||
| 94 | getThreadCheckpointContextCalls += 1; | ||
| 95 | return Option.none(); | ||
| 96 | }), | ||
| 97 | getFullThreadDiffContext: () => | ||
| 98 | Effect.sync(() => { | ||
| 99 | getFullThreadDiffContextCalls += 1; | ||
| 100 | return Option.some({ | ||
| 101 | threadId, | ||
| 102 | projectId, | ||
| 103 | workspaceRoot: "/tmp/workspace", | ||
| 104 | worktreePath: "/tmp/worktree", | ||
| 105 | latestCheckpointTurnCount: 4, | ||
| 106 | toCheckpointRef, | ||
| 107 | }); | ||
| 108 | }), | ||
| 109 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 110 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 111 | }), | ||
| 112 | ), | ||
| 113 | ); | ||
| 114 | |||
| 115 | const result = await Effect.runPromise( | ||
| 116 | Effect.gen(function* () { | ||
| 117 | const query = yield* CheckpointDiffQuery; | ||
| 118 | return yield* query.getFullThreadDiff({ | ||
| 119 | threadId, | ||
| 120 | toTurnCount: 4, | ||
| 121 | ignoreWhitespace: true, | ||
| 122 | }); | ||
| 123 | }).pipe(Effect.provide(layer)), | ||
| 124 | ); | ||
| 125 | |||
| 126 | expect(getThreadCheckpointContextCalls).toBe(0); | ||
| 127 | expect(getFullThreadDiffContextCalls).toBe(1); | ||
| 128 | expect(diffCheckpointsCalls).toEqual([ | ||
| 129 | { | ||
| 130 | cwd: "/tmp/worktree", | ||
| 131 | fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0), | ||
| 132 | toCheckpointRef, | ||
| 133 | ignoreWhitespace: true, | ||
| 134 | }, | ||
| 135 | ]); | ||
| 136 | expect(result).toEqual({ | ||
| 137 | threadId, | ||
| 138 | fromTurnCount: 0, | ||
| 139 | toTurnCount: 4, | ||
| 140 | diff: "full thread diff patch", | ||
| 141 | }); | ||
| 142 | }); | ||
| 143 | |||
| 144 | it("computes diffs using canonical turn-0 checkpoint refs", async () => { | ||
| 145 | const projectId = ProjectId.make("project-1"); | ||
| 146 | const threadId = ThreadId.make("thread-1"); | ||
| 147 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 148 | const diffCheckpointsCalls: Array<{ | ||
| 149 | readonly fromCheckpointRef: CheckpointRef; | ||
| 150 | readonly toCheckpointRef: CheckpointRef; | ||
| 151 | readonly cwd: string; | ||
| 152 | readonly ignoreWhitespace: boolean; | ||
| 153 | }> = []; | ||
| 154 | |||
| 155 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 156 | projectId, | ||
| 157 | threadId, | ||
| 158 | workspaceRoot: "/tmp/workspace", | ||
| 159 | worktreePath: null, | ||
| 160 | checkpointTurnCount: 1, | ||
| 161 | checkpointRef: toCheckpointRef, | ||
| 162 | }); | ||
| 163 | |||
| 164 | const checkpointStore: CheckpointStoreShape = { | ||
| 165 | isGitRepository: () => Effect.succeed(true), | ||
| 166 | captureCheckpoint: () => Effect.void, | ||
| 167 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 168 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 169 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => | ||
| 170 | Effect.sync(() => { | ||
| 171 | diffCheckpointsCalls.push({ | ||
| 172 | fromCheckpointRef, | ||
| 173 | toCheckpointRef, | ||
| 174 | cwd, | ||
| 175 | ignoreWhitespace, | ||
| 176 | }); | ||
| 177 | return "diff patch"; | ||
| 178 | }), | ||
| 179 | deleteCheckpointRefs: () => Effect.void, | ||
| 180 | }; | ||
| 181 | |||
| 182 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 183 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 184 | Layer.provideMerge( | ||
| 185 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 186 | getCommandReadModel: () => | ||
| 187 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 188 | getSnapshot: () => | ||
| 189 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 190 | getShellSnapshot: () => | ||
| 191 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 192 | getArchivedShellSnapshot: () => | ||
| 193 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 194 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 195 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 196 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 197 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 198 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 199 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 200 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 201 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 202 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 203 | }), | ||
| 204 | ), | ||
| 205 | ); | ||
| 206 | |||
| 207 | const result = await Effect.runPromise( | ||
| 208 | Effect.gen(function* () { | ||
| 209 | const query = yield* CheckpointDiffQuery; | ||
| 210 | return yield* query.getTurnDiff({ | ||
| 211 | threadId, | ||
| 212 | fromTurnCount: 0, | ||
| 213 | toTurnCount: 1, | ||
| 214 | ignoreWhitespace: true, | ||
| 215 | }); | ||
| 216 | }).pipe(Effect.provide(layer)), | ||
| 217 | ); | ||
| 218 | |||
| 219 | const expectedFromRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 220 | expect(diffCheckpointsCalls).toEqual([ | ||
| 221 | { | ||
| 222 | cwd: "/tmp/workspace", | ||
| 223 | fromCheckpointRef: expectedFromRef, | ||
| 224 | toCheckpointRef, | ||
| 225 | ignoreWhitespace: true, | ||
| 226 | }, | ||
| 227 | ]); | ||
| 228 | expect(result).toEqual({ | ||
| 229 | threadId, | ||
| 230 | fromTurnCount: 0, | ||
| 231 | toTurnCount: 1, | ||
| 232 | diff: "diff patch", | ||
| 233 | }); | ||
| 234 | }); | ||
| 235 | |||
| 236 | it("defaults to hide whitespace changes", async () => { | ||
| 237 | const projectId = ProjectId.make("project-default-whitespace"); | ||
| 238 | const threadId = ThreadId.make("thread-default-whitespace"); | ||
| 239 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 240 | const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = []; | ||
| 241 | |||
| 242 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 243 | projectId, | ||
| 244 | threadId, | ||
| 245 | workspaceRoot: "/tmp/workspace", | ||
| 246 | worktreePath: null, | ||
| 247 | checkpointTurnCount: 1, | ||
| 248 | checkpointRef: toCheckpointRef, | ||
| 249 | }); | ||
| 250 | |||
| 251 | const checkpointStore: CheckpointStoreShape = { | ||
| 252 | isGitRepository: () => Effect.succeed(true), | ||
| 253 | captureCheckpoint: () => Effect.void, | ||
| 254 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 255 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 256 | diffCheckpoints: ({ ignoreWhitespace }) => | ||
| 257 | Effect.sync(() => { | ||
| 258 | diffCheckpointsCalls.push({ ignoreWhitespace }); | ||
| 259 | return "diff patch"; | ||
| 260 | }), | ||
| 261 | deleteCheckpointRefs: () => Effect.void, | ||
| 262 | }; | ||
| 263 | |||
| 264 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 265 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 266 | Layer.provideMerge( | ||
| 267 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 268 | getCommandReadModel: () => | ||
| 269 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 270 | getSnapshot: () => | ||
| 271 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 272 | getShellSnapshot: () => | ||
| 273 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 274 | getArchivedShellSnapshot: () => | ||
| 275 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 276 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 277 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 278 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 279 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 280 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 281 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 282 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 283 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 284 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 285 | }), | ||
| 286 | ), | ||
| 287 | ); | ||
| 288 | |||
| 289 | await Effect.runPromise( | ||
| 290 | Effect.gen(function* () { | ||
| 291 | const query = yield* CheckpointDiffQuery; | ||
| 292 | return yield* query.getTurnDiff({ | ||
| 293 | threadId, | ||
| 294 | fromTurnCount: 0, | ||
| 295 | toTurnCount: 1, | ||
| 296 | }); | ||
| 297 | }).pipe(Effect.provide(layer)), | ||
| 298 | ); | ||
| 299 | |||
| 300 | expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]); | ||
| 301 | }); | ||
| 302 | |||
| 303 | it("does not preflight checkpoint refs before diffing", async () => { | ||
| 304 | const projectId = ProjectId.make("project-no-preflight"); | ||
| 305 | const threadId = ThreadId.make("thread-no-preflight"); | ||
| 306 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 307 | let hasCheckpointRefCallCount = 0; | ||
| 308 | |||
| 309 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 310 | projectId, | ||
| 311 | threadId, | ||
| 312 | workspaceRoot: "/tmp/workspace", | ||
| 313 | worktreePath: null, | ||
| 314 | checkpointTurnCount: 1, | ||
| 315 | checkpointRef: toCheckpointRef, | ||
| 316 | }); | ||
| 317 | |||
| 318 | const checkpointStore: CheckpointStoreShape = { | ||
| 319 | isGitRepository: () => Effect.succeed(true), | ||
| 320 | captureCheckpoint: () => Effect.void, | ||
| 321 | hasCheckpointRef: () => | ||
| 322 | Effect.sync(() => { | ||
| 323 | hasCheckpointRefCallCount += 1; | ||
| 324 | return true; | ||
| 325 | }), | ||
| 326 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 327 | diffCheckpoints: () => Effect.succeed("diff patch"), | ||
| 328 | deleteCheckpointRefs: () => Effect.void, | ||
| 329 | }; | ||
| 330 | |||
| 331 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 332 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 333 | Layer.provideMerge( | ||
| 334 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 335 | getCommandReadModel: () => | ||
| 336 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 337 | getSnapshot: () => | ||
| 338 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 339 | getShellSnapshot: () => | ||
| 340 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 341 | getArchivedShellSnapshot: () => | ||
| 342 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 343 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 344 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 345 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 346 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 347 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 348 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 349 | getFullThreadDiffContext: () => Effect.die("unused"), | ||
| 350 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 351 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 352 | }), | ||
| 353 | ), | ||
| 354 | ); | ||
| 355 | |||
| 356 | await Effect.runPromise( | ||
| 357 | Effect.gen(function* () { | ||
| 358 | const query = yield* CheckpointDiffQuery; | ||
| 359 | return yield* query.getTurnDiff({ | ||
| 360 | threadId, | ||
| 361 | fromTurnCount: 0, | ||
| 362 | toTurnCount: 1, | ||
| 363 | ignoreWhitespace: true, | ||
| 364 | }); | ||
| 365 | }).pipe(Effect.provide(layer)), | ||
| 366 | ); | ||
| 367 | |||
| 368 | expect(hasCheckpointRefCallCount).toBe(0); | ||
| 369 | }); | ||
| 370 | |||
| 371 | it("fails when the thread is missing from the snapshot", async () => { | ||
| 372 | const threadId = ThreadId.make("thread-missing"); | ||
| 373 | |||
| 374 | const checkpointStore: CheckpointStoreShape = { | ||
| 375 | isGitRepository: () => Effect.succeed(true), | ||
| 376 | captureCheckpoint: () => Effect.void, | ||
| 377 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 378 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 379 | diffCheckpoints: () => Effect.succeed(""), | ||
| 380 | deleteCheckpointRefs: () => Effect.void, | ||
| 381 | }; | ||
| 382 | |||
| 383 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 384 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 385 | Layer.provideMerge( | ||
| 386 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 387 | getCommandReadModel: () => | ||
| 388 | Effect.die("CheckpointDiffQuery should not request the command read model"), | ||
| 389 | getSnapshot: () => | ||
| 390 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 391 | getShellSnapshot: () => | ||
| 392 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 393 | getArchivedShellSnapshot: () => | ||
| 394 | Effect.die("CheckpointDiffQuery should not request archived shell snapshots"), | ||
| 395 | getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), | ||
| 396 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 397 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 398 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 399 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 400 | getThreadCheckpointContext: () => Effect.succeed(Option.none()), | ||
| 401 | getFullThreadDiffContext: () => Effect.succeed(Option.none()), | ||
| 402 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 403 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 404 | }), | ||
| 405 | ), | ||
| 406 | ); | ||
| 407 | |||
| 408 | await expect( | ||
| 409 | Effect.runPromise( | ||
| 410 | Effect.gen(function* () { | ||
| 411 | const query = yield* CheckpointDiffQuery; | ||
| 412 | return yield* query.getTurnDiff({ | ||
| 413 | threadId, | ||
| 414 | fromTurnCount: 0, | ||
| 415 | toTurnCount: 1, | ||
| 416 | }); | ||
| 417 | }).pipe(Effect.provide(layer)), | ||
| 418 | ), | ||
| 419 | ).rejects.toThrow("Thread 'thread-missing' not found."); | ||
| 420 | }); | ||
| 421 | }); | ||
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts deleted-248| ... | @@ -1,248 +0,0 @@ | ||
| 1 | import { | ||
| 2 | type CheckpointRef, | ||
| 3 | OrchestrationGetTurnDiffResult, | ||
| 4 | type ThreadId, | ||
| 5 | type OrchestrationGetFullThreadDiffResult, | ||
| 6 | type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType, | ||
| 7 | } from "@t3tools/contracts"; | ||
| 8 | import * as Effect from "effect/Effect"; | ||
| 9 | import * as Layer from "effect/Layer"; | ||
| 10 | import * as Option from "effect/Option"; | ||
| 11 | import * as Schema from "effect/Schema"; | ||
| 12 | |||
| 13 | import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; | ||
| 14 | import { CheckpointInvariantError, CheckpointUnavailableError } from "../Errors.ts"; | ||
| 15 | import { checkpointRefForThreadTurn } from "../Utils.ts"; | ||
| 16 | import { CheckpointStore } from "../Services/CheckpointStore.ts"; | ||
| 17 | import { | ||
| 18 | CheckpointDiffQuery, | ||
| 19 | type CheckpointDiffQueryShape, | ||
| 20 | } from "../Services/CheckpointDiffQuery.ts"; | ||
| 21 | |||
| 22 | const isTurnDiffResult = Schema.is(OrchestrationGetTurnDiffResult); | ||
| 23 | |||
| 24 | function buildTurnDiffResult( | ||
| 25 | input: { | ||
| 26 | readonly threadId: ThreadId; | ||
| 27 | readonly fromTurnCount: number; | ||
| 28 | readonly toTurnCount: number; | ||
| 29 | }, | ||
| 30 | diff: string, | ||
| 31 | ): OrchestrationGetTurnDiffResultType { | ||
| 32 | return { | ||
| 33 | threadId: input.threadId, | ||
| 34 | fromTurnCount: input.fromTurnCount, | ||
| 35 | toTurnCount: input.toTurnCount, | ||
| 36 | diff, | ||
| 37 | }; | ||
| 38 | } | ||
| 39 | |||
| 40 | const make = Effect.gen(function* () { | ||
| 41 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; | ||
| 42 | const checkpointStore = yield* CheckpointStore; | ||
| 43 | |||
| 44 | const getTurnDiff: CheckpointDiffQueryShape["getTurnDiff"] = Effect.fn("getTurnDiff")( | ||
| 45 | function* (input) { | ||
| 46 | const operation = "CheckpointDiffQuery.getTurnDiff"; | ||
| 47 | const ignoreWhitespace = input.ignoreWhitespace ?? true; | ||
| 48 | yield* Effect.annotateCurrentSpan({ | ||
| 49 | "checkpoint.thread_id": input.threadId, | ||
| 50 | "checkpoint.from_turn_count": input.fromTurnCount, | ||
| 51 | "checkpoint.to_turn_count": input.toTurnCount, | ||
| 52 | "checkpoint.ignore_whitespace": ignoreWhitespace, | ||
| 53 | }); | ||
| 54 | |||
| 55 | if (input.fromTurnCount === input.toTurnCount) { | ||
| 56 | const emptyDiff: OrchestrationGetTurnDiffResultType = { | ||
| 57 | threadId: input.threadId, | ||
| 58 | fromTurnCount: input.fromTurnCount, | ||
| 59 | toTurnCount: input.toTurnCount, | ||
| 60 | diff: "", | ||
| 61 | }; | ||
| 62 | if (!isTurnDiffResult(emptyDiff)) { | ||
| 63 | return yield* new CheckpointInvariantError({ | ||
| 64 | operation, | ||
| 65 | detail: "Computed turn diff result does not satisfy contract schema.", | ||
| 66 | }); | ||
| 67 | } | ||
| 68 | return emptyDiff; | ||
| 69 | } | ||
| 70 | |||
| 71 | const threadContext = yield* projectionSnapshotQuery | ||
| 72 | .getThreadCheckpointContext(input.threadId) | ||
| 73 | .pipe(Effect.withSpan("checkpoint.turnDiff.lookupContext")); | ||
| 74 | if (Option.isNone(threadContext)) { | ||
| 75 | return yield* new CheckpointInvariantError({ | ||
| 76 | operation, | ||
| 77 | detail: `Thread '${input.threadId}' not found.`, | ||
| 78 | }); | ||
| 79 | } | ||
| 80 | |||
| 81 | const maxTurnCount = threadContext.value.checkpoints.reduce( | ||
| 82 | (max, checkpoint) => Math.max(max, checkpoint.checkpointTurnCount), | ||
| 83 | 0, | ||
| 84 | ); | ||
| 85 | if (input.toTurnCount > maxTurnCount) { | ||
| 86 | return yield* new CheckpointUnavailableError({ | ||
| 87 | threadId: input.threadId, | ||
| 88 | turnCount: input.toTurnCount, | ||
| 89 | detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${maxTurnCount}.`, | ||
| 90 | }); | ||
| 91 | } | ||
| 92 | |||
| 93 | const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; | ||
| 94 | if (!workspaceCwd) { | ||
| 95 | return yield* new CheckpointInvariantError({ | ||
| 96 | operation, | ||
| 97 | detail: `Workspace path missing for thread '${input.threadId}' when computing turn diff.`, | ||
| 98 | }); | ||
| 99 | } | ||
| 100 | |||
| 101 | const fromCheckpointRef = | ||
| 102 | input.fromTurnCount === 0 | ||
| 103 | ? checkpointRefForThreadTurn(input.threadId, 0) | ||
| 104 | : threadContext.value.checkpoints.find( | ||
| 105 | (checkpoint) => checkpoint.checkpointTurnCount === input.fromTurnCount, | ||
| 106 | )?.checkpointRef; | ||
| 107 | if (!fromCheckpointRef) { | ||
| 108 | return yield* new CheckpointUnavailableError({ | ||
| 109 | threadId: input.threadId, | ||
| 110 | turnCount: input.fromTurnCount, | ||
| 111 | detail: `Checkpoint ref is unavailable for turn ${input.fromTurnCount}.`, | ||
| 112 | }); | ||
| 113 | } | ||
| 114 | |||
| 115 | const toCheckpointRef = threadContext.value.checkpoints.find( | ||
| 116 | (checkpoint) => checkpoint.checkpointTurnCount === input.toTurnCount, | ||
| 117 | )?.checkpointRef; | ||
| 118 | if (!toCheckpointRef) { | ||
| 119 | return yield* new CheckpointUnavailableError({ | ||
| 120 | threadId: input.threadId, | ||
| 121 | turnCount: input.toTurnCount, | ||
| 122 | detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, | ||
| 123 | }); | ||
| 124 | } | ||
| 125 | |||
| 126 | const diff = yield* checkpointStore | ||
| 127 | .diffCheckpoints({ | ||
| 128 | cwd: workspaceCwd, | ||
| 129 | fromCheckpointRef, | ||
| 130 | toCheckpointRef, | ||
| 131 | fallbackFromToHead: false, | ||
| 132 | ignoreWhitespace, | ||
| 133 | }) | ||
| 134 | .pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints")); | ||
| 135 | |||
| 136 | const turnDiff = buildTurnDiffResult(input, diff); | ||
| 137 | if (!isTurnDiffResult(turnDiff)) { | ||
| 138 | return yield* new CheckpointInvariantError({ | ||
| 139 | operation, | ||
| 140 | detail: "Computed turn diff result does not satisfy contract schema.", | ||
| 141 | }); | ||
| 142 | } | ||
| 143 | |||
| 144 | return turnDiff; | ||
| 145 | }, | ||
| 146 | ); | ||
| 147 | |||
| 148 | const getFullThreadDiff: CheckpointDiffQueryShape["getFullThreadDiff"] = Effect.fn( | ||
| 149 | "CheckpointDiffQuery.getFullThreadDiff", | ||
| 150 | )(function* (input) { | ||
| 151 | const operation = "CheckpointDiffQuery.getFullThreadDiff"; | ||
| 152 | const ignoreWhitespace = input.ignoreWhitespace ?? true; | ||
| 153 | yield* Effect.annotateCurrentSpan({ | ||
| 154 | "checkpoint.thread_id": input.threadId, | ||
| 155 | "checkpoint.from_turn_count": 0, | ||
| 156 | "checkpoint.to_turn_count": input.toTurnCount, | ||
| 157 | "checkpoint.ignore_whitespace": ignoreWhitespace, | ||
| 158 | "checkpoint.diff_kind": "full-thread", | ||
| 159 | }); | ||
| 160 | |||
| 161 | if (input.toTurnCount === 0) { | ||
| 162 | const emptyDiff = buildTurnDiffResult( | ||
| 163 | { | ||
| 164 | threadId: input.threadId, | ||
| 165 | fromTurnCount: 0, | ||
| 166 | toTurnCount: 0, | ||
| 167 | }, | ||
| 168 | "", | ||
| 169 | ); | ||
| 170 | if (!isTurnDiffResult(emptyDiff)) { | ||
| 171 | return yield* new CheckpointInvariantError({ | ||
| 172 | operation, | ||
| 173 | detail: "Computed full thread diff result does not satisfy contract schema.", | ||
| 174 | }); | ||
| 175 | } | ||
| 176 | return emptyDiff satisfies OrchestrationGetFullThreadDiffResult; | ||
| 177 | } | ||
| 178 | |||
| 179 | const threadContext = yield* projectionSnapshotQuery | ||
| 180 | .getFullThreadDiffContext(input.threadId, input.toTurnCount) | ||
| 181 | .pipe(Effect.withSpan("checkpoint.fullThread.lookupContext")); | ||
| 182 | |||
| 183 | if (Option.isNone(threadContext)) { | ||
| 184 | return yield* new CheckpointInvariantError({ | ||
| 185 | operation, | ||
| 186 | detail: `Thread '${input.threadId}' not found.`, | ||
| 187 | }); | ||
| 188 | } | ||
| 189 | |||
| 190 | if (input.toTurnCount > threadContext.value.latestCheckpointTurnCount) { | ||
| 191 | return yield* new CheckpointUnavailableError({ | ||
| 192 | threadId: input.threadId, | ||
| 193 | turnCount: input.toTurnCount, | ||
| 194 | detail: `Turn diff range exceeds current turn count: requested ${input.toTurnCount}, current ${threadContext.value.latestCheckpointTurnCount}.`, | ||
| 195 | }); | ||
| 196 | } | ||
| 197 | |||
| 198 | const workspaceCwd = threadContext.value.worktreePath ?? threadContext.value.workspaceRoot; | ||
| 199 | if (!workspaceCwd) { | ||
| 200 | return yield* new CheckpointInvariantError({ | ||
| 201 | operation, | ||
| 202 | detail: `Workspace path missing for thread '${input.threadId}' when computing full thread diff.`, | ||
| 203 | }); | ||
| 204 | } | ||
| 205 | |||
| 206 | if (!threadContext.value.toCheckpointRef) { | ||
| 207 | return yield* new CheckpointUnavailableError({ | ||
| 208 | threadId: input.threadId, | ||
| 209 | turnCount: input.toTurnCount, | ||
| 210 | detail: `Checkpoint ref is unavailable for turn ${input.toTurnCount}.`, | ||
| 211 | }); | ||
| 212 | } | ||
| 213 | |||
| 214 | const diff = yield* checkpointStore | ||
| 215 | .diffCheckpoints({ | ||
| 216 | cwd: workspaceCwd, | ||
| 217 | fromCheckpointRef: checkpointRefForThreadTurn(input.threadId, 0), | ||
| 218 | toCheckpointRef: threadContext.value.toCheckpointRef as CheckpointRef, | ||
| 219 | fallbackFromToHead: false, | ||
| 220 | ignoreWhitespace, | ||
| 221 | }) | ||
| 222 | .pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints")); | ||
| 223 | |||
| 224 | const turnDiff = buildTurnDiffResult( | ||
| 225 | { | ||
| 226 | threadId: input.threadId, | ||
| 227 | fromTurnCount: 0, | ||
| 228 | toTurnCount: input.toTurnCount, | ||
| 229 | }, | ||
| 230 | diff, | ||
| 231 | ); | ||
| 232 | if (!isTurnDiffResult(turnDiff)) { | ||
| 233 | return yield* new CheckpointInvariantError({ | ||
| 234 | operation, | ||
| 235 | detail: "Computed full thread diff result does not satisfy contract schema.", | ||
| 236 | }); | ||
| 237 | } | ||
| 238 | |||
| 239 | return turnDiff satisfies OrchestrationGetFullThreadDiffResult; | ||
| 240 | }); | ||
| 241 | |||
| 242 | return { | ||
| 243 | getTurnDiff, | ||
| 244 | getFullThreadDiff, | ||
| 245 | } satisfies CheckpointDiffQueryShape; | ||
| 246 | }); | ||
| 247 | |||
| 248 | export const CheckpointDiffQueryLive = Layer.effect(CheckpointDiffQuery, make); | ||
apps/server/src/checkpointing/Layers/CheckpointStore.test.ts deleted-206| ... | @@ -1,206 +0,0 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | import path from "node:path"; | ||
| 3 | |||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 5 | import { it } from "@effect/vitest"; | ||
| 6 | import * as Effect from "effect/Effect"; | ||
| 7 | import * as FileSystem from "effect/FileSystem"; | ||
| 8 | import * as Layer from "effect/Layer"; | ||
| 9 | import * as PlatformError from "effect/PlatformError"; | ||
| 10 | import * as Scope from "effect/Scope"; | ||
| 11 | import { describe, expect } from "vite-plus/test"; | ||
| 12 | |||
| 13 | import { checkpointRefForThreadTurn } from "../Utils.ts"; | ||
| 14 | import { CheckpointStoreLive } from "./CheckpointStore.ts"; | ||
| 15 | import { CheckpointStore } from "../Services/CheckpointStore.ts"; | ||
| 16 | import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts"; | ||
| 17 | import * as VcsProcess from "../../vcs/VcsProcess.ts"; | ||
| 18 | import type { VcsError } from "@t3tools/contracts"; | ||
| 19 | import { ServerConfig } from "../../config.ts"; | ||
| 20 | import { ThreadId } from "@t3tools/contracts"; | ||
| 21 | |||
| 22 | const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { | ||
| 23 | prefix: "t3-checkpoint-store-test-", | ||
| 24 | }); | ||
| 25 | const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer)); | ||
| 26 | const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer)); | ||
| 27 | const CheckpointStoreTestLayer = CheckpointStoreLive.pipe( | ||
| 28 | Layer.provideMerge(VcsDriverTestLayer), | ||
| 29 | Layer.provideMerge(NodeServices.layer), | ||
| 30 | ); | ||
| 31 | const TestLayer = CheckpointStoreTestLayer.pipe( | ||
| 32 | Layer.provideMerge(VcsProcessTestLayer), | ||
| 33 | Layer.provideMerge(VcsDriverTestLayer), | ||
| 34 | Layer.provideMerge(ServerConfigLayer), | ||
| 35 | Layer.provideMerge(NodeServices.layer), | ||
| 36 | ); | ||
| 37 | |||
| 38 | function makeTmpDir( | ||
| 39 | prefix = "checkpoint-store-test-", | ||
| 40 | ): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> { | ||
| 41 | return Effect.gen(function* () { | ||
| 42 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 43 | return yield* fileSystem.makeTempDirectoryScoped({ prefix }); | ||
| 44 | }); | ||
| 45 | } | ||
| 46 | |||
| 47 | function writeTextFile( | ||
| 48 | filePath: string, | ||
| 49 | contents: string, | ||
| 50 | ): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> { | ||
| 51 | return Effect.gen(function* () { | ||
| 52 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 53 | yield* fileSystem.writeFileString(filePath, contents); | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | |||
| 57 | function git( | ||
| 58 | cwd: string, | ||
| 59 | args: ReadonlyArray<string>, | ||
| 60 | ): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> { | ||
| 61 | return Effect.gen(function* () { | ||
| 62 | const process = yield* VcsProcess.VcsProcess; | ||
| 63 | const result = yield* process.run({ | ||
| 64 | operation: "CheckpointStore.test.git", | ||
| 65 | command: "git", | ||
| 66 | cwd, | ||
| 67 | args, | ||
| 68 | timeoutMs: 10_000, | ||
| 69 | }); | ||
| 70 | return result.stdout.trim(); | ||
| 71 | }); | ||
| 72 | } | ||
| 73 | |||
| 74 | function initRepoWithCommit( | ||
| 75 | cwd: string, | ||
| 76 | ): Effect.Effect< | ||
| 77 | void, | ||
| 78 | VcsError | PlatformError.PlatformError, | ||
| 79 | VcsProcess.VcsProcess | FileSystem.FileSystem | ||
| 80 | > { | ||
| 81 | return Effect.gen(function* () { | ||
| 82 | yield* git(cwd, ["init"]); | ||
| 83 | yield* git(cwd, ["config", "user.email", "test@test.com"]); | ||
| 84 | yield* git(cwd, ["config", "user.name", "Test"]); | ||
| 85 | yield* writeTextFile(path.join(cwd, "README.md"), "# test\n"); | ||
| 86 | yield* git(cwd, ["add", "."]); | ||
| 87 | yield* git(cwd, ["commit", "-m", "initial commit"]); | ||
| 88 | }); | ||
| 89 | } | ||
| 90 | |||
| 91 | function buildLargeText(lineCount = 5_000): string { | ||
| 92 | return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`) | ||
| 93 | .join("\n") | ||
| 94 | .concat("\n"); | ||
| 95 | } | ||
| 96 | |||
| 97 | it.layer(TestLayer)("CheckpointStoreLive", (it) => { | ||
| 98 | describe("diffCheckpoints", () => { | ||
| 99 | it.effect("returns full oversized checkpoint diffs without truncation", () => | ||
| 100 | Effect.gen(function* () { | ||
| 101 | const tmp = yield* makeTmpDir(); | ||
| 102 | yield* initRepoWithCommit(tmp); | ||
| 103 | const checkpointStore = yield* CheckpointStore; | ||
| 104 | const threadId = ThreadId.make("thread-checkpoint-store"); | ||
| 105 | const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 106 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 107 | |||
| 108 | yield* checkpointStore.captureCheckpoint({ | ||
| 109 | cwd: tmp, | ||
| 110 | checkpointRef: fromCheckpointRef, | ||
| 111 | }); | ||
| 112 | yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText()); | ||
| 113 | yield* checkpointStore.captureCheckpoint({ | ||
| 114 | cwd: tmp, | ||
| 115 | checkpointRef: toCheckpointRef, | ||
| 116 | }); | ||
| 117 | |||
| 118 | const diff = yield* checkpointStore.diffCheckpoints({ | ||
| 119 | cwd: tmp, | ||
| 120 | fromCheckpointRef, | ||
| 121 | toCheckpointRef, | ||
| 122 | ignoreWhitespace: true, | ||
| 123 | }); | ||
| 124 | |||
| 125 | expect(diff).toContain("diff --git"); | ||
| 126 | expect(diff).not.toContain("[truncated]"); | ||
| 127 | expect(diff).toContain("+line 04999"); | ||
| 128 | }), | ||
| 129 | ); | ||
| 130 | |||
| 131 | it.effect("can hide indentation churn when changes wrap existing lines", () => | ||
| 132 | Effect.gen(function* () { | ||
| 133 | const tmp = yield* makeTmpDir(); | ||
| 134 | yield* initRepoWithCommit(tmp); | ||
| 135 | const checkpointStore = yield* CheckpointStore; | ||
| 136 | const threadId = ThreadId.make("thread-checkpoint-store-whitespace"); | ||
| 137 | const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 138 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 139 | |||
| 140 | const componentPath = path.join(tmp, "Component.tsx"); | ||
| 141 | yield* writeTextFile( | ||
| 142 | componentPath, | ||
| 143 | [ | ||
| 144 | "export function View() {", | ||
| 145 | " return (", | ||
| 146 | " <section>", | ||
| 147 | " <h1>Title</h1>", | ||
| 148 | " <p>Body</p>", | ||
| 149 | " </section>", | ||
| 150 | " );", | ||
| 151 | "}", | ||
| 152 | "", | ||
| 153 | ].join("\n"), | ||
| 154 | ); | ||
| 155 | yield* checkpointStore.captureCheckpoint({ | ||
| 156 | cwd: tmp, | ||
| 157 | checkpointRef: fromCheckpointRef, | ||
| 158 | }); | ||
| 159 | yield* writeTextFile( | ||
| 160 | componentPath, | ||
| 161 | [ | ||
| 162 | "export function View() {", | ||
| 163 | " return (", | ||
| 164 | " <section>", | ||
| 165 | " {isReady ? (", | ||
| 166 | " <div>", | ||
| 167 | " <h1>Title</h1>", | ||
| 168 | " <p>Body</p>", | ||
| 169 | " </div>", | ||
| 170 | " ) : null}", | ||
| 171 | " </section>", | ||
| 172 | " );", | ||
| 173 | "}", | ||
| 174 | "", | ||
| 175 | ].join("\n"), | ||
| 176 | ); | ||
| 177 | yield* checkpointStore.captureCheckpoint({ | ||
| 178 | cwd: tmp, | ||
| 179 | checkpointRef: toCheckpointRef, | ||
| 180 | }); | ||
| 181 | |||
| 182 | const normalDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 183 | cwd: tmp, | ||
| 184 | fromCheckpointRef, | ||
| 185 | toCheckpointRef, | ||
| 186 | ignoreWhitespace: false, | ||
| 187 | }); | ||
| 188 | const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 189 | cwd: tmp, | ||
| 190 | fromCheckpointRef, | ||
| 191 | toCheckpointRef, | ||
| 192 | ignoreWhitespace: true, | ||
| 193 | }); | ||
| 194 | |||
| 195 | expect(normalDiff).toContain("diff --git"); | ||
| 196 | expect(normalDiff).toContain("- <h1>Title</h1>"); | ||
| 197 | expect(normalDiff).toContain("+ <h1>Title</h1>"); | ||
| 198 | expect(whitespaceIgnoredDiff).toContain("diff --git"); | ||
| 199 | expect(whitespaceIgnoredDiff).toContain("+ {isReady ? ("); | ||
| 200 | expect(whitespaceIgnoredDiff).toContain("+ <div>"); | ||
| 201 | expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>"); | ||
| 202 | expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>"); | ||
| 203 | }), | ||
| 204 | ); | ||
| 205 | }); | ||
| 206 | }); | ||
apps/server/src/checkpointing/Layers/CheckpointStore.ts deleted-89| ... | @@ -1,89 +0,0 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointStoreLive - Filesystem checkpoint store adapter layer. | ||
| 3 | * | ||
| 4 | * Resolves the active VCS driver once per checkpoint operation and delegates | ||
| 5 | * checkpoint-specific behavior to the driver's optional checkpoint capability. | ||
| 6 | * | ||
| 7 | * @module CheckpointStoreLive | ||
| 8 | */ | ||
| 9 | import * as Effect from "effect/Effect"; | ||
| 10 | import * as Layer from "effect/Layer"; | ||
| 11 | |||
| 12 | import { CheckpointStore, type CheckpointStoreShape } from "../Services/CheckpointStore.ts"; | ||
| 13 | import { VcsUnsupportedOperationError } from "@t3tools/contracts"; | ||
| 14 | import { VcsDriverRegistry } from "../../vcs/VcsDriverRegistry.ts"; | ||
| 15 | import type { VcsCheckpointOps } from "../../vcs/VcsDriver.ts"; | ||
| 16 | |||
| 17 | const makeCheckpointStore = Effect.gen(function* () { | ||
| 18 | const vcsRegistry = yield* VcsDriverRegistry; | ||
| 19 | |||
| 20 | const resolveCheckpoints = Effect.fn("CheckpointStore.resolveCheckpoints")(function* ( | ||
| 21 | operation: string, | ||
| 22 | cwd: string, | ||
| 23 | ) { | ||
| 24 | const handle = yield* vcsRegistry.resolve({ cwd }); | ||
| 25 | if (!handle.driver.checkpoints) { | ||
| 26 | return yield* new VcsUnsupportedOperationError({ | ||
| 27 | operation, | ||
| 28 | kind: handle.kind, | ||
| 29 | detail: `${handle.kind} driver does not implement checkpoint operations.`, | ||
| 30 | }); | ||
| 31 | } | ||
| 32 | return handle.driver.checkpoints satisfies VcsCheckpointOps; | ||
| 33 | }); | ||
| 34 | |||
| 35 | const isGitRepository: CheckpointStoreShape["isGitRepository"] = (cwd) => | ||
| 36 | vcsRegistry.resolve({ cwd, requestedKind: "git" }).pipe( | ||
| 37 | Effect.map(() => true), | ||
| 38 | Effect.orElseSucceed(() => false), | ||
| 39 | ); | ||
| 40 | |||
| 41 | const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = Effect.fn( | ||
| 42 | "captureCheckpoint", | ||
| 43 | )(function* (input) { | ||
| 44 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.captureCheckpoint", input.cwd); | ||
| 45 | return yield* checkpoints.captureCheckpoint(input); | ||
| 46 | }); | ||
| 47 | |||
| 48 | const hasCheckpointRef: CheckpointStoreShape["hasCheckpointRef"] = Effect.fn("hasCheckpointRef")( | ||
| 49 | function* (input) { | ||
| 50 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.hasCheckpointRef", input.cwd); | ||
| 51 | return yield* checkpoints.hasCheckpointRef(input); | ||
| 52 | }, | ||
| 53 | ); | ||
| 54 | |||
| 55 | const restoreCheckpoint: CheckpointStoreShape["restoreCheckpoint"] = Effect.fn( | ||
| 56 | "restoreCheckpoint", | ||
| 57 | )(function* (input) { | ||
| 58 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.restoreCheckpoint", input.cwd); | ||
| 59 | return yield* checkpoints.restoreCheckpoint(input); | ||
| 60 | }); | ||
| 61 | |||
| 62 | const diffCheckpoints: CheckpointStoreShape["diffCheckpoints"] = Effect.fn("diffCheckpoints")( | ||
| 63 | function* (input) { | ||
| 64 | const checkpoints = yield* resolveCheckpoints("CheckpointStore.diffCheckpoints", input.cwd); | ||
| 65 | return yield* checkpoints.diffCheckpoints(input); | ||
| 66 | }, | ||
| 67 | ); | ||
| 68 | |||
| 69 | const deleteCheckpointRefs: CheckpointStoreShape["deleteCheckpointRefs"] = Effect.fn( | ||
| 70 | "deleteCheckpointRefs", | ||
| 71 | )(function* (input) { | ||
| 72 | const checkpoints = yield* resolveCheckpoints( | ||
| 73 | "CheckpointStore.deleteCheckpointRefs", | ||
| 74 | input.cwd, | ||
| 75 | ); | ||
| 76 | return yield* checkpoints.deleteCheckpointRefs(input); | ||
| 77 | }); | ||
| 78 | |||
| 79 | return { | ||
| 80 | isGitRepository, | ||
| 81 | captureCheckpoint, | ||
| 82 | hasCheckpointRef, | ||
| 83 | restoreCheckpoint, | ||
| 84 | diffCheckpoints, | ||
| 85 | deleteCheckpointRefs, | ||
| 86 | } satisfies CheckpointStoreShape; | ||
| 87 | }); | ||
| 88 | |||
| 89 | export const CheckpointStoreLive = Layer.effect(CheckpointStore, makeCheckpointStore); | ||
apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts deleted-49| ... | @@ -1,49 +0,0 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointDiffQuery - Query interface for computed checkpoint diffs. | ||
| 3 | * | ||
| 4 | * Provides read-only diff operations across checkpoint snapshots used by | ||
| 5 | * orchestration APIs. | ||
| 6 | * | ||
| 7 | * @module CheckpointDiffQuery | ||
| 8 | */ | ||
| 9 | import type { | ||
| 10 | OrchestrationGetFullThreadDiffInput, | ||
| 11 | OrchestrationGetFullThreadDiffResult, | ||
| 12 | OrchestrationGetTurnDiffInput, | ||
| 13 | OrchestrationGetTurnDiffResult, | ||
| 14 | } from "@t3tools/contracts"; | ||
| 15 | import * as Context from "effect/Context"; | ||
| 16 | import type * as Effect from "effect/Effect"; | ||
| 17 | |||
| 18 | import type { CheckpointServiceError } from "../Errors.ts"; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * CheckpointDiffQueryShape - Service API for checkpoint diff queries. | ||
| 22 | */ | ||
| 23 | export interface CheckpointDiffQueryShape { | ||
| 24 | /** | ||
| 25 | * Read the patch diff for a single turn checkpoint transition. | ||
| 26 | * | ||
| 27 | * Verifies checkpoint availability in both projection state and filesystem. | ||
| 28 | */ | ||
| 29 | readonly getTurnDiff: ( | ||
| 30 | input: OrchestrationGetTurnDiffInput, | ||
| 31 | ) => Effect.Effect<OrchestrationGetTurnDiffResult, CheckpointServiceError>; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Read the full patch diff across a thread range of checkpoints. | ||
| 35 | * | ||
| 36 | * Delegates to turn diff with `fromTurnCount = 0`. | ||
| 37 | */ | ||
| 38 | readonly getFullThreadDiff: ( | ||
| 39 | input: OrchestrationGetFullThreadDiffInput, | ||
| 40 | ) => Effect.Effect<OrchestrationGetFullThreadDiffResult, CheckpointServiceError>; | ||
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * CheckpointDiffQuery - Service tag for checkpoint diff queries. | ||
| 45 | */ | ||
| 46 | export class CheckpointDiffQuery extends Context.Service< | ||
| 47 | CheckpointDiffQuery, | ||
| 48 | CheckpointDiffQueryShape | ||
| 49 | >()("t3/checkpointing/Services/CheckpointDiffQuery") {} | ||
apps/server/src/checkpointing/Services/CheckpointStore.ts deleted-101| ... | @@ -1,101 +0,0 @@ | ||
| 1 | /** | ||
| 2 | * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints. | ||
| 3 | * | ||
| 4 | * Owns hidden Git-ref checkpoint capture/restore and diff computation for a | ||
| 5 | * workspace thread timeline. It does not store user-facing checkpoint metadata | ||
| 6 | * and does not coordinate provider conversation rollback. | ||
| 7 | * | ||
| 8 | * Uses Effect `Context.Service` for dependency injection and exposes typed | ||
| 9 | * domain errors for checkpoint storage operations. | ||
| 10 | * | ||
| 11 | * @module CheckpointStore | ||
| 12 | */ | ||
| 13 | import * as Context from "effect/Context"; | ||
| 14 | import type * as Effect from "effect/Effect"; | ||
| 15 | |||
| 16 | import type { CheckpointStoreError } from "../Errors.ts"; | ||
| 17 | import { CheckpointRef } from "@t3tools/contracts"; | ||
| 18 | |||
| 19 | export interface CaptureCheckpointInput { | ||
| 20 | readonly cwd: string; | ||
| 21 | readonly checkpointRef: CheckpointRef; | ||
| 22 | } | ||
| 23 | |||
| 24 | export interface RestoreCheckpointInput { | ||
| 25 | readonly cwd: string; | ||
| 26 | readonly checkpointRef: CheckpointRef; | ||
| 27 | readonly fallbackToHead?: boolean; | ||
| 28 | } | ||
| 29 | |||
| 30 | export interface DiffCheckpointsInput { | ||
| 31 | readonly cwd: string; | ||
| 32 | readonly fromCheckpointRef: CheckpointRef; | ||
| 33 | readonly toCheckpointRef: CheckpointRef; | ||
| 34 | readonly fallbackFromToHead?: boolean; | ||
| 35 | readonly ignoreWhitespace: boolean; | ||
| 36 | } | ||
| 37 | |||
| 38 | export interface DeleteCheckpointRefsInput { | ||
| 39 | readonly cwd: string; | ||
| 40 | readonly checkpointRefs: ReadonlyArray<CheckpointRef>; | ||
| 41 | } | ||
| 42 | |||
| 43 | /** | ||
| 44 | * CheckpointStoreShape - Service API for checkpoint capture/restore and diff access. | ||
| 45 | */ | ||
| 46 | export interface CheckpointStoreShape { | ||
| 47 | /** | ||
| 48 | * Check whether cwd is inside a Git worktree. | ||
| 49 | */ | ||
| 50 | readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Capture a checkpoint commit and store it at the provided checkpoint ref. | ||
| 54 | * | ||
| 55 | * Uses an isolated temporary Git index and writes a hidden ref. | ||
| 56 | */ | ||
| 57 | readonly captureCheckpoint: ( | ||
| 58 | input: CaptureCheckpointInput, | ||
| 59 | ) => Effect.Effect<void, CheckpointStoreError>; | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Check whether a checkpoint ref exists. | ||
| 63 | */ | ||
| 64 | readonly hasCheckpointRef: ( | ||
| 65 | input: Omit<RestoreCheckpointInput, "fallbackToHead">, | ||
| 66 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 67 | |||
| 68 | /** | ||
| 69 | * Restore workspace/staging state to a checkpoint. | ||
| 70 | * | ||
| 71 | * Optionally falls back to current `HEAD` when the checkpoint ref is missing. | ||
| 72 | */ | ||
| 73 | readonly restoreCheckpoint: ( | ||
| 74 | input: RestoreCheckpointInput, | ||
| 75 | ) => Effect.Effect<boolean, CheckpointStoreError>; | ||
| 76 | |||
| 77 | /** | ||
| 78 | * Compute patch diff between two checkpoint refs. | ||
| 79 | * | ||
| 80 | * Can optionally treat missing "from" ref as `HEAD`. | ||
| 81 | */ | ||
| 82 | readonly diffCheckpoints: ( | ||
| 83 | input: DiffCheckpointsInput, | ||
| 84 | ) => Effect.Effect<string, CheckpointStoreError>; | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Delete the provided checkpoint refs. | ||
| 88 | * | ||
| 89 | * Best-effort delete: missing refs are tolerated. | ||
| 90 | */ | ||
| 91 | readonly deleteCheckpointRefs: ( | ||
| 92 | input: DeleteCheckpointRefsInput, | ||
| 93 | ) => Effect.Effect<void, CheckpointStoreError>; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * CheckpointStore - Service tag for checkpoint persistence and restore operations. | ||
| 98 | */ | ||
| 99 | export class CheckpointStore extends Context.Service<CheckpointStore, CheckpointStoreShape>()( | ||
| 100 | "t3/checkpointing/Services/CheckpointStore", | ||
| 101 | ) {} | ||
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+9-5| ... | @@ -30,8 +30,7 @@ import * as Scope from "effect/Scope"; | ... | @@ -30,8 +30,7 @@ import * as Scope from "effect/Scope"; |
| 30 | import * as Stream from "effect/Stream"; | 30 | import * as Stream from "effect/Stream"; |
| 31 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | 31 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 32 | 32 | ||
| 33 | import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts"; | 33 | import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts"; |
| 34 | import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts"; | ||
| 35 | import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts"; | 34 | import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts"; |
| 36 | import * as VcsProcess from "../../vcs/VcsProcess.ts"; | 35 | import * as VcsProcess from "../../vcs/VcsProcess.ts"; |
| 37 | import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; | 36 | import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts"; |
| ... | @@ -247,7 +246,10 @@ async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) | ... | @@ -247,7 +246,10 @@ async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) |
| 247 | 246 | ||
| 248 | describe("CheckpointReactor", () => { | 247 | describe("CheckpointReactor", () => { |
| 249 | let runtime: ManagedRuntime.ManagedRuntime< | 248 | let runtime: ManagedRuntime.ManagedRuntime< |
| 250 | OrchestrationEngineService | CheckpointReactor | CheckpointStore | ProjectionSnapshotQuery, | 249 | | OrchestrationEngineService |
| 250 | | CheckpointReactor | ||
| 251 | | CheckpointStore.CheckpointStore | ||
| 252 | | ProjectionSnapshotQuery, | ||
| 251 | unknown | 253 | unknown |
| 252 | > | null = null; | 254 | > | null = null; |
| 253 | let scope: Scope.Closeable | null = null; | 255 | let scope: Scope.Closeable | null = null; |
| ... | @@ -328,7 +330,7 @@ describe("CheckpointReactor", () => { | ... | @@ -328,7 +330,7 @@ describe("CheckpointReactor", () => { |
| 328 | Layer.provideMerge(RuntimeReceiptBusLive), | 330 | Layer.provideMerge(RuntimeReceiptBusLive), |
| 329 | Layer.provideMerge(Layer.succeed(ProviderService, provider.service)), | 331 | Layer.provideMerge(Layer.succeed(ProviderService, provider.service)), |
| 330 | Layer.provideMerge(vcsStatusBroadcasterLayer), | 332 | Layer.provideMerge(vcsStatusBroadcasterLayer), |
| 331 | Layer.provideMerge(CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistry.layer))), | 333 | Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistry.layer))), |
| 332 | Layer.provideMerge( | 334 | Layer.provideMerge( |
| 333 | WorkspaceEntries.layer.pipe( | 335 | WorkspaceEntries.layer.pipe( |
| 334 | Layer.provide(WorkspacePathsLive), | 336 | Layer.provide(WorkspacePathsLive), |
| ... | @@ -345,7 +347,9 @@ describe("CheckpointReactor", () => { | ... | @@ -345,7 +347,9 @@ describe("CheckpointReactor", () => { |
| 345 | const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); | 347 | const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); |
| 346 | const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); | 348 | const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); |
| 347 | const reactor = await runtime.runPromise(Effect.service(CheckpointReactor)); | 349 | const reactor = await runtime.runPromise(Effect.service(CheckpointReactor)); |
| 348 | const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore)); | 350 | const checkpointStore = await runtime.runPromise( |
| 351 | Effect.service(CheckpointStore.CheckpointStore), | ||
| 352 | ); | ||
| 349 | scope = await Effect.runPromise(Scope.make("sequential")); | 353 | scope = await Effect.runPromise(Scope.make("sequential")); |
| 350 | await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); | 354 | await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); |
| 351 | const drain = () => Effect.runPromise(reactor.drain); | 355 | const drain = () => Effect.runPromise(reactor.drain); |
apps/server/src/orchestration/Layers/CheckpointReactor.ts+2-2| ... | @@ -24,7 +24,7 @@ import { | ... | @@ -24,7 +24,7 @@ import { |
| 24 | checkpointRefForThreadTurn, | 24 | checkpointRefForThreadTurn, |
| 25 | resolveThreadWorkspaceCwd, | 25 | resolveThreadWorkspaceCwd, |
| 26 | } from "../../checkpointing/Utils.ts"; | 26 | } from "../../checkpointing/Utils.ts"; |
| 27 | import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts"; | 27 | import * as CheckpointStore from "../../checkpointing/CheckpointStore.ts"; |
| 28 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; | 28 | import { ProviderService } from "../../provider/Services/ProviderService.ts"; |
| 29 | import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts"; | 29 | import { CheckpointReactor, type CheckpointReactorShape } from "../Services/CheckpointReactor.ts"; |
| 30 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; | 30 | import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; |
| ... | @@ -81,7 +81,7 @@ const make = Effect.gen(function* () { | ... | @@ -81,7 +81,7 @@ const make = Effect.gen(function* () { |
| 81 | const orchestrationEngine = yield* OrchestrationEngineService; | 81 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 82 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; | 82 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; |
| 83 | const providerService = yield* ProviderService; | 83 | const providerService = yield* ProviderService; |
| 84 | const checkpointStore = yield* CheckpointStore; | 84 | const checkpointStore = yield* CheckpointStore.CheckpointStore; |
| 85 | const receiptBus = yield* RuntimeReceiptBus; | 85 | const receiptBus = yield* RuntimeReceiptBus; |
| 86 | const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; | 86 | const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; |
| 87 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; | 87 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; |
apps/server/src/server.test.ts+1-1| ... | @@ -71,7 +71,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); | ... | @@ -71,7 +71,7 @@ const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); |
| 71 | 71 | ||
| 72 | import * as ServerConfig from "./config.ts"; | 72 | import * as ServerConfig from "./config.ts"; |
| 73 | import { makeRoutesLayer } from "./server.ts"; | 73 | import { makeRoutesLayer } from "./server.ts"; |
| 74 | import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts"; | 74 | import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; |
| 75 | import * as GitManager from "./git/GitManager.ts"; | 75 | import * as GitManager from "./git/GitManager.ts"; |
| 76 | import * as Keybindings from "./keybindings.ts"; | 76 | import * as Keybindings from "./keybindings.ts"; |
| 77 | import * as ExternalLauncher from "./process/externalLauncher.ts"; | 77 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
apps/server/src/server.ts+4-4| ... | @@ -25,8 +25,8 @@ import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts | ... | @@ -25,8 +25,8 @@ import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts |
| 25 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; | 25 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; |
| 26 | import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts"; | 26 | import { ProviderSessionReaperLive } from "./provider/Layers/ProviderSessionReaper.ts"; |
| 27 | import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts"; | 27 | import * as OpenCodeRuntime from "./provider/opencodeRuntime.ts"; |
| 28 | import { CheckpointDiffQueryLive } from "./checkpointing/Layers/CheckpointDiffQuery.ts"; | 28 | import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; |
| 29 | import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore.ts"; | 29 | import * as CheckpointStore from "./checkpointing/CheckpointStore.ts"; |
| 30 | import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; | 30 | import * as AzureDevOpsCli from "./sourceControl/AzureDevOpsCli.ts"; |
| 31 | import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; | 31 | import * as BitbucketApi from "./sourceControl/BitbucketApi.ts"; |
| 32 | import * as GitHubCli from "./sourceControl/GitHubCli.ts"; | 32 | import * as GitHubCli from "./sourceControl/GitHubCli.ts"; |
| ... | @@ -232,8 +232,8 @@ const VcsLayerLive = Layer.empty.pipe( | ... | @@ -232,8 +232,8 @@ const VcsLayerLive = Layer.empty.pipe( |
| 232 | ); | 232 | ); |
| 233 | 233 | ||
| 234 | const CheckpointingLayerLive = Layer.empty.pipe( | 234 | const CheckpointingLayerLive = Layer.empty.pipe( |
| 235 | Layer.provideMerge(CheckpointDiffQueryLive), | 235 | Layer.provideMerge(CheckpointDiffQuery.layer), |
| 236 | Layer.provideMerge(CheckpointStoreLive.pipe(Layer.provide(VcsDriverRegistryLayerLive))), | 236 | Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))), |
| 237 | ); | 237 | ); |
| 238 | 238 | ||
| 239 | const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer)); | 239 | const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer)); |
apps/server/src/ws.ts+1-1| ... | @@ -56,7 +56,7 @@ import { clamp } from "effect/Number"; | ... | @@ -56,7 +56,7 @@ import { clamp } from "effect/Number"; |
| 56 | import { HttpRouter, HttpServerRequest, HttpServerRespondable } from "effect/unstable/http"; | 56 | import { HttpRouter, HttpServerRequest, HttpServerRespondable } from "effect/unstable/http"; |
| 57 | import { RpcSerialization, RpcServer } from "effect/unstable/rpc"; | 57 | import { RpcSerialization, RpcServer } from "effect/unstable/rpc"; |
| 58 | 58 | ||
| 59 | import * as CheckpointDiffQuery from "./checkpointing/Services/CheckpointDiffQuery.ts"; | 59 | import * as CheckpointDiffQuery from "./checkpointing/CheckpointDiffQuery.ts"; |
| 60 | import * as ServerConfig from "./config.ts"; | 60 | import * as ServerConfig from "./config.ts"; |
| 61 | import * as Keybindings from "./keybindings.ts"; | 61 | import * as Keybindings from "./keybindings.ts"; |
| 62 | import * as ExternalLauncher from "./process/externalLauncher.ts"; | 62 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
docs/operations/effect-fn-checklist.md+6-6| ... | @@ -130,12 +130,12 @@ Effect.fn("name")( | ... | @@ -130,12 +130,12 @@ Effect.fn("name")( |
| 130 | - [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555) | 130 | - [x] [listener](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/Layers/CodexAdapter.ts#L1555) |
| 131 | - [x] Remaining nested callback wrappers in this file | 131 | - [x] Remaining nested callback wrappers in this file |
| 132 | 132 | ||
| 133 | ### `apps/server/src/checkpointing/Layers/CheckpointStore.ts` (`10`) | 133 | ### `apps/server/src/checkpointing/CheckpointStore.ts` (`10`) |
| 134 | 134 | ||
| 135 | - [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L89) | 135 | - [ ] [captureCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L123) |
| 136 | - [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L183) | 136 | - [ ] [restoreCheckpoint](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L137) |
| 137 | - [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L220) | 137 | - [ ] [diffCheckpoints](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L144) |
| 138 | - [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointStore.ts#L252) | 138 | - [ ] [deleteCheckpointRefs](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointStore.ts#L151) |
| 139 | - [ ] Nested callback wrappers in this file | 139 | - [ ] Nested callback wrappers in this file |
| 140 | 140 | ||
| 141 | ### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`) | 141 | ### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`) |
| ... | @@ -190,7 +190,7 @@ Effect.fn("name")( | ... | @@ -190,7 +190,7 @@ Effect.fn("name")( |
| 190 | - [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`) | 190 | - [ ] [apps/server/src/persistence/Migrations.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/persistence/Migrations.ts) (`2`) |
| 191 | - [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`) | 191 | - [ ] [apps/server/src/open.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/open.ts) (`2`) |
| 192 | - [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) | 192 | - [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) |
| 193 | - [ ] [apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts) (`2`) | 193 | - [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`) |
| 194 | - [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`) | 194 | - [ ] [apps/server/src/provider/makeManagedServerProvider.ts](/Users/julius/Development/Work/codething-mvp/apps/server/src/provider/makeManagedServerProvider.ts) (`1`) |
| 195 | 195 | ||
| 196 | ``` | 196 | ``` |
docs/reference/encyclopedia.md+2-2| ... | @@ -172,8 +172,8 @@ The file patch and changed-file summary for one turn. It is usually computed in | ... | @@ -172,8 +172,8 @@ The file patch and changed-file summary for one turn. It is usually computed in |
| 172 | [16]: ./provider-architecture.md | 172 | [16]: ./provider-architecture.md |
| 173 | [17]: ../apps/server/src/provider/Layers/CodexAdapter.ts | 173 | [17]: ../apps/server/src/provider/Layers/CodexAdapter.ts |
| 174 | [18]: ./runtime-modes.md | 174 | [18]: ./runtime-modes.md |
| 175 | [19]: ../apps/server/src/checkpointing/Services/CheckpointStore.ts | 175 | [19]: ../apps/server/src/checkpointing/CheckpointStore.ts |
| 176 | [20]: ../apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts | 176 | [20]: ../apps/server/src/checkpointing/CheckpointDiffQuery.ts |
| 177 | [21]: ../apps/server/src/persistence/Services/ProjectionCheckpoints.ts | 177 | [21]: ../apps/server/src/persistence/Services/ProjectionCheckpoints.ts |
| 178 | [22]: ../apps/server/src/checkpointing/Utils.ts | 178 | [22]: ../apps/server/src/checkpointing/Utils.ts |
| 179 | [23]: ../apps/server/src/checkpointing/Diffs.ts | 179 | [23]: ../apps/server/src/checkpointing/Diffs.ts |
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts-1| ... | @@ -25,7 +25,6 @@ const LEGACY_BASELINE = new Map<string, number>([ | ... | @@ -25,7 +25,6 @@ const LEGACY_BASELINE = new Map<string, number>([ |
| 25 | ["apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts", 1], | 25 | ["apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts", 1], |
| 26 | ["apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts", 2], | 26 | ["apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts", 2], |
| 27 | ["apps/mobile/src/state/use-remote-environment-registry.test.ts", 2], | 27 | ["apps/mobile/src/state/use-remote-environment-registry.test.ts", 2], |
| 28 | ["apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts", 5], | ||
| 29 | ["apps/server/src/orchestration/commandInvariants.test.ts", 6], | 28 | ["apps/server/src/orchestration/commandInvariants.test.ts", 6], |
| 30 | ["apps/server/src/orchestration/Layers/CheckpointReactor.test.ts", 42], | 29 | ["apps/server/src/orchestration/Layers/CheckpointReactor.test.ts", 42], |
| 31 | ["apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts", 5], | 30 | ["apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts", 5], |