@git-stunts/git-warp 10.8.0 → 11.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,598 @@
1
+ import { summarizeOps } from '../../../src/visualization/renderers/ascii/history.js';
2
+ import { diffStates } from '../../../src/domain/services/StateDiff.js';
3
+ import {
4
+ buildCursorActiveRef,
5
+ buildCursorSavedRef,
6
+ buildCursorSavedPrefix,
7
+ } from '../../../src/domain/utils/RefLayout.js';
8
+ import { parseCursorBlob } from '../../../src/domain/utils/parseCursorBlob.js';
9
+ import { stableStringify } from '../../presenters/json.js';
10
+ import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infrastructure.js';
11
+ import { seekSchema } from '../schemas.js';
12
+ import { openGraph, readActiveCursor, writeActiveCursor, wireSeekCache } from '../shared.js';
13
+
14
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
15
+ /** @typedef {import('../types.js').Persistence} Persistence */
16
+ /** @typedef {import('../types.js').WarpGraphInstance} WarpGraphInstance */
17
+ /** @typedef {import('../types.js').WriterTickInfo} WriterTickInfo */
18
+ /** @typedef {import('../types.js').CursorBlob} CursorBlob */
19
+ /** @typedef {import('../types.js').SeekSpec} SeekSpec */
20
+ /** @typedef {import('../../../src/domain/services/StateDiff.js').StateDiffResult} StateDiffResult */
21
+
22
+ // ============================================================================
23
+ // Cursor I/O Helpers (seek-only)
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Removes the active seek cursor for a graph, returning to present state.
28
+ *
29
+ * @param {Persistence} persistence
30
+ * @param {string} graphName
31
+ * @returns {Promise<void>}
32
+ */
33
+ async function clearActiveCursor(persistence, graphName) {
34
+ const ref = buildCursorActiveRef(graphName);
35
+ const exists = await persistence.readRef(ref);
36
+ if (exists) {
37
+ await persistence.deleteRef(ref);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Reads a named saved cursor from Git ref storage.
43
+ *
44
+ * @param {Persistence} persistence
45
+ * @param {string} graphName
46
+ * @param {string} name
47
+ * @returns {Promise<CursorBlob|null>}
48
+ */
49
+ async function readSavedCursor(persistence, graphName, name) {
50
+ const ref = buildCursorSavedRef(graphName, name);
51
+ const oid = await persistence.readRef(ref);
52
+ if (!oid) {
53
+ return null;
54
+ }
55
+ const buf = await persistence.readBlob(oid);
56
+ return parseCursorBlob(buf, `saved cursor '${name}'`);
57
+ }
58
+
59
+ /**
60
+ * Persists a cursor under a named saved-cursor ref.
61
+ *
62
+ * @param {Persistence} persistence
63
+ * @param {string} graphName
64
+ * @param {string} name
65
+ * @param {CursorBlob} cursor
66
+ * @returns {Promise<void>}
67
+ */
68
+ async function writeSavedCursor(persistence, graphName, name, cursor) {
69
+ const ref = buildCursorSavedRef(graphName, name);
70
+ const json = JSON.stringify(cursor);
71
+ const oid = await persistence.writeBlob(Buffer.from(json, 'utf8'));
72
+ await persistence.updateRef(ref, oid);
73
+ }
74
+
75
+ /**
76
+ * Deletes a named saved cursor from Git ref storage.
77
+ *
78
+ * @param {Persistence} persistence
79
+ * @param {string} graphName
80
+ * @param {string} name
81
+ * @returns {Promise<void>}
82
+ */
83
+ async function deleteSavedCursor(persistence, graphName, name) {
84
+ const ref = buildCursorSavedRef(graphName, name);
85
+ const exists = await persistence.readRef(ref);
86
+ if (exists) {
87
+ await persistence.deleteRef(ref);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Lists all saved cursors for a graph.
93
+ *
94
+ * @param {Persistence} persistence
95
+ * @param {string} graphName
96
+ * @returns {Promise<Array<{name: string, tick: number, mode?: string}>>}
97
+ */
98
+ async function listSavedCursors(persistence, graphName) {
99
+ const prefix = buildCursorSavedPrefix(graphName);
100
+ const refs = await persistence.listRefs(prefix);
101
+ const cursors = [];
102
+ for (const ref of refs) {
103
+ const name = ref.slice(prefix.length);
104
+ if (name) {
105
+ const oid = await persistence.readRef(ref);
106
+ if (oid) {
107
+ const buf = await persistence.readBlob(oid);
108
+ const cursor = parseCursorBlob(buf, `saved cursor '${name}'`);
109
+ cursors.push({ name, ...cursor });
110
+ }
111
+ }
112
+ }
113
+ return cursors;
114
+ }
115
+
116
+ // ============================================================================
117
+ // Seek Arg Parser
118
+ // ============================================================================
119
+
120
+ const SEEK_OPTIONS = {
121
+ tick: { type: 'string' },
122
+ latest: { type: 'boolean', default: false },
123
+ save: { type: 'string' },
124
+ load: { type: 'string' },
125
+ list: { type: 'boolean', default: false },
126
+ drop: { type: 'string' },
127
+ 'clear-cache': { type: 'boolean', default: false },
128
+ 'no-persistent-cache': { type: 'boolean', default: false },
129
+ diff: { type: 'boolean', default: false },
130
+ 'diff-limit': { type: 'string', default: '2000' },
131
+ };
132
+
133
+ /**
134
+ * @param {string[]} args
135
+ * @returns {SeekSpec}
136
+ */
137
+ function parseSeekArgs(args) {
138
+ const { values } = parseCommandArgs(args, SEEK_OPTIONS, seekSchema);
139
+ return /** @type {SeekSpec} */ (values);
140
+ }
141
+
142
+ // ============================================================================
143
+ // Tick Resolution
144
+ // ============================================================================
145
+
146
+ /**
147
+ * @param {string} tickValue
148
+ * @param {number|null} currentTick
149
+ * @param {number[]} ticks
150
+ * @param {number} maxTick
151
+ * @returns {number}
152
+ */
153
+ function resolveTickValue(tickValue, currentTick, ticks, maxTick) {
154
+ if (tickValue.startsWith('+') || tickValue.startsWith('-')) {
155
+ const delta = parseInt(tickValue, 10);
156
+ if (!Number.isInteger(delta)) {
157
+ throw usageError(`Invalid tick delta: ${tickValue}`);
158
+ }
159
+ const base = currentTick ?? 0;
160
+ const allPoints = (ticks.length > 0 && ticks[0] === 0) ? [...ticks] : [0, ...ticks];
161
+ const currentIdx = allPoints.indexOf(base);
162
+ const startIdx = currentIdx === -1 ? 0 : currentIdx;
163
+ const targetIdx = Math.max(0, Math.min(allPoints.length - 1, startIdx + delta));
164
+ return allPoints[targetIdx];
165
+ }
166
+
167
+ const n = parseInt(tickValue, 10);
168
+ if (!Number.isInteger(n) || n < 0) {
169
+ throw usageError(`Invalid tick value: ${tickValue}. Must be a non-negative integer, or +N/-N for relative.`);
170
+ }
171
+ return Math.min(n, maxTick);
172
+ }
173
+
174
+ // ============================================================================
175
+ // Seek Helpers
176
+ // ============================================================================
177
+
178
+ /**
179
+ * @param {Map<string, WriterTickInfo>} perWriter
180
+ * @returns {Record<string, WriterTickInfo>}
181
+ */
182
+ function serializePerWriter(perWriter) {
183
+ /** @type {Record<string, WriterTickInfo>} */
184
+ const result = {};
185
+ for (const [writerId, info] of perWriter) {
186
+ result[writerId] = { ticks: info.ticks, tipSha: info.tipSha, tickShas: info.tickShas };
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * @param {number} tick
193
+ * @param {Map<string, WriterTickInfo>} perWriter
194
+ * @returns {number}
195
+ */
196
+ function countPatchesAtTick(tick, perWriter) {
197
+ let count = 0;
198
+ for (const [, info] of perWriter) {
199
+ for (const t of info.ticks) {
200
+ if (t <= tick) {
201
+ count++;
202
+ }
203
+ }
204
+ }
205
+ return count;
206
+ }
207
+
208
+ /**
209
+ * @param {Map<string, WriterTickInfo>} perWriter
210
+ * @returns {Promise<string>}
211
+ */
212
+ async function computeFrontierHash(perWriter) {
213
+ /** @type {Record<string, string|null>} */
214
+ const tips = {};
215
+ for (const [writerId, info] of perWriter) {
216
+ tips[writerId] = info?.tipSha || null;
217
+ }
218
+ const data = new TextEncoder().encode(stableStringify(tips));
219
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', data);
220
+ return Array.from(new Uint8Array(digest))
221
+ .map((b) => b.toString(16).padStart(2, '0'))
222
+ .join('');
223
+ }
224
+
225
+ /**
226
+ * @param {CursorBlob|null} cursor
227
+ * @returns {{nodes: number|null, edges: number|null}}
228
+ */
229
+ function readSeekCounts(cursor) {
230
+ if (!cursor || typeof cursor !== 'object') {
231
+ return { nodes: null, edges: null };
232
+ }
233
+ const nodes = typeof cursor.nodes === 'number' && Number.isFinite(cursor.nodes) ? cursor.nodes : null;
234
+ const edges = typeof cursor.edges === 'number' && Number.isFinite(cursor.edges) ? cursor.edges : null;
235
+ return { nodes, edges };
236
+ }
237
+
238
+ /**
239
+ * @param {CursorBlob|null} prevCursor
240
+ * @param {{nodes: number, edges: number}} next
241
+ * @param {string} frontierHash
242
+ * @returns {{nodes: number, edges: number}|null}
243
+ */
244
+ function computeSeekStateDiff(prevCursor, next, frontierHash) {
245
+ const prev = readSeekCounts(prevCursor);
246
+ if (prev.nodes === null || prev.edges === null) {
247
+ return null;
248
+ }
249
+ const prevFrontierHash = typeof prevCursor?.frontierHash === 'string' ? prevCursor.frontierHash : null;
250
+ if (!prevFrontierHash || prevFrontierHash !== frontierHash) {
251
+ return null;
252
+ }
253
+ return {
254
+ nodes: next.nodes - prev.nodes,
255
+ edges: next.edges - prev.edges,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * @param {{tick: number, perWriter: Map<string, WriterTickInfo>, graph: WarpGraphInstance}} params
261
+ * @returns {Promise<Record<string, {sha: string, opSummary: unknown}>|null>}
262
+ */
263
+ async function buildTickReceipt({ tick, perWriter, graph }) {
264
+ if (!Number.isInteger(tick) || tick <= 0) {
265
+ return null;
266
+ }
267
+
268
+ /** @type {Record<string, {sha: string, opSummary: unknown}>} */
269
+ const receipt = {};
270
+
271
+ for (const [writerId, info] of perWriter) {
272
+ const tickShas = /** @type {Record<number, string> | undefined} */ (info?.tickShas);
273
+ const sha = tickShas?.[tick];
274
+ if (!sha) {
275
+ continue;
276
+ }
277
+
278
+ const patch = await graph.loadPatchBySha(sha);
279
+ const ops = Array.isArray(patch?.ops) ? patch.ops : [];
280
+ receipt[writerId] = { sha, opSummary: summarizeOps(ops) };
281
+ }
282
+
283
+ return Object.keys(receipt).length > 0 ? receipt : null;
284
+ }
285
+
286
+ /**
287
+ * @param {{graph: WarpGraphInstance, prevTick: number|null, currentTick: number, diffLimit: number}} params
288
+ * @returns {Promise<{structuralDiff: unknown, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}>}
289
+ */
290
+ async function computeStructuralDiff({ graph, prevTick, currentTick, diffLimit }) {
291
+ let beforeState = null;
292
+ let diffBaseline = 'empty';
293
+ let baselineTick = null;
294
+
295
+ if (prevTick !== null && prevTick === currentTick) {
296
+ const empty = { nodes: { added: [], removed: [] }, edges: { added: [], removed: [] }, props: { set: [], removed: [] } };
297
+ return { structuralDiff: empty, diffBaseline: 'tick', baselineTick: prevTick, truncated: false, totalChanges: 0, shownChanges: 0 };
298
+ }
299
+
300
+ if (prevTick !== null && prevTick > 0) {
301
+ await graph.materialize({ ceiling: prevTick });
302
+ beforeState = await graph.getStateSnapshot();
303
+ diffBaseline = 'tick';
304
+ baselineTick = prevTick;
305
+ }
306
+
307
+ await graph.materialize({ ceiling: currentTick });
308
+ const afterState = await graph.getStateSnapshot();
309
+ if (!afterState) {
310
+ const empty = { nodes: { added: [], removed: [] }, edges: { added: [], removed: [] }, props: { set: [], removed: [] } };
311
+ return applyDiffLimit(empty, diffBaseline, baselineTick, diffLimit);
312
+ }
313
+ const diff = diffStates(beforeState, afterState);
314
+
315
+ return applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit);
316
+ }
317
+
318
+ /**
319
+ * @param {StateDiffResult} diff
320
+ * @param {string} diffBaseline
321
+ * @param {number|null} baselineTick
322
+ * @param {number} diffLimit
323
+ * @returns {{structuralDiff: StateDiffResult, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}}
324
+ */
325
+ function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
326
+ const totalChanges =
327
+ diff.nodes.added.length + diff.nodes.removed.length +
328
+ diff.edges.added.length + diff.edges.removed.length +
329
+ diff.props.set.length + diff.props.removed.length;
330
+
331
+ if (totalChanges <= diffLimit) {
332
+ return { structuralDiff: diff, diffBaseline, baselineTick, truncated: false, totalChanges, shownChanges: totalChanges };
333
+ }
334
+
335
+ let remaining = diffLimit;
336
+ const cap = (/** @type {unknown[]} */ arr) => {
337
+ const take = Math.min(arr.length, remaining);
338
+ remaining -= take;
339
+ return arr.slice(0, take);
340
+ };
341
+
342
+ const capped = {
343
+ nodes: { added: cap(diff.nodes.added), removed: cap(diff.nodes.removed) },
344
+ edges: { added: cap(diff.edges.added), removed: cap(diff.edges.removed) },
345
+ props: { set: cap(diff.props.set), removed: cap(diff.props.removed) },
346
+ };
347
+
348
+ const shownChanges = diffLimit - remaining;
349
+ return { structuralDiff: /** @type {StateDiffResult} */ (capped), diffBaseline, baselineTick, truncated: true, totalChanges, shownChanges };
350
+ }
351
+
352
+ // ============================================================================
353
+ // Seek Status Handler
354
+ // ============================================================================
355
+
356
+ /**
357
+ * @param {{graph: WarpGraphInstance, graphName: string, persistence: Persistence, activeCursor: CursorBlob|null, ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>, frontierHash: string}} params
358
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
359
+ */
360
+ async function handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash }) {
361
+ if (activeCursor) {
362
+ await graph.materialize({ ceiling: activeCursor.tick });
363
+ const nodes = await graph.getNodes();
364
+ const edges = await graph.getEdges();
365
+ const prevCounts = readSeekCounts(activeCursor);
366
+ const prevFrontierHash = typeof activeCursor.frontierHash === 'string' ? activeCursor.frontierHash : null;
367
+ if (prevCounts.nodes === null || prevCounts.edges === null || prevCounts.nodes !== nodes.length || prevCounts.edges !== edges.length || prevFrontierHash !== frontierHash) {
368
+ await writeActiveCursor(persistence, graphName, { tick: activeCursor.tick, mode: activeCursor.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
369
+ }
370
+ const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
371
+ const tickReceipt = await buildTickReceipt({ tick: activeCursor.tick, perWriter, graph });
372
+ return {
373
+ payload: {
374
+ graph: graphName,
375
+ action: 'status',
376
+ tick: activeCursor.tick,
377
+ maxTick,
378
+ ticks,
379
+ nodes: nodes.length,
380
+ edges: edges.length,
381
+ perWriter: serializePerWriter(perWriter),
382
+ patchCount: countPatchesAtTick(activeCursor.tick, perWriter),
383
+ diff,
384
+ tickReceipt,
385
+ cursor: { active: true, mode: activeCursor.mode, tick: activeCursor.tick, maxTick, name: 'active' },
386
+ },
387
+ exitCode: EXIT_CODES.OK,
388
+ };
389
+ }
390
+ await graph.materialize();
391
+ const nodes = await graph.getNodes();
392
+ const edges = await graph.getEdges();
393
+ const tickReceipt = await buildTickReceipt({ tick: maxTick, perWriter, graph });
394
+ return {
395
+ payload: {
396
+ graph: graphName,
397
+ action: 'status',
398
+ tick: maxTick,
399
+ maxTick,
400
+ ticks,
401
+ nodes: nodes.length,
402
+ edges: edges.length,
403
+ perWriter: serializePerWriter(perWriter),
404
+ patchCount: countPatchesAtTick(maxTick, perWriter),
405
+ diff: null,
406
+ tickReceipt,
407
+ cursor: { active: false },
408
+ },
409
+ exitCode: EXIT_CODES.OK,
410
+ };
411
+ }
412
+
413
+ // ============================================================================
414
+ // Main Seek Handler
415
+ // ============================================================================
416
+
417
+ /**
418
+ * Handles the `git warp seek` command across all sub-actions.
419
+ * @param {{options: CliOptions, args: string[]}} params
420
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
421
+ */
422
+ export default async function handleSeek({ options, args }) {
423
+ const seekSpec = parseSeekArgs(args);
424
+ const { graph, graphName, persistence } = await openGraph(options);
425
+ void wireSeekCache({ graph, persistence, graphName, seekSpec });
426
+
427
+ // Handle --clear-cache before discovering ticks (no materialization needed)
428
+ if (seekSpec.action === 'clear-cache') {
429
+ if (graph.seekCache) {
430
+ await graph.seekCache.clear();
431
+ }
432
+ return {
433
+ payload: { graph: graphName, action: 'clear-cache', message: 'Seek cache cleared.' },
434
+ exitCode: EXIT_CODES.OK,
435
+ };
436
+ }
437
+
438
+ const activeCursor = await readActiveCursor(persistence, graphName);
439
+ const { ticks, maxTick, perWriter } = await graph.discoverTicks();
440
+ const frontierHash = await computeFrontierHash(perWriter);
441
+ if (seekSpec.action === 'list') {
442
+ const saved = await listSavedCursors(persistence, graphName);
443
+ return {
444
+ payload: {
445
+ graph: graphName,
446
+ action: 'list',
447
+ cursors: saved,
448
+ activeTick: activeCursor ? activeCursor.tick : null,
449
+ maxTick,
450
+ },
451
+ exitCode: EXIT_CODES.OK,
452
+ };
453
+ }
454
+ if (seekSpec.action === 'drop') {
455
+ const dropName = /** @type {string} */ (seekSpec.name);
456
+ const existing = await readSavedCursor(persistence, graphName, dropName);
457
+ if (!existing) {
458
+ throw notFoundError(`Saved cursor not found: ${dropName}`);
459
+ }
460
+ await deleteSavedCursor(persistence, graphName, dropName);
461
+ return {
462
+ payload: {
463
+ graph: graphName,
464
+ action: 'drop',
465
+ name: seekSpec.name,
466
+ tick: existing.tick,
467
+ },
468
+ exitCode: EXIT_CODES.OK,
469
+ };
470
+ }
471
+ if (seekSpec.action === 'latest') {
472
+ const prevTick = activeCursor ? activeCursor.tick : null;
473
+ let sdResult = null;
474
+ if (seekSpec.diff) {
475
+ sdResult = await computeStructuralDiff({ graph, prevTick, currentTick: maxTick, diffLimit: seekSpec.diffLimit });
476
+ }
477
+ await clearActiveCursor(persistence, graphName);
478
+ // When --diff already materialized at maxTick, skip redundant re-materialize
479
+ if (!sdResult) {
480
+ await graph.materialize({ ceiling: maxTick });
481
+ }
482
+ const nodes = await graph.getNodes();
483
+ const edges = await graph.getEdges();
484
+ const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
485
+ const tickReceipt = await buildTickReceipt({ tick: maxTick, perWriter, graph });
486
+ return {
487
+ payload: {
488
+ graph: graphName,
489
+ action: 'latest',
490
+ tick: maxTick,
491
+ maxTick,
492
+ ticks,
493
+ nodes: nodes.length,
494
+ edges: edges.length,
495
+ perWriter: serializePerWriter(perWriter),
496
+ patchCount: countPatchesAtTick(maxTick, perWriter),
497
+ diff,
498
+ tickReceipt,
499
+ cursor: { active: false },
500
+ ...sdResult,
501
+ },
502
+ exitCode: EXIT_CODES.OK,
503
+ };
504
+ }
505
+ if (seekSpec.action === 'save') {
506
+ if (!activeCursor) {
507
+ throw usageError('No active cursor to save. Use --tick first.');
508
+ }
509
+ await writeSavedCursor(persistence, graphName, /** @type {string} */ (seekSpec.name), activeCursor);
510
+ return {
511
+ payload: {
512
+ graph: graphName,
513
+ action: 'save',
514
+ name: seekSpec.name,
515
+ tick: activeCursor.tick,
516
+ },
517
+ exitCode: EXIT_CODES.OK,
518
+ };
519
+ }
520
+ if (seekSpec.action === 'load') {
521
+ const loadName = /** @type {string} */ (seekSpec.name);
522
+ const saved = await readSavedCursor(persistence, graphName, loadName);
523
+ if (!saved) {
524
+ throw notFoundError(`Saved cursor not found: ${loadName}`);
525
+ }
526
+ const prevTick = activeCursor ? activeCursor.tick : null;
527
+ let sdResult = null;
528
+ if (seekSpec.diff) {
529
+ sdResult = await computeStructuralDiff({ graph, prevTick, currentTick: saved.tick, diffLimit: seekSpec.diffLimit });
530
+ }
531
+ // When --diff already materialized at saved.tick, skip redundant call
532
+ if (!sdResult) {
533
+ await graph.materialize({ ceiling: saved.tick });
534
+ }
535
+ const nodes = await graph.getNodes();
536
+ const edges = await graph.getEdges();
537
+ await writeActiveCursor(persistence, graphName, { tick: saved.tick, mode: saved.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
538
+ const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
539
+ const tickReceipt = await buildTickReceipt({ tick: saved.tick, perWriter, graph });
540
+ return {
541
+ payload: {
542
+ graph: graphName,
543
+ action: 'load',
544
+ name: seekSpec.name,
545
+ tick: saved.tick,
546
+ maxTick,
547
+ ticks,
548
+ nodes: nodes.length,
549
+ edges: edges.length,
550
+ perWriter: serializePerWriter(perWriter),
551
+ patchCount: countPatchesAtTick(saved.tick, perWriter),
552
+ diff,
553
+ tickReceipt,
554
+ cursor: { active: true, mode: saved.mode, tick: saved.tick, maxTick, name: seekSpec.name },
555
+ ...sdResult,
556
+ },
557
+ exitCode: EXIT_CODES.OK,
558
+ };
559
+ }
560
+ if (seekSpec.action === 'tick') {
561
+ const currentTick = activeCursor ? activeCursor.tick : null;
562
+ const resolvedTick = resolveTickValue(/** @type {string} */ (seekSpec.tickValue), currentTick, ticks, maxTick);
563
+ let sdResult = null;
564
+ if (seekSpec.diff) {
565
+ sdResult = await computeStructuralDiff({ graph, prevTick: currentTick, currentTick: resolvedTick, diffLimit: seekSpec.diffLimit });
566
+ }
567
+ // When --diff already materialized at resolvedTick, skip redundant call
568
+ if (!sdResult) {
569
+ await graph.materialize({ ceiling: resolvedTick });
570
+ }
571
+ const nodes = await graph.getNodes();
572
+ const edges = await graph.getEdges();
573
+ await writeActiveCursor(persistence, graphName, { tick: resolvedTick, mode: 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
574
+ const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
575
+ const tickReceipt = await buildTickReceipt({ tick: resolvedTick, perWriter, graph });
576
+ return {
577
+ payload: {
578
+ graph: graphName,
579
+ action: 'tick',
580
+ tick: resolvedTick,
581
+ maxTick,
582
+ ticks,
583
+ nodes: nodes.length,
584
+ edges: edges.length,
585
+ perWriter: serializePerWriter(perWriter),
586
+ patchCount: countPatchesAtTick(resolvedTick, perWriter),
587
+ diff,
588
+ tickReceipt,
589
+ cursor: { active: true, mode: 'lamport', tick: resolvedTick, maxTick, name: 'active' },
590
+ ...sdResult,
591
+ },
592
+ exitCode: EXIT_CODES.OK,
593
+ };
594
+ }
595
+
596
+ // status (bare seek)
597
+ return await handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash });
598
+ }