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