@git-stunts/git-warp 10.3.2 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/SECURITY.md +89 -1
- package/bin/warp-graph.js +574 -208
- package/index.d.ts +55 -0
- package/index.js +4 -0
- package/package.json +8 -4
- package/src/domain/WarpGraph.js +334 -161
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +88 -19
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncAuthService.js +396 -0
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +19 -0
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/adapters/adapterValidation.js +90 -0
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +17 -4
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +92 -1
- package/src/visualization/renderers/ascii/history.js +28 -26
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +15 -7
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +187 -23
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
package/bin/warp-graph.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import readline from 'node:readline';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
|
+
// @ts-expect-error — no type declarations for @git-stunts/plumbing
|
|
9
10
|
import GitPlumbing, { ShellRunnerFactory } from '@git-stunts/plumbing';
|
|
10
11
|
import WarpGraph from '../src/domain/WarpGraph.js';
|
|
11
12
|
import GitGraphAdapter from '../src/infrastructure/adapters/GitGraphAdapter.js';
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
buildCursorSavedRef,
|
|
23
24
|
buildCursorSavedPrefix,
|
|
24
25
|
} from '../src/domain/utils/RefLayout.js';
|
|
26
|
+
import CasSeekCacheAdapter from '../src/infrastructure/adapters/CasSeekCacheAdapter.js';
|
|
25
27
|
import { HookInstaller, classifyExistingHook } from '../src/domain/services/HookInstaller.js';
|
|
26
28
|
import { renderInfoView } from '../src/visualization/renderers/ascii/info.js';
|
|
27
29
|
import { renderCheckView } from '../src/visualization/renderers/ascii/check.js';
|
|
@@ -29,11 +31,94 @@ import { renderHistoryView, summarizeOps } from '../src/visualization/renderers/
|
|
|
29
31
|
import { renderPathView } from '../src/visualization/renderers/ascii/path.js';
|
|
30
32
|
import { renderMaterializeView } from '../src/visualization/renderers/ascii/materialize.js';
|
|
31
33
|
import { parseCursorBlob } from '../src/domain/utils/parseCursorBlob.js';
|
|
32
|
-
import {
|
|
34
|
+
import { diffStates } from '../src/domain/services/StateDiff.js';
|
|
35
|
+
import { renderSeekView, formatStructuralDiff } from '../src/visualization/renderers/ascii/seek.js';
|
|
33
36
|
import { renderGraphView } from '../src/visualization/renderers/ascii/graph.js';
|
|
34
37
|
import { renderSvg } from '../src/visualization/renderers/svg/index.js';
|
|
35
38
|
import { layoutGraph, queryResultToGraphData, pathResultToGraphData } from '../src/visualization/layouts/index.js';
|
|
36
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} Persistence
|
|
42
|
+
* @property {(prefix: string) => Promise<string[]>} listRefs
|
|
43
|
+
* @property {(ref: string) => Promise<string|null>} readRef
|
|
44
|
+
* @property {(ref: string, oid: string) => Promise<void>} updateRef
|
|
45
|
+
* @property {(ref: string) => Promise<void>} deleteRef
|
|
46
|
+
* @property {(oid: string) => Promise<Buffer>} readBlob
|
|
47
|
+
* @property {(buf: Buffer) => Promise<string>} writeBlob
|
|
48
|
+
* @property {(sha: string) => Promise<{date?: string|null}>} getNodeInfo
|
|
49
|
+
* @property {(sha: string, coverageSha: string) => Promise<boolean>} isAncestor
|
|
50
|
+
* @property {() => Promise<{ok: boolean}>} ping
|
|
51
|
+
* @property {*} plumbing
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} WarpGraphInstance
|
|
56
|
+
* @property {(opts?: {ceiling?: number}) => Promise<void>} materialize
|
|
57
|
+
* @property {() => Promise<Array<{id: string}>>} getNodes
|
|
58
|
+
* @property {() => Promise<Array<{from: string, to: string, label?: string}>>} getEdges
|
|
59
|
+
* @property {() => Promise<string|null>} createCheckpoint
|
|
60
|
+
* @property {() => *} query
|
|
61
|
+
* @property {{ shortestPath: Function }} traverse
|
|
62
|
+
* @property {(writerId: string) => Promise<Array<{patch: any, sha: string}>>} getWriterPatches
|
|
63
|
+
* @property {() => Promise<{frontier: Record<string, any>}>} status
|
|
64
|
+
* @property {() => Promise<Map<string, any>>} getFrontier
|
|
65
|
+
* @property {() => {totalTombstones: number, tombstoneRatio: number}} getGCMetrics
|
|
66
|
+
* @property {() => Promise<number>} getPropertyCount
|
|
67
|
+
* @property {() => Promise<import('../src/domain/services/JoinReducer.js').WarpStateV5 | null>} getStateSnapshot
|
|
68
|
+
* @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
|
|
69
|
+
* @property {(sha: string) => Promise<{ops?: any[]}>} loadPatchBySha
|
|
70
|
+
* @property {(cache: any) => void} setSeekCache
|
|
71
|
+
* @property {*} seekCache
|
|
72
|
+
* @property {number} [_seekCeiling]
|
|
73
|
+
* @property {boolean} [_provenanceDegraded]
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} WriterTickInfo
|
|
78
|
+
* @property {number[]} ticks
|
|
79
|
+
* @property {string|null} tipSha
|
|
80
|
+
* @property {Record<number, string>} [tickShas]
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {Object} CursorBlob
|
|
85
|
+
* @property {number} tick
|
|
86
|
+
* @property {string} [mode]
|
|
87
|
+
* @property {number} [nodes]
|
|
88
|
+
* @property {number} [edges]
|
|
89
|
+
* @property {string} [frontierHash]
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {Object} CliOptions
|
|
94
|
+
* @property {string} repo
|
|
95
|
+
* @property {boolean} json
|
|
96
|
+
* @property {string|null} view
|
|
97
|
+
* @property {string|null} graph
|
|
98
|
+
* @property {string} writer
|
|
99
|
+
* @property {boolean} help
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @typedef {Object} GraphInfoResult
|
|
104
|
+
* @property {string} name
|
|
105
|
+
* @property {{count: number, ids?: string[]}} writers
|
|
106
|
+
* @property {{ref: string, sha: string|null, date?: string|null}} [checkpoint]
|
|
107
|
+
* @property {{ref: string, sha: string|null}} [coverage]
|
|
108
|
+
* @property {Record<string, number>} [writerPatches]
|
|
109
|
+
* @property {{active: boolean, tick?: number, mode?: string}} [cursor]
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @typedef {Object} SeekSpec
|
|
114
|
+
* @property {string} action
|
|
115
|
+
* @property {string|null} tickValue
|
|
116
|
+
* @property {string|null} name
|
|
117
|
+
* @property {boolean} noPersistentCache
|
|
118
|
+
* @property {boolean} diff
|
|
119
|
+
* @property {number} diffLimit
|
|
120
|
+
*/
|
|
121
|
+
|
|
37
122
|
const EXIT_CODES = {
|
|
38
123
|
OK: 0,
|
|
39
124
|
USAGE: 1,
|
|
@@ -90,6 +175,8 @@ Seek options:
|
|
|
90
175
|
--load <name> Restore a saved cursor
|
|
91
176
|
--list List all saved cursors
|
|
92
177
|
--drop <name> Delete a saved cursor
|
|
178
|
+
--diff Show structural diff (added/removed nodes, edges, props)
|
|
179
|
+
--diff-limit <N> Max diff entries (default 2000)
|
|
93
180
|
`;
|
|
94
181
|
|
|
95
182
|
/**
|
|
@@ -111,20 +198,25 @@ class CliError extends Error {
|
|
|
111
198
|
}
|
|
112
199
|
}
|
|
113
200
|
|
|
201
|
+
/** @param {string} message */
|
|
114
202
|
function usageError(message) {
|
|
115
203
|
return new CliError(message, { code: 'E_USAGE', exitCode: EXIT_CODES.USAGE });
|
|
116
204
|
}
|
|
117
205
|
|
|
206
|
+
/** @param {string} message */
|
|
118
207
|
function notFoundError(message) {
|
|
119
208
|
return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
|
|
120
209
|
}
|
|
121
210
|
|
|
211
|
+
/** @param {*} value */
|
|
122
212
|
function stableStringify(value) {
|
|
213
|
+
/** @param {*} input @returns {*} */
|
|
123
214
|
const normalize = (input) => {
|
|
124
215
|
if (Array.isArray(input)) {
|
|
125
216
|
return input.map(normalize);
|
|
126
217
|
}
|
|
127
218
|
if (input && typeof input === 'object') {
|
|
219
|
+
/** @type {Record<string, *>} */
|
|
128
220
|
const sorted = {};
|
|
129
221
|
for (const key of Object.keys(input).sort()) {
|
|
130
222
|
sorted[key] = normalize(input[key]);
|
|
@@ -137,8 +229,10 @@ function stableStringify(value) {
|
|
|
137
229
|
return JSON.stringify(normalize(value), null, 2);
|
|
138
230
|
}
|
|
139
231
|
|
|
232
|
+
/** @param {string[]} argv */
|
|
140
233
|
function parseArgs(argv) {
|
|
141
234
|
const options = createDefaultOptions();
|
|
235
|
+
/** @type {string[]} */
|
|
142
236
|
const positionals = [];
|
|
143
237
|
const optionDefs = [
|
|
144
238
|
{ flag: '--repo', shortFlag: '-r', key: 'repo' },
|
|
@@ -169,6 +263,14 @@ function createDefaultOptions() {
|
|
|
169
263
|
};
|
|
170
264
|
}
|
|
171
265
|
|
|
266
|
+
/**
|
|
267
|
+
* @param {Object} params
|
|
268
|
+
* @param {string[]} params.argv
|
|
269
|
+
* @param {number} params.index
|
|
270
|
+
* @param {Record<string, *>} params.options
|
|
271
|
+
* @param {Array<{flag: string, shortFlag?: string, key: string}>} params.optionDefs
|
|
272
|
+
* @param {string[]} params.positionals
|
|
273
|
+
*/
|
|
172
274
|
function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
173
275
|
const arg = argv[index];
|
|
174
276
|
|
|
@@ -220,8 +322,10 @@ function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
|
220
322
|
shortFlag: matched.shortFlag,
|
|
221
323
|
allowEmpty: false,
|
|
222
324
|
});
|
|
223
|
-
|
|
224
|
-
|
|
325
|
+
if (result) {
|
|
326
|
+
options[matched.key] = result.value;
|
|
327
|
+
return { consumed: result.consumed };
|
|
328
|
+
}
|
|
225
329
|
}
|
|
226
330
|
|
|
227
331
|
if (arg.startsWith('-')) {
|
|
@@ -232,6 +336,10 @@ function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
|
232
336
|
return { consumed: argv.length - index - 1, done: true };
|
|
233
337
|
}
|
|
234
338
|
|
|
339
|
+
/**
|
|
340
|
+
* @param {string} arg
|
|
341
|
+
* @param {Array<{flag: string, shortFlag?: string, key: string}>} optionDefs
|
|
342
|
+
*/
|
|
235
343
|
function matchOptionDef(arg, optionDefs) {
|
|
236
344
|
return optionDefs.find((def) =>
|
|
237
345
|
arg === def.flag ||
|
|
@@ -240,6 +348,7 @@ function matchOptionDef(arg, optionDefs) {
|
|
|
240
348
|
);
|
|
241
349
|
}
|
|
242
350
|
|
|
351
|
+
/** @param {string} repoPath @returns {Promise<{persistence: Persistence}>} */
|
|
243
352
|
async function createPersistence(repoPath) {
|
|
244
353
|
const runner = ShellRunnerFactory.create();
|
|
245
354
|
const plumbing = new GitPlumbing({ cwd: repoPath, runner });
|
|
@@ -251,6 +360,7 @@ async function createPersistence(repoPath) {
|
|
|
251
360
|
return { persistence };
|
|
252
361
|
}
|
|
253
362
|
|
|
363
|
+
/** @param {Persistence} persistence @returns {Promise<string[]>} */
|
|
254
364
|
async function listGraphNames(persistence) {
|
|
255
365
|
if (typeof persistence.listRefs !== 'function') {
|
|
256
366
|
return [];
|
|
@@ -273,6 +383,11 @@ async function listGraphNames(persistence) {
|
|
|
273
383
|
return [...names].sort();
|
|
274
384
|
}
|
|
275
385
|
|
|
386
|
+
/**
|
|
387
|
+
* @param {Persistence} persistence
|
|
388
|
+
* @param {string|null} explicitGraph
|
|
389
|
+
* @returns {Promise<string>}
|
|
390
|
+
*/
|
|
276
391
|
async function resolveGraphName(persistence, explicitGraph) {
|
|
277
392
|
if (explicitGraph) {
|
|
278
393
|
return explicitGraph;
|
|
@@ -289,14 +404,14 @@ async function resolveGraphName(persistence, explicitGraph) {
|
|
|
289
404
|
|
|
290
405
|
/**
|
|
291
406
|
* Collects metadata about a single graph (writer count, refs, patches, checkpoint).
|
|
292
|
-
* @param {
|
|
407
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
293
408
|
* @param {string} graphName - Name of the graph to inspect
|
|
294
409
|
* @param {Object} [options]
|
|
295
410
|
* @param {boolean} [options.includeWriterIds=false] - Include writer ID list
|
|
296
411
|
* @param {boolean} [options.includeRefs=false] - Include checkpoint/coverage refs
|
|
297
412
|
* @param {boolean} [options.includeWriterPatches=false] - Include per-writer patch counts
|
|
298
413
|
* @param {boolean} [options.includeCheckpointDate=false] - Include checkpoint date
|
|
299
|
-
* @returns {Promise<
|
|
414
|
+
* @returns {Promise<GraphInfoResult>} Graph info object
|
|
300
415
|
*/
|
|
301
416
|
async function getGraphInfo(persistence, graphName, {
|
|
302
417
|
includeWriterIds = false,
|
|
@@ -308,11 +423,12 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
308
423
|
const writerRefs = typeof persistence.listRefs === 'function'
|
|
309
424
|
? await persistence.listRefs(writersPrefix)
|
|
310
425
|
: [];
|
|
311
|
-
const writerIds = writerRefs
|
|
426
|
+
const writerIds = /** @type {string[]} */ (writerRefs
|
|
312
427
|
.map((ref) => parseWriterIdFromRef(ref))
|
|
313
428
|
.filter(Boolean)
|
|
314
|
-
.sort();
|
|
429
|
+
.sort());
|
|
315
430
|
|
|
431
|
+
/** @type {GraphInfoResult} */
|
|
316
432
|
const info = {
|
|
317
433
|
name: graphName,
|
|
318
434
|
writers: {
|
|
@@ -328,6 +444,7 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
328
444
|
const checkpointRef = buildCheckpointRef(graphName);
|
|
329
445
|
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
330
446
|
|
|
447
|
+
/** @type {{ref: string, sha: string|null, date?: string|null}} */
|
|
331
448
|
const checkpoint = { ref: checkpointRef, sha: checkpointSha || null };
|
|
332
449
|
|
|
333
450
|
if (includeCheckpointDate && checkpointSha) {
|
|
@@ -351,10 +468,11 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
351
468
|
writerId: 'cli',
|
|
352
469
|
crypto: new NodeCryptoAdapter(),
|
|
353
470
|
});
|
|
471
|
+
/** @type {Record<string, number>} */
|
|
354
472
|
const writerPatches = {};
|
|
355
473
|
for (const writerId of writerIds) {
|
|
356
474
|
const patches = await graph.getWriterPatches(writerId);
|
|
357
|
-
writerPatches[writerId] = patches.length;
|
|
475
|
+
writerPatches[/** @type {string} */ (writerId)] = patches.length;
|
|
358
476
|
}
|
|
359
477
|
info.writerPatches = writerPatches;
|
|
360
478
|
}
|
|
@@ -364,11 +482,8 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
364
482
|
|
|
365
483
|
/**
|
|
366
484
|
* Opens a WarpGraph for the given CLI options.
|
|
367
|
-
* @param {
|
|
368
|
-
* @
|
|
369
|
-
* @param {string} [options.graph] - Explicit graph name
|
|
370
|
-
* @param {string} [options.writer] - Writer ID
|
|
371
|
-
* @returns {Promise<{graph: Object, graphName: string, persistence: Object}>}
|
|
485
|
+
* @param {CliOptions} options - Parsed CLI options
|
|
486
|
+
* @returns {Promise<{graph: WarpGraphInstance, graphName: string, persistence: Persistence}>}
|
|
372
487
|
* @throws {CliError} If the specified graph is not found
|
|
373
488
|
*/
|
|
374
489
|
async function openGraph(options) {
|
|
@@ -380,15 +495,16 @@ async function openGraph(options) {
|
|
|
380
495
|
throw notFoundError(`Graph not found: ${options.graph}`);
|
|
381
496
|
}
|
|
382
497
|
}
|
|
383
|
-
const graph = await WarpGraph.open({
|
|
498
|
+
const graph = /** @type {WarpGraphInstance} */ (/** @type {*} */ (await WarpGraph.open({ // TODO(ts-cleanup): narrow port type
|
|
384
499
|
persistence,
|
|
385
500
|
graphName,
|
|
386
501
|
writerId: options.writer,
|
|
387
502
|
crypto: new NodeCryptoAdapter(),
|
|
388
|
-
});
|
|
503
|
+
})));
|
|
389
504
|
return { graph, graphName, persistence };
|
|
390
505
|
}
|
|
391
506
|
|
|
507
|
+
/** @param {string[]} args */
|
|
392
508
|
function parseQueryArgs(args) {
|
|
393
509
|
const spec = {
|
|
394
510
|
match: null,
|
|
@@ -407,6 +523,11 @@ function parseQueryArgs(args) {
|
|
|
407
523
|
return spec;
|
|
408
524
|
}
|
|
409
525
|
|
|
526
|
+
/**
|
|
527
|
+
* @param {string[]} args
|
|
528
|
+
* @param {number} index
|
|
529
|
+
* @param {{match: string|null, select: string[]|null, steps: Array<{type: string, label?: string, key?: string, value?: string}>}} spec
|
|
530
|
+
*/
|
|
410
531
|
function consumeQueryArg(args, index, spec) {
|
|
411
532
|
const stepResult = readTraversalStep(args, index);
|
|
412
533
|
if (stepResult) {
|
|
@@ -450,6 +571,7 @@ function consumeQueryArg(args, index, spec) {
|
|
|
450
571
|
return null;
|
|
451
572
|
}
|
|
452
573
|
|
|
574
|
+
/** @param {string} value */
|
|
453
575
|
function parseWhereProp(value) {
|
|
454
576
|
const [key, ...rest] = value.split('=');
|
|
455
577
|
if (!key || rest.length === 0) {
|
|
@@ -458,6 +580,7 @@ function parseWhereProp(value) {
|
|
|
458
580
|
return { type: 'where-prop', key, value: rest.join('=') };
|
|
459
581
|
}
|
|
460
582
|
|
|
583
|
+
/** @param {string} value */
|
|
461
584
|
function parseSelectFields(value) {
|
|
462
585
|
if (value === '') {
|
|
463
586
|
return [];
|
|
@@ -465,6 +588,10 @@ function parseSelectFields(value) {
|
|
|
465
588
|
return value.split(',').map((field) => field.trim()).filter(Boolean);
|
|
466
589
|
}
|
|
467
590
|
|
|
591
|
+
/**
|
|
592
|
+
* @param {string[]} args
|
|
593
|
+
* @param {number} index
|
|
594
|
+
*/
|
|
468
595
|
function readTraversalStep(args, index) {
|
|
469
596
|
const arg = args[index];
|
|
470
597
|
if (arg !== '--outgoing' && arg !== '--incoming') {
|
|
@@ -476,6 +603,9 @@ function readTraversalStep(args, index) {
|
|
|
476
603
|
return { step: { type: arg.slice(2), label }, consumed };
|
|
477
604
|
}
|
|
478
605
|
|
|
606
|
+
/**
|
|
607
|
+
* @param {{args: string[], index: number, flag: string, shortFlag?: string, allowEmpty?: boolean}} params
|
|
608
|
+
*/
|
|
479
609
|
function readOptionValue({ args, index, flag, shortFlag, allowEmpty = false }) {
|
|
480
610
|
const arg = args[index];
|
|
481
611
|
if (matchesOptionFlag(arg, flag, shortFlag)) {
|
|
@@ -489,10 +619,16 @@ function readOptionValue({ args, index, flag, shortFlag, allowEmpty = false }) {
|
|
|
489
619
|
return null;
|
|
490
620
|
}
|
|
491
621
|
|
|
622
|
+
/**
|
|
623
|
+
* @param {string} arg
|
|
624
|
+
* @param {string} flag
|
|
625
|
+
* @param {string} [shortFlag]
|
|
626
|
+
*/
|
|
492
627
|
function matchesOptionFlag(arg, flag, shortFlag) {
|
|
493
628
|
return arg === flag || (shortFlag && arg === shortFlag);
|
|
494
629
|
}
|
|
495
630
|
|
|
631
|
+
/** @param {{args: string[], index: number, flag: string, allowEmpty?: boolean}} params */
|
|
496
632
|
function readNextOptionValue({ args, index, flag, allowEmpty }) {
|
|
497
633
|
const value = args[index + 1];
|
|
498
634
|
if (value === undefined || (!allowEmpty && value === '')) {
|
|
@@ -501,6 +637,7 @@ function readNextOptionValue({ args, index, flag, allowEmpty }) {
|
|
|
501
637
|
return { value, consumed: 1 };
|
|
502
638
|
}
|
|
503
639
|
|
|
640
|
+
/** @param {{arg: string, flag: string, allowEmpty?: boolean}} params */
|
|
504
641
|
function readInlineOptionValue({ arg, flag, allowEmpty }) {
|
|
505
642
|
const value = arg.slice(flag.length + 1);
|
|
506
643
|
if (!allowEmpty && value === '') {
|
|
@@ -509,9 +646,12 @@ function readInlineOptionValue({ arg, flag, allowEmpty }) {
|
|
|
509
646
|
return { value, consumed: 0 };
|
|
510
647
|
}
|
|
511
648
|
|
|
649
|
+
/** @param {string[]} args */
|
|
512
650
|
function parsePathArgs(args) {
|
|
513
651
|
const options = createPathOptions();
|
|
652
|
+
/** @type {string[]} */
|
|
514
653
|
const labels = [];
|
|
654
|
+
/** @type {string[]} */
|
|
515
655
|
const positionals = [];
|
|
516
656
|
|
|
517
657
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -523,6 +663,7 @@ function parsePathArgs(args) {
|
|
|
523
663
|
return options;
|
|
524
664
|
}
|
|
525
665
|
|
|
666
|
+
/** @returns {{from: string|null, to: string|null, dir: string|undefined, labelFilter: string|string[]|undefined, maxDepth: number|undefined}} */
|
|
526
667
|
function createPathOptions() {
|
|
527
668
|
return {
|
|
528
669
|
from: null,
|
|
@@ -533,8 +674,12 @@ function createPathOptions() {
|
|
|
533
674
|
};
|
|
534
675
|
}
|
|
535
676
|
|
|
677
|
+
/**
|
|
678
|
+
* @param {{args: string[], index: number, options: ReturnType<typeof createPathOptions>, labels: string[], positionals: string[]}} params
|
|
679
|
+
*/
|
|
536
680
|
function consumePathArg({ args, index, options, labels, positionals }) {
|
|
537
681
|
const arg = args[index];
|
|
682
|
+
/** @type {Array<{flag: string, apply: (value: string) => void}>} */
|
|
538
683
|
const handlers = [
|
|
539
684
|
{ flag: '--from', apply: (value) => { options.from = value; } },
|
|
540
685
|
{ flag: '--to', apply: (value) => { options.to = value; } },
|
|
@@ -559,6 +704,11 @@ function consumePathArg({ args, index, options, labels, positionals }) {
|
|
|
559
704
|
return { consumed: 0 };
|
|
560
705
|
}
|
|
561
706
|
|
|
707
|
+
/**
|
|
708
|
+
* @param {ReturnType<typeof createPathOptions>} options
|
|
709
|
+
* @param {string[]} labels
|
|
710
|
+
* @param {string[]} positionals
|
|
711
|
+
*/
|
|
562
712
|
function finalizePathOptions(options, labels, positionals) {
|
|
563
713
|
if (!options.from) {
|
|
564
714
|
options.from = positionals[0] || null;
|
|
@@ -579,10 +729,12 @@ function finalizePathOptions(options, labels, positionals) {
|
|
|
579
729
|
}
|
|
580
730
|
}
|
|
581
731
|
|
|
732
|
+
/** @param {string} value */
|
|
582
733
|
function parseLabels(value) {
|
|
583
734
|
return value.split(',').map((label) => label.trim()).filter(Boolean);
|
|
584
735
|
}
|
|
585
736
|
|
|
737
|
+
/** @param {string} value */
|
|
586
738
|
function parseMaxDepth(value) {
|
|
587
739
|
const parsed = Number.parseInt(value, 10);
|
|
588
740
|
if (Number.isNaN(parsed)) {
|
|
@@ -591,7 +743,9 @@ function parseMaxDepth(value) {
|
|
|
591
743
|
return parsed;
|
|
592
744
|
}
|
|
593
745
|
|
|
746
|
+
/** @param {string[]} args */
|
|
594
747
|
function parseHistoryArgs(args) {
|
|
748
|
+
/** @type {{node: string|null}} */
|
|
595
749
|
const options = { node: null };
|
|
596
750
|
|
|
597
751
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -622,6 +776,10 @@ function parseHistoryArgs(args) {
|
|
|
622
776
|
return options;
|
|
623
777
|
}
|
|
624
778
|
|
|
779
|
+
/**
|
|
780
|
+
* @param {*} patch
|
|
781
|
+
* @param {string} nodeId
|
|
782
|
+
*/
|
|
625
783
|
function patchTouchesNode(patch, nodeId) {
|
|
626
784
|
const ops = Array.isArray(patch?.ops) ? patch.ops : [];
|
|
627
785
|
for (const op of ops) {
|
|
@@ -635,6 +793,7 @@ function patchTouchesNode(patch, nodeId) {
|
|
|
635
793
|
return false;
|
|
636
794
|
}
|
|
637
795
|
|
|
796
|
+
/** @param {*} payload */
|
|
638
797
|
function renderInfo(payload) {
|
|
639
798
|
const lines = [`Repo: ${payload.repo}`];
|
|
640
799
|
lines.push(`Graphs: ${payload.graphs.length}`);
|
|
@@ -654,6 +813,7 @@ function renderInfo(payload) {
|
|
|
654
813
|
return `${lines.join('\n')}\n`;
|
|
655
814
|
}
|
|
656
815
|
|
|
816
|
+
/** @param {*} payload */
|
|
657
817
|
function renderQuery(payload) {
|
|
658
818
|
const lines = [
|
|
659
819
|
`Graph: ${payload.graph}`,
|
|
@@ -672,6 +832,7 @@ function renderQuery(payload) {
|
|
|
672
832
|
return `${lines.join('\n')}\n`;
|
|
673
833
|
}
|
|
674
834
|
|
|
835
|
+
/** @param {*} payload */
|
|
675
836
|
function renderPath(payload) {
|
|
676
837
|
const lines = [
|
|
677
838
|
`Graph: ${payload.graph}`,
|
|
@@ -694,6 +855,7 @@ const ANSI_RED = '\x1b[31m';
|
|
|
694
855
|
const ANSI_DIM = '\x1b[2m';
|
|
695
856
|
const ANSI_RESET = '\x1b[0m';
|
|
696
857
|
|
|
858
|
+
/** @param {string} state */
|
|
697
859
|
function colorCachedState(state) {
|
|
698
860
|
if (state === 'fresh') {
|
|
699
861
|
return `${ANSI_GREEN}${state}${ANSI_RESET}`;
|
|
@@ -704,6 +866,7 @@ function colorCachedState(state) {
|
|
|
704
866
|
return `${ANSI_RED}${ANSI_DIM}${state}${ANSI_RESET}`;
|
|
705
867
|
}
|
|
706
868
|
|
|
869
|
+
/** @param {*} payload */
|
|
707
870
|
function renderCheck(payload) {
|
|
708
871
|
const lines = [
|
|
709
872
|
`Graph: ${payload.graph}`,
|
|
@@ -754,6 +917,7 @@ function renderCheck(payload) {
|
|
|
754
917
|
return `${lines.join('\n')}\n`;
|
|
755
918
|
}
|
|
756
919
|
|
|
920
|
+
/** @param {*} hook */
|
|
757
921
|
function formatHookStatusLine(hook) {
|
|
758
922
|
if (!hook.installed && hook.foreign) {
|
|
759
923
|
return "Hook: foreign hook present — run 'git warp install-hooks'";
|
|
@@ -767,6 +931,7 @@ function formatHookStatusLine(hook) {
|
|
|
767
931
|
return `Hook: installed (v${hook.version}) — upgrade available, run 'git warp install-hooks'`;
|
|
768
932
|
}
|
|
769
933
|
|
|
934
|
+
/** @param {*} payload */
|
|
770
935
|
function renderHistory(payload) {
|
|
771
936
|
const lines = [
|
|
772
937
|
`Graph: ${payload.graph}`,
|
|
@@ -785,6 +950,7 @@ function renderHistory(payload) {
|
|
|
785
950
|
return `${lines.join('\n')}\n`;
|
|
786
951
|
}
|
|
787
952
|
|
|
953
|
+
/** @param {*} payload */
|
|
788
954
|
function renderError(payload) {
|
|
789
955
|
return `Error: ${payload.error.message}\n`;
|
|
790
956
|
}
|
|
@@ -803,11 +969,8 @@ function writeHtmlExport(filePath, svgContent) {
|
|
|
803
969
|
* Writes a command result to stdout/stderr in the appropriate format.
|
|
804
970
|
* Dispatches to JSON, SVG file, HTML file, ASCII view, or plain text
|
|
805
971
|
* based on the combination of flags.
|
|
806
|
-
* @param {
|
|
807
|
-
* @param {
|
|
808
|
-
* @param {boolean} options.json - Emit JSON to stdout
|
|
809
|
-
* @param {string} options.command - Command name (info, query, path, etc.)
|
|
810
|
-
* @param {string|boolean} options.view - View mode (true for ascii, 'svg:PATH', 'html:PATH', 'browser')
|
|
972
|
+
* @param {*} payload - Command result payload
|
|
973
|
+
* @param {{json: boolean, command: string, view: string|null}} options
|
|
811
974
|
*/
|
|
812
975
|
function emit(payload, { json, command, view }) {
|
|
813
976
|
if (json) {
|
|
@@ -925,9 +1088,8 @@ function emit(payload, { json, command, view }) {
|
|
|
925
1088
|
|
|
926
1089
|
/**
|
|
927
1090
|
* Handles the `info` command: summarizes graphs in the repository.
|
|
928
|
-
* @param {
|
|
929
|
-
* @
|
|
930
|
-
* @returns {Promise<{repo: string, graphs: Object[]}>} Info payload
|
|
1091
|
+
* @param {{options: CliOptions}} params
|
|
1092
|
+
* @returns {Promise<{repo: string, graphs: GraphInfoResult[]}>} Info payload
|
|
931
1093
|
* @throws {CliError} If the specified graph is not found
|
|
932
1094
|
*/
|
|
933
1095
|
async function handleInfo({ options }) {
|
|
@@ -974,10 +1136,8 @@ async function handleInfo({ options }) {
|
|
|
974
1136
|
|
|
975
1137
|
/**
|
|
976
1138
|
* Handles the `query` command: runs a logical graph query.
|
|
977
|
-
* @param {
|
|
978
|
-
* @
|
|
979
|
-
* @param {string[]} params.args - Remaining positional arguments (query spec)
|
|
980
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Query result payload
|
|
1139
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1140
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Query result payload
|
|
981
1141
|
* @throws {CliError} On invalid query options or query execution errors
|
|
982
1142
|
*/
|
|
983
1143
|
async function handleQuery({ options, args }) {
|
|
@@ -1021,6 +1181,10 @@ async function handleQuery({ options, args }) {
|
|
|
1021
1181
|
}
|
|
1022
1182
|
}
|
|
1023
1183
|
|
|
1184
|
+
/**
|
|
1185
|
+
* @param {*} builder
|
|
1186
|
+
* @param {Array<{type: string, label?: string, key?: string, value?: string}>} steps
|
|
1187
|
+
*/
|
|
1024
1188
|
function applyQuerySteps(builder, steps) {
|
|
1025
1189
|
let current = builder;
|
|
1026
1190
|
for (const step of steps) {
|
|
@@ -1029,6 +1193,10 @@ function applyQuerySteps(builder, steps) {
|
|
|
1029
1193
|
return current;
|
|
1030
1194
|
}
|
|
1031
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* @param {*} builder
|
|
1198
|
+
* @param {{type: string, label?: string, key?: string, value?: string}} step
|
|
1199
|
+
*/
|
|
1032
1200
|
function applyQueryStep(builder, step) {
|
|
1033
1201
|
if (step.type === 'outgoing') {
|
|
1034
1202
|
return builder.outgoing(step.label);
|
|
@@ -1037,11 +1205,16 @@ function applyQueryStep(builder, step) {
|
|
|
1037
1205
|
return builder.incoming(step.label);
|
|
1038
1206
|
}
|
|
1039
1207
|
if (step.type === 'where-prop') {
|
|
1040
|
-
return builder.where((node) => matchesPropFilter(node, step.key, step.value));
|
|
1208
|
+
return builder.where((/** @type {*} */ node) => matchesPropFilter(node, /** @type {string} */ (step.key), /** @type {string} */ (step.value))); // TODO(ts-cleanup): type CLI payload
|
|
1041
1209
|
}
|
|
1042
1210
|
return builder;
|
|
1043
1211
|
}
|
|
1044
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* @param {*} node
|
|
1215
|
+
* @param {string} key
|
|
1216
|
+
* @param {string} value
|
|
1217
|
+
*/
|
|
1045
1218
|
function matchesPropFilter(node, key, value) {
|
|
1046
1219
|
const props = node.props || {};
|
|
1047
1220
|
if (!Object.prototype.hasOwnProperty.call(props, key)) {
|
|
@@ -1050,6 +1223,11 @@ function matchesPropFilter(node, key, value) {
|
|
|
1050
1223
|
return String(props[key]) === value;
|
|
1051
1224
|
}
|
|
1052
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* @param {string} graphName
|
|
1228
|
+
* @param {*} result
|
|
1229
|
+
* @returns {{graph: string, stateHash: *, nodes: *, _renderedSvg?: string, _renderedAscii?: string}}
|
|
1230
|
+
*/
|
|
1053
1231
|
function buildQueryPayload(graphName, result) {
|
|
1054
1232
|
return {
|
|
1055
1233
|
graph: graphName,
|
|
@@ -1058,6 +1236,7 @@ function buildQueryPayload(graphName, result) {
|
|
|
1058
1236
|
};
|
|
1059
1237
|
}
|
|
1060
1238
|
|
|
1239
|
+
/** @param {*} error */
|
|
1061
1240
|
function mapQueryError(error) {
|
|
1062
1241
|
if (error && error.code && String(error.code).startsWith('E_QUERY')) {
|
|
1063
1242
|
throw usageError(error.message);
|
|
@@ -1067,10 +1246,8 @@ function mapQueryError(error) {
|
|
|
1067
1246
|
|
|
1068
1247
|
/**
|
|
1069
1248
|
* Handles the `path` command: finds a shortest path between two nodes.
|
|
1070
|
-
* @param {
|
|
1071
|
-
* @
|
|
1072
|
-
* @param {string[]} params.args - Remaining positional arguments (path spec)
|
|
1073
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Path result payload
|
|
1249
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1250
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Path result payload
|
|
1074
1251
|
* @throws {CliError} If --from/--to are missing or a node is not found
|
|
1075
1252
|
*/
|
|
1076
1253
|
async function handlePath({ options, args }) {
|
|
@@ -1107,7 +1284,7 @@ async function handlePath({ options, args }) {
|
|
|
1107
1284
|
payload,
|
|
1108
1285
|
exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NOT_FOUND,
|
|
1109
1286
|
};
|
|
1110
|
-
} catch (error) {
|
|
1287
|
+
} catch (/** @type {*} */ error) { // TODO(ts-cleanup): type error
|
|
1111
1288
|
if (error && error.code === 'NODE_NOT_FOUND') {
|
|
1112
1289
|
throw notFoundError(error.message);
|
|
1113
1290
|
}
|
|
@@ -1117,9 +1294,8 @@ async function handlePath({ options, args }) {
|
|
|
1117
1294
|
|
|
1118
1295
|
/**
|
|
1119
1296
|
* Handles the `check` command: reports graph health, GC, and hook status.
|
|
1120
|
-
* @param {
|
|
1121
|
-
* @
|
|
1122
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Health check payload
|
|
1297
|
+
* @param {{options: CliOptions}} params
|
|
1298
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Health check payload
|
|
1123
1299
|
*/
|
|
1124
1300
|
async function handleCheck({ options }) {
|
|
1125
1301
|
const { graph, graphName, persistence } = await openGraph(options);
|
|
@@ -1149,17 +1325,20 @@ async function handleCheck({ options }) {
|
|
|
1149
1325
|
};
|
|
1150
1326
|
}
|
|
1151
1327
|
|
|
1328
|
+
/** @param {Persistence} persistence */
|
|
1152
1329
|
async function getHealth(persistence) {
|
|
1153
1330
|
const clock = ClockAdapter.node();
|
|
1154
|
-
const healthService = new HealthCheckService({ persistence, clock });
|
|
1331
|
+
const healthService = new HealthCheckService({ persistence: /** @type {*} */ (persistence), clock }); // TODO(ts-cleanup): narrow port type
|
|
1155
1332
|
return await healthService.getHealth();
|
|
1156
1333
|
}
|
|
1157
1334
|
|
|
1335
|
+
/** @param {WarpGraphInstance} graph */
|
|
1158
1336
|
async function getGcMetrics(graph) {
|
|
1159
1337
|
await graph.materialize();
|
|
1160
1338
|
return graph.getGCMetrics();
|
|
1161
1339
|
}
|
|
1162
1340
|
|
|
1341
|
+
/** @param {WarpGraphInstance} graph */
|
|
1163
1342
|
async function collectWriterHeads(graph) {
|
|
1164
1343
|
const frontier = await graph.getFrontier();
|
|
1165
1344
|
return [...frontier.entries()]
|
|
@@ -1167,6 +1346,10 @@ async function collectWriterHeads(graph) {
|
|
|
1167
1346
|
.map(([writerId, sha]) => ({ writerId, sha }));
|
|
1168
1347
|
}
|
|
1169
1348
|
|
|
1349
|
+
/**
|
|
1350
|
+
* @param {Persistence} persistence
|
|
1351
|
+
* @param {string} graphName
|
|
1352
|
+
*/
|
|
1170
1353
|
async function loadCheckpointInfo(persistence, graphName) {
|
|
1171
1354
|
const checkpointRef = buildCheckpointRef(graphName);
|
|
1172
1355
|
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
@@ -1181,6 +1364,10 @@ async function loadCheckpointInfo(persistence, graphName) {
|
|
|
1181
1364
|
};
|
|
1182
1365
|
}
|
|
1183
1366
|
|
|
1367
|
+
/**
|
|
1368
|
+
* @param {Persistence} persistence
|
|
1369
|
+
* @param {string|null} checkpointSha
|
|
1370
|
+
*/
|
|
1184
1371
|
async function readCheckpointDate(persistence, checkpointSha) {
|
|
1185
1372
|
if (!checkpointSha) {
|
|
1186
1373
|
return null;
|
|
@@ -1189,6 +1376,7 @@ async function readCheckpointDate(persistence, checkpointSha) {
|
|
|
1189
1376
|
return info.date || null;
|
|
1190
1377
|
}
|
|
1191
1378
|
|
|
1379
|
+
/** @param {string|null} checkpointDate */
|
|
1192
1380
|
function computeAgeSeconds(checkpointDate) {
|
|
1193
1381
|
if (!checkpointDate) {
|
|
1194
1382
|
return null;
|
|
@@ -1200,6 +1388,11 @@ function computeAgeSeconds(checkpointDate) {
|
|
|
1200
1388
|
return Math.max(0, Math.floor((Date.now() - parsed) / 1000));
|
|
1201
1389
|
}
|
|
1202
1390
|
|
|
1391
|
+
/**
|
|
1392
|
+
* @param {Persistence} persistence
|
|
1393
|
+
* @param {string} graphName
|
|
1394
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
1395
|
+
*/
|
|
1203
1396
|
async function loadCoverageInfo(persistence, graphName, writerHeads) {
|
|
1204
1397
|
const coverageRef = buildCoverageRef(graphName);
|
|
1205
1398
|
const coverageSha = await persistence.readRef(coverageRef);
|
|
@@ -1214,6 +1407,11 @@ async function loadCoverageInfo(persistence, graphName, writerHeads) {
|
|
|
1214
1407
|
};
|
|
1215
1408
|
}
|
|
1216
1409
|
|
|
1410
|
+
/**
|
|
1411
|
+
* @param {Persistence} persistence
|
|
1412
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
1413
|
+
* @param {string} coverageSha
|
|
1414
|
+
*/
|
|
1217
1415
|
async function findMissingWriters(persistence, writerHeads, coverageSha) {
|
|
1218
1416
|
const missing = [];
|
|
1219
1417
|
for (const head of writerHeads) {
|
|
@@ -1225,6 +1423,9 @@ async function findMissingWriters(persistence, writerHeads, coverageSha) {
|
|
|
1225
1423
|
return missing;
|
|
1226
1424
|
}
|
|
1227
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* @param {{repo: string, graphName: string, health: *, checkpoint: *, writerHeads: Array<{writerId: string, sha: string}>, coverage: *, gcMetrics: *, hook: *|null, status: *|null}} params
|
|
1428
|
+
*/
|
|
1228
1429
|
function buildCheckPayload({
|
|
1229
1430
|
repo,
|
|
1230
1431
|
graphName,
|
|
@@ -1254,10 +1455,8 @@ function buildCheckPayload({
|
|
|
1254
1455
|
|
|
1255
1456
|
/**
|
|
1256
1457
|
* Handles the `history` command: shows patch history for a writer.
|
|
1257
|
-
* @param {
|
|
1258
|
-
* @
|
|
1259
|
-
* @param {string[]} params.args - Remaining positional arguments (history options)
|
|
1260
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} History payload
|
|
1458
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1459
|
+
* @returns {Promise<{payload: *, exitCode: number}>} History payload
|
|
1261
1460
|
* @throws {CliError} If no patches are found for the writer
|
|
1262
1461
|
*/
|
|
1263
1462
|
async function handleHistory({ options, args }) {
|
|
@@ -1269,15 +1468,15 @@ async function handleHistory({ options, args }) {
|
|
|
1269
1468
|
const writerId = options.writer;
|
|
1270
1469
|
let patches = await graph.getWriterPatches(writerId);
|
|
1271
1470
|
if (cursorInfo.active) {
|
|
1272
|
-
patches = patches.filter(({ patch }) => patch.lamport <= cursorInfo.tick);
|
|
1471
|
+
patches = patches.filter((/** @type {*} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick)); // TODO(ts-cleanup): type CLI payload
|
|
1273
1472
|
}
|
|
1274
1473
|
if (patches.length === 0) {
|
|
1275
1474
|
throw notFoundError(`No patches found for writer: ${writerId}`);
|
|
1276
1475
|
}
|
|
1277
1476
|
|
|
1278
1477
|
const entries = patches
|
|
1279
|
-
.filter(({ patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node))
|
|
1280
|
-
.map(({ patch, sha }) => ({
|
|
1478
|
+
.filter((/** @type {*} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node)) // TODO(ts-cleanup): type CLI payload
|
|
1479
|
+
.map((/** @type {*} */ { patch, sha }) => ({ // TODO(ts-cleanup): type CLI payload
|
|
1281
1480
|
sha,
|
|
1282
1481
|
schema: patch.schema,
|
|
1283
1482
|
lamport: patch.lamport,
|
|
@@ -1299,12 +1498,8 @@ async function handleHistory({ options, args }) {
|
|
|
1299
1498
|
* Materializes a single graph, creates a checkpoint, and returns summary stats.
|
|
1300
1499
|
* When a ceiling tick is provided (seek cursor active), the checkpoint step is
|
|
1301
1500
|
* skipped because the user is exploring historical state, not persisting it.
|
|
1302
|
-
* @param {
|
|
1303
|
-
* @
|
|
1304
|
-
* @param {string} params.graphName - Name of the graph to materialize
|
|
1305
|
-
* @param {string} params.writerId - Writer ID for the CLI session
|
|
1306
|
-
* @param {number} [params.ceiling] - Optional seek ceiling tick
|
|
1307
|
-
* @returns {Promise<{graph: string, nodes: number, edges: number, properties: number, checkpoint: string|null, writers: Object, patchCount: number}>}
|
|
1501
|
+
* @param {{persistence: Persistence, graphName: string, writerId: string, ceiling?: number}} params
|
|
1502
|
+
* @returns {Promise<{graph: string, nodes: number, edges: number, properties: number, checkpoint: string|null, writers: Record<string, number>, patchCount: number}>}
|
|
1308
1503
|
*/
|
|
1309
1504
|
async function materializeOneGraph({ persistence, graphName, writerId, ceiling }) {
|
|
1310
1505
|
const graph = await WarpGraph.open({ persistence, graphName, writerId, crypto: new NodeCryptoAdapter() });
|
|
@@ -1315,6 +1510,7 @@ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }
|
|
|
1315
1510
|
const status = await graph.status();
|
|
1316
1511
|
|
|
1317
1512
|
// Build per-writer patch counts for the view renderer
|
|
1513
|
+
/** @type {Record<string, number>} */
|
|
1318
1514
|
const writers = {};
|
|
1319
1515
|
let totalPatchCount = 0;
|
|
1320
1516
|
for (const wId of Object.keys(status.frontier)) {
|
|
@@ -1338,9 +1534,8 @@ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }
|
|
|
1338
1534
|
|
|
1339
1535
|
/**
|
|
1340
1536
|
* Handles the `materialize` command: materializes and checkpoints all graphs.
|
|
1341
|
-
* @param {
|
|
1342
|
-
* @
|
|
1343
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Materialize result payload
|
|
1537
|
+
* @param {{options: CliOptions}} params
|
|
1538
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Materialize result payload
|
|
1344
1539
|
* @throws {CliError} If the specified graph is not found
|
|
1345
1540
|
*/
|
|
1346
1541
|
async function handleMaterialize({ options }) {
|
|
@@ -1387,13 +1582,14 @@ async function handleMaterialize({ options }) {
|
|
|
1387
1582
|
}
|
|
1388
1583
|
}
|
|
1389
1584
|
|
|
1390
|
-
const allFailed = results.every((r) => r.error);
|
|
1585
|
+
const allFailed = results.every((r) => /** @type {*} */ (r).error); // TODO(ts-cleanup): type CLI payload
|
|
1391
1586
|
return {
|
|
1392
1587
|
payload: { graphs: results },
|
|
1393
1588
|
exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
|
|
1394
1589
|
};
|
|
1395
1590
|
}
|
|
1396
1591
|
|
|
1592
|
+
/** @param {*} payload */
|
|
1397
1593
|
function renderMaterialize(payload) {
|
|
1398
1594
|
if (payload.graphs.length === 0) {
|
|
1399
1595
|
return 'No graphs found in repo.\n';
|
|
@@ -1410,6 +1606,7 @@ function renderMaterialize(payload) {
|
|
|
1410
1606
|
return `${lines.join('\n')}\n`;
|
|
1411
1607
|
}
|
|
1412
1608
|
|
|
1609
|
+
/** @param {*} payload */
|
|
1413
1610
|
function renderInstallHooks(payload) {
|
|
1414
1611
|
if (payload.action === 'up-to-date') {
|
|
1415
1612
|
return `Hook: already up to date (v${payload.version}) at ${payload.hookPath}\n`;
|
|
@@ -1430,7 +1627,7 @@ function createHookInstaller() {
|
|
|
1430
1627
|
const templateDir = path.resolve(__dirname, '..', 'hooks');
|
|
1431
1628
|
const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
1432
1629
|
return new HookInstaller({
|
|
1433
|
-
fs,
|
|
1630
|
+
fs: /** @type {*} */ (fs), // TODO(ts-cleanup): narrow port type
|
|
1434
1631
|
execGitConfig: execGitConfigValue,
|
|
1435
1632
|
version,
|
|
1436
1633
|
templateDir,
|
|
@@ -1438,6 +1635,11 @@ function createHookInstaller() {
|
|
|
1438
1635
|
});
|
|
1439
1636
|
}
|
|
1440
1637
|
|
|
1638
|
+
/**
|
|
1639
|
+
* @param {string} repoPath
|
|
1640
|
+
* @param {string} key
|
|
1641
|
+
* @returns {string|null}
|
|
1642
|
+
*/
|
|
1441
1643
|
function execGitConfigValue(repoPath, key) {
|
|
1442
1644
|
try {
|
|
1443
1645
|
if (key === '--git-dir') {
|
|
@@ -1457,6 +1659,7 @@ function isInteractive() {
|
|
|
1457
1659
|
return Boolean(process.stderr.isTTY);
|
|
1458
1660
|
}
|
|
1459
1661
|
|
|
1662
|
+
/** @param {string} question @returns {Promise<string>} */
|
|
1460
1663
|
function promptUser(question) {
|
|
1461
1664
|
const rl = readline.createInterface({
|
|
1462
1665
|
input: process.stdin,
|
|
@@ -1470,6 +1673,7 @@ function promptUser(question) {
|
|
|
1470
1673
|
});
|
|
1471
1674
|
}
|
|
1472
1675
|
|
|
1676
|
+
/** @param {string[]} args */
|
|
1473
1677
|
function parseInstallHooksArgs(args) {
|
|
1474
1678
|
const options = { force: false };
|
|
1475
1679
|
for (const arg of args) {
|
|
@@ -1482,6 +1686,10 @@ function parseInstallHooksArgs(args) {
|
|
|
1482
1686
|
return options;
|
|
1483
1687
|
}
|
|
1484
1688
|
|
|
1689
|
+
/**
|
|
1690
|
+
* @param {*} classification
|
|
1691
|
+
* @param {{force: boolean}} hookOptions
|
|
1692
|
+
*/
|
|
1485
1693
|
async function resolveStrategy(classification, hookOptions) {
|
|
1486
1694
|
if (hookOptions.force) {
|
|
1487
1695
|
return 'replace';
|
|
@@ -1498,6 +1706,7 @@ async function resolveStrategy(classification, hookOptions) {
|
|
|
1498
1706
|
return await promptForForeignStrategy();
|
|
1499
1707
|
}
|
|
1500
1708
|
|
|
1709
|
+
/** @param {*} classification */
|
|
1501
1710
|
async function promptForOursStrategy(classification) {
|
|
1502
1711
|
const installer = createHookInstaller();
|
|
1503
1712
|
if (classification.version === installer._version) {
|
|
@@ -1539,10 +1748,8 @@ async function promptForForeignStrategy() {
|
|
|
1539
1748
|
|
|
1540
1749
|
/**
|
|
1541
1750
|
* Handles the `install-hooks` command: installs or upgrades the post-merge git hook.
|
|
1542
|
-
* @param {
|
|
1543
|
-
* @
|
|
1544
|
-
* @param {string[]} params.args - Remaining positional arguments (install-hooks options)
|
|
1545
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Install result payload
|
|
1751
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1752
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Install result payload
|
|
1546
1753
|
* @throws {CliError} If an existing hook is found and the session is not interactive
|
|
1547
1754
|
*/
|
|
1548
1755
|
async function handleInstallHooks({ options, args }) {
|
|
@@ -1578,6 +1785,7 @@ async function handleInstallHooks({ options, args }) {
|
|
|
1578
1785
|
};
|
|
1579
1786
|
}
|
|
1580
1787
|
|
|
1788
|
+
/** @param {string} hookPath */
|
|
1581
1789
|
function readHookContent(hookPath) {
|
|
1582
1790
|
try {
|
|
1583
1791
|
return fs.readFileSync(hookPath, 'utf8');
|
|
@@ -1586,6 +1794,7 @@ function readHookContent(hookPath) {
|
|
|
1586
1794
|
}
|
|
1587
1795
|
}
|
|
1588
1796
|
|
|
1797
|
+
/** @param {string} repoPath */
|
|
1589
1798
|
function getHookStatusForCheck(repoPath) {
|
|
1590
1799
|
try {
|
|
1591
1800
|
const installer = createHookInstaller();
|
|
@@ -1602,10 +1811,9 @@ function getHookStatusForCheck(repoPath) {
|
|
|
1602
1811
|
/**
|
|
1603
1812
|
* Reads the active seek cursor for a graph from Git ref storage.
|
|
1604
1813
|
*
|
|
1605
|
-
* @
|
|
1606
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1814
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1607
1815
|
* @param {string} graphName - Name of the WARP graph
|
|
1608
|
-
* @returns {Promise<
|
|
1816
|
+
* @returns {Promise<CursorBlob|null>} Cursor object, or null if no active cursor
|
|
1609
1817
|
* @throws {Error} If the stored blob is corrupted or not valid JSON
|
|
1610
1818
|
*/
|
|
1611
1819
|
async function readActiveCursor(persistence, graphName) {
|
|
@@ -1624,10 +1832,9 @@ async function readActiveCursor(persistence, graphName) {
|
|
|
1624
1832
|
* Serializes the cursor as JSON, stores it as a Git blob, and points
|
|
1625
1833
|
* the active cursor ref at that blob.
|
|
1626
1834
|
*
|
|
1627
|
-
* @
|
|
1628
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1835
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1629
1836
|
* @param {string} graphName - Name of the WARP graph
|
|
1630
|
-
* @param {
|
|
1837
|
+
* @param {CursorBlob} cursor - Cursor state to persist
|
|
1631
1838
|
* @returns {Promise<void>}
|
|
1632
1839
|
*/
|
|
1633
1840
|
async function writeActiveCursor(persistence, graphName, cursor) {
|
|
@@ -1642,8 +1849,7 @@ async function writeActiveCursor(persistence, graphName, cursor) {
|
|
|
1642
1849
|
*
|
|
1643
1850
|
* No-op if no active cursor exists.
|
|
1644
1851
|
*
|
|
1645
|
-
* @
|
|
1646
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1852
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1647
1853
|
* @param {string} graphName - Name of the WARP graph
|
|
1648
1854
|
* @returns {Promise<void>}
|
|
1649
1855
|
*/
|
|
@@ -1658,11 +1864,10 @@ async function clearActiveCursor(persistence, graphName) {
|
|
|
1658
1864
|
/**
|
|
1659
1865
|
* Reads a named saved cursor from Git ref storage.
|
|
1660
1866
|
*
|
|
1661
|
-
* @
|
|
1662
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1867
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1663
1868
|
* @param {string} graphName - Name of the WARP graph
|
|
1664
1869
|
* @param {string} name - Saved cursor name
|
|
1665
|
-
* @returns {Promise<
|
|
1870
|
+
* @returns {Promise<CursorBlob|null>} Cursor object, or null if not found
|
|
1666
1871
|
* @throws {Error} If the stored blob is corrupted or not valid JSON
|
|
1667
1872
|
*/
|
|
1668
1873
|
async function readSavedCursor(persistence, graphName, name) {
|
|
@@ -1681,11 +1886,10 @@ async function readSavedCursor(persistence, graphName, name) {
|
|
|
1681
1886
|
* Serializes the cursor as JSON, stores it as a Git blob, and points
|
|
1682
1887
|
* the named saved-cursor ref at that blob.
|
|
1683
1888
|
*
|
|
1684
|
-
* @
|
|
1685
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1889
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1686
1890
|
* @param {string} graphName - Name of the WARP graph
|
|
1687
1891
|
* @param {string} name - Saved cursor name
|
|
1688
|
-
* @param {
|
|
1892
|
+
* @param {CursorBlob} cursor - Cursor state to persist
|
|
1689
1893
|
* @returns {Promise<void>}
|
|
1690
1894
|
*/
|
|
1691
1895
|
async function writeSavedCursor(persistence, graphName, name, cursor) {
|
|
@@ -1700,8 +1904,7 @@ async function writeSavedCursor(persistence, graphName, name, cursor) {
|
|
|
1700
1904
|
*
|
|
1701
1905
|
* No-op if the named cursor does not exist.
|
|
1702
1906
|
*
|
|
1703
|
-
* @
|
|
1704
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1907
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1705
1908
|
* @param {string} graphName - Name of the WARP graph
|
|
1706
1909
|
* @param {string} name - Saved cursor name to delete
|
|
1707
1910
|
* @returns {Promise<void>}
|
|
@@ -1717,8 +1920,7 @@ async function deleteSavedCursor(persistence, graphName, name) {
|
|
|
1717
1920
|
/**
|
|
1718
1921
|
* Lists all saved cursors for a graph, reading each blob to include full cursor state.
|
|
1719
1922
|
*
|
|
1720
|
-
* @
|
|
1721
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1923
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1722
1924
|
* @param {string} graphName - Name of the WARP graph
|
|
1723
1925
|
* @returns {Promise<Array<{name: string, tick: number, mode?: string}>>} Array of saved cursors with their names
|
|
1724
1926
|
* @throws {Error} If any stored blob is corrupted or not valid JSON
|
|
@@ -1745,24 +1947,92 @@ async function listSavedCursors(persistence, graphName) {
|
|
|
1745
1947
|
// Seek Arg Parser
|
|
1746
1948
|
// ============================================================================
|
|
1747
1949
|
|
|
1950
|
+
/**
|
|
1951
|
+
* @param {string} arg
|
|
1952
|
+
* @param {SeekSpec} spec
|
|
1953
|
+
*/
|
|
1954
|
+
function handleSeekBooleanFlag(arg, spec) {
|
|
1955
|
+
if (arg === '--clear-cache') {
|
|
1956
|
+
if (spec.action !== 'status') {
|
|
1957
|
+
throw usageError('--clear-cache cannot be combined with other seek flags');
|
|
1958
|
+
}
|
|
1959
|
+
spec.action = 'clear-cache';
|
|
1960
|
+
} else if (arg === '--no-persistent-cache') {
|
|
1961
|
+
spec.noPersistentCache = true;
|
|
1962
|
+
} else if (arg === '--diff') {
|
|
1963
|
+
spec.diff = true;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Parses --diff-limit / --diff-limit=N into the seek spec.
|
|
1969
|
+
* @param {string} arg
|
|
1970
|
+
* @param {string[]} args
|
|
1971
|
+
* @param {number} i
|
|
1972
|
+
* @param {SeekSpec} spec
|
|
1973
|
+
*/
|
|
1974
|
+
function handleDiffLimitFlag(arg, args, i, spec) {
|
|
1975
|
+
let raw;
|
|
1976
|
+
if (arg.startsWith('--diff-limit=')) {
|
|
1977
|
+
raw = arg.slice('--diff-limit='.length);
|
|
1978
|
+
} else {
|
|
1979
|
+
raw = args[i + 1];
|
|
1980
|
+
if (raw === undefined) {
|
|
1981
|
+
throw usageError('Missing value for --diff-limit');
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const n = Number(raw);
|
|
1985
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
1986
|
+
throw usageError(`Invalid --diff-limit value: ${raw}. Must be a positive integer.`);
|
|
1987
|
+
}
|
|
1988
|
+
spec.diffLimit = n;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* Parses a named action flag (--save, --load, --drop) with its value.
|
|
1993
|
+
* @param {string} flagName - e.g. 'save'
|
|
1994
|
+
* @param {string} arg - Current arg token
|
|
1995
|
+
* @param {string[]} args - All args
|
|
1996
|
+
* @param {number} i - Current index
|
|
1997
|
+
* @param {SeekSpec} spec
|
|
1998
|
+
* @returns {number} Number of extra args consumed (0 or 1)
|
|
1999
|
+
*/
|
|
2000
|
+
function parseSeekNamedAction(flagName, arg, args, i, spec) {
|
|
2001
|
+
if (spec.action !== 'status') {
|
|
2002
|
+
throw usageError(`--${flagName} cannot be combined with other seek flags`);
|
|
2003
|
+
}
|
|
2004
|
+
spec.action = flagName;
|
|
2005
|
+
if (arg === `--${flagName}`) {
|
|
2006
|
+
const val = args[i + 1];
|
|
2007
|
+
if (val === undefined || val.startsWith('-')) {
|
|
2008
|
+
throw usageError(`Missing name for --${flagName}`);
|
|
2009
|
+
}
|
|
2010
|
+
spec.name = val;
|
|
2011
|
+
return 1;
|
|
2012
|
+
}
|
|
2013
|
+
spec.name = arg.slice(`--${flagName}=`.length);
|
|
2014
|
+
if (!spec.name) {
|
|
2015
|
+
throw usageError(`Missing name for --${flagName}`);
|
|
2016
|
+
}
|
|
2017
|
+
return 0;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1748
2020
|
/**
|
|
1749
2021
|
* Parses CLI arguments for the `seek` command into a structured spec.
|
|
1750
|
-
*
|
|
1751
|
-
* Supports mutually exclusive actions: `--tick <value>`, `--latest`,
|
|
1752
|
-
* `--save <name>`, `--load <name>`, `--list`, `--drop <name>`.
|
|
1753
|
-
* Defaults to `status` when no flags are provided.
|
|
1754
|
-
*
|
|
1755
|
-
* @private
|
|
1756
2022
|
* @param {string[]} args - Raw CLI arguments following the `seek` subcommand
|
|
1757
|
-
* @returns {
|
|
1758
|
-
* @throws {CliError} If arguments are invalid or flags are combined
|
|
2023
|
+
* @returns {SeekSpec} Parsed spec
|
|
1759
2024
|
*/
|
|
1760
2025
|
function parseSeekArgs(args) {
|
|
2026
|
+
/** @type {SeekSpec} */
|
|
1761
2027
|
const spec = {
|
|
1762
|
-
action: 'status', // status, tick, latest, save, load, list, drop
|
|
2028
|
+
action: 'status', // status, tick, latest, save, load, list, drop, clear-cache
|
|
1763
2029
|
tickValue: null,
|
|
1764
2030
|
name: null,
|
|
2031
|
+
noPersistentCache: false,
|
|
2032
|
+
diff: false,
|
|
2033
|
+
diffLimit: 2000,
|
|
1765
2034
|
};
|
|
2035
|
+
let diffLimitProvided = false;
|
|
1766
2036
|
|
|
1767
2037
|
for (let i = 0; i < args.length; i++) {
|
|
1768
2038
|
const arg = args[i];
|
|
@@ -1789,76 +2059,39 @@ function parseSeekArgs(args) {
|
|
|
1789
2059
|
throw usageError('--latest cannot be combined with other seek flags');
|
|
1790
2060
|
}
|
|
1791
2061
|
spec.action = 'latest';
|
|
1792
|
-
} else if (arg === '--save') {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
spec.action = 'save';
|
|
1797
|
-
const val = args[i + 1];
|
|
1798
|
-
if (val === undefined || val.startsWith('-')) {
|
|
1799
|
-
throw usageError('Missing name for --save');
|
|
1800
|
-
}
|
|
1801
|
-
spec.name = val;
|
|
1802
|
-
i += 1;
|
|
1803
|
-
} else if (arg.startsWith('--save=')) {
|
|
1804
|
-
if (spec.action !== 'status') {
|
|
1805
|
-
throw usageError('--save cannot be combined with other seek flags');
|
|
1806
|
-
}
|
|
1807
|
-
spec.action = 'save';
|
|
1808
|
-
spec.name = arg.slice('--save='.length);
|
|
1809
|
-
if (!spec.name) {
|
|
1810
|
-
throw usageError('Missing name for --save');
|
|
1811
|
-
}
|
|
1812
|
-
} else if (arg === '--load') {
|
|
1813
|
-
if (spec.action !== 'status') {
|
|
1814
|
-
throw usageError('--load cannot be combined with other seek flags');
|
|
1815
|
-
}
|
|
1816
|
-
spec.action = 'load';
|
|
1817
|
-
const val = args[i + 1];
|
|
1818
|
-
if (val === undefined || val.startsWith('-')) {
|
|
1819
|
-
throw usageError('Missing name for --load');
|
|
1820
|
-
}
|
|
1821
|
-
spec.name = val;
|
|
1822
|
-
i += 1;
|
|
1823
|
-
} else if (arg.startsWith('--load=')) {
|
|
1824
|
-
if (spec.action !== 'status') {
|
|
1825
|
-
throw usageError('--load cannot be combined with other seek flags');
|
|
1826
|
-
}
|
|
1827
|
-
spec.action = 'load';
|
|
1828
|
-
spec.name = arg.slice('--load='.length);
|
|
1829
|
-
if (!spec.name) {
|
|
1830
|
-
throw usageError('Missing name for --load');
|
|
1831
|
-
}
|
|
2062
|
+
} else if (arg === '--save' || arg.startsWith('--save=')) {
|
|
2063
|
+
i += parseSeekNamedAction('save', arg, args, i, spec);
|
|
2064
|
+
} else if (arg === '--load' || arg.startsWith('--load=')) {
|
|
2065
|
+
i += parseSeekNamedAction('load', arg, args, i, spec);
|
|
1832
2066
|
} else if (arg === '--list') {
|
|
1833
2067
|
if (spec.action !== 'status') {
|
|
1834
2068
|
throw usageError('--list cannot be combined with other seek flags');
|
|
1835
2069
|
}
|
|
1836
2070
|
spec.action = 'list';
|
|
1837
|
-
} else if (arg === '--drop') {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
spec.name = val;
|
|
1847
|
-
i += 1;
|
|
1848
|
-
} else if (arg.startsWith('--drop=')) {
|
|
1849
|
-
if (spec.action !== 'status') {
|
|
1850
|
-
throw usageError('--drop cannot be combined with other seek flags');
|
|
1851
|
-
}
|
|
1852
|
-
spec.action = 'drop';
|
|
1853
|
-
spec.name = arg.slice('--drop='.length);
|
|
1854
|
-
if (!spec.name) {
|
|
1855
|
-
throw usageError('Missing name for --drop');
|
|
2071
|
+
} else if (arg === '--drop' || arg.startsWith('--drop=')) {
|
|
2072
|
+
i += parseSeekNamedAction('drop', arg, args, i, spec);
|
|
2073
|
+
} else if (arg === '--clear-cache' || arg === '--no-persistent-cache' || arg === '--diff') {
|
|
2074
|
+
handleSeekBooleanFlag(arg, spec);
|
|
2075
|
+
} else if (arg === '--diff-limit' || arg.startsWith('--diff-limit=')) {
|
|
2076
|
+
handleDiffLimitFlag(arg, args, i, spec);
|
|
2077
|
+
diffLimitProvided = true;
|
|
2078
|
+
if (arg === '--diff-limit') {
|
|
2079
|
+
i += 1;
|
|
1856
2080
|
}
|
|
1857
2081
|
} else if (arg.startsWith('-')) {
|
|
1858
2082
|
throw usageError(`Unknown seek option: ${arg}`);
|
|
1859
2083
|
}
|
|
1860
2084
|
}
|
|
1861
2085
|
|
|
2086
|
+
// --diff is only meaningful for actions that navigate to a tick
|
|
2087
|
+
const DIFF_ACTIONS = new Set(['tick', 'latest', 'load']);
|
|
2088
|
+
if (spec.diff && !DIFF_ACTIONS.has(spec.action)) {
|
|
2089
|
+
throw usageError(`--diff cannot be used with --${spec.action}`);
|
|
2090
|
+
}
|
|
2091
|
+
if (diffLimitProvided && !spec.diff) {
|
|
2092
|
+
throw usageError('--diff-limit requires --diff');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1862
2095
|
return spec;
|
|
1863
2096
|
}
|
|
1864
2097
|
|
|
@@ -1909,28 +2142,44 @@ function resolveTickValue(tickValue, currentTick, ticks, maxTick) {
|
|
|
1909
2142
|
// Seek Handler
|
|
1910
2143
|
// ============================================================================
|
|
1911
2144
|
|
|
2145
|
+
/**
|
|
2146
|
+
* @param {WarpGraphInstance} graph
|
|
2147
|
+
* @param {Persistence} persistence
|
|
2148
|
+
* @param {string} graphName
|
|
2149
|
+
* @param {SeekSpec} seekSpec
|
|
2150
|
+
*/
|
|
2151
|
+
function wireSeekCache(graph, persistence, graphName, seekSpec) {
|
|
2152
|
+
if (seekSpec.noPersistentCache) {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
graph.setSeekCache(new CasSeekCacheAdapter({
|
|
2156
|
+
persistence,
|
|
2157
|
+
plumbing: persistence.plumbing,
|
|
2158
|
+
graphName,
|
|
2159
|
+
}));
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1912
2162
|
/**
|
|
1913
2163
|
* Handles the `git warp seek` command across all sub-actions.
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
* - `status`: show current cursor position or "no cursor" state
|
|
1917
|
-
* - `tick`: set the cursor to an absolute or relative Lamport tick
|
|
1918
|
-
* - `latest`: clear the cursor, returning to present state
|
|
1919
|
-
* - `save`: persist the active cursor under a name
|
|
1920
|
-
* - `load`: restore a named cursor as the active cursor
|
|
1921
|
-
* - `list`: enumerate all saved cursors
|
|
1922
|
-
* - `drop`: delete a named saved cursor
|
|
1923
|
-
*
|
|
1924
|
-
* @private
|
|
1925
|
-
* @param {Object} params - Command parameters
|
|
1926
|
-
* @param {Object} params.options - CLI options (repo, graph, writer, json)
|
|
1927
|
-
* @param {string[]} params.args - Raw CLI arguments following the `seek` subcommand
|
|
1928
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Command result with payload and exit code
|
|
1929
|
-
* @throws {CliError} On invalid arguments or missing cursors
|
|
2164
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
2165
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
1930
2166
|
*/
|
|
1931
2167
|
async function handleSeek({ options, args }) {
|
|
1932
2168
|
const seekSpec = parseSeekArgs(args);
|
|
1933
2169
|
const { graph, graphName, persistence } = await openGraph(options);
|
|
2170
|
+
void wireSeekCache(graph, persistence, graphName, seekSpec);
|
|
2171
|
+
|
|
2172
|
+
// Handle --clear-cache before discovering ticks (no materialization needed)
|
|
2173
|
+
if (seekSpec.action === 'clear-cache') {
|
|
2174
|
+
if (graph.seekCache) {
|
|
2175
|
+
await graph.seekCache.clear();
|
|
2176
|
+
}
|
|
2177
|
+
return {
|
|
2178
|
+
payload: { graph: graphName, action: 'clear-cache', message: 'Seek cache cleared.' },
|
|
2179
|
+
exitCode: EXIT_CODES.OK,
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
|
|
1934
2183
|
const activeCursor = await readActiveCursor(persistence, graphName);
|
|
1935
2184
|
const { ticks, maxTick, perWriter } = await graph.discoverTicks();
|
|
1936
2185
|
const frontierHash = computeFrontierHash(perWriter);
|
|
@@ -1948,11 +2197,12 @@ async function handleSeek({ options, args }) {
|
|
|
1948
2197
|
};
|
|
1949
2198
|
}
|
|
1950
2199
|
if (seekSpec.action === 'drop') {
|
|
1951
|
-
const
|
|
2200
|
+
const dropName = /** @type {string} */ (seekSpec.name);
|
|
2201
|
+
const existing = await readSavedCursor(persistence, graphName, dropName);
|
|
1952
2202
|
if (!existing) {
|
|
1953
|
-
throw notFoundError(`Saved cursor not found: ${
|
|
2203
|
+
throw notFoundError(`Saved cursor not found: ${dropName}`);
|
|
1954
2204
|
}
|
|
1955
|
-
await deleteSavedCursor(persistence, graphName,
|
|
2205
|
+
await deleteSavedCursor(persistence, graphName, dropName);
|
|
1956
2206
|
return {
|
|
1957
2207
|
payload: {
|
|
1958
2208
|
graph: graphName,
|
|
@@ -1964,8 +2214,16 @@ async function handleSeek({ options, args }) {
|
|
|
1964
2214
|
};
|
|
1965
2215
|
}
|
|
1966
2216
|
if (seekSpec.action === 'latest') {
|
|
2217
|
+
const prevTick = activeCursor ? activeCursor.tick : null;
|
|
2218
|
+
let sdResult = null;
|
|
2219
|
+
if (seekSpec.diff) {
|
|
2220
|
+
sdResult = await computeStructuralDiff({ graph, prevTick, currentTick: maxTick, diffLimit: seekSpec.diffLimit });
|
|
2221
|
+
}
|
|
1967
2222
|
await clearActiveCursor(persistence, graphName);
|
|
1968
|
-
|
|
2223
|
+
// When --diff already materialized at maxTick, skip redundant re-materialize
|
|
2224
|
+
if (!sdResult) {
|
|
2225
|
+
await graph.materialize({ ceiling: maxTick });
|
|
2226
|
+
}
|
|
1969
2227
|
const nodes = await graph.getNodes();
|
|
1970
2228
|
const edges = await graph.getEdges();
|
|
1971
2229
|
const diff = computeSeekStateDiff(activeCursor, { nodes: nodes.length, edges: edges.length }, frontierHash);
|
|
@@ -1984,6 +2242,7 @@ async function handleSeek({ options, args }) {
|
|
|
1984
2242
|
diff,
|
|
1985
2243
|
tickReceipt,
|
|
1986
2244
|
cursor: { active: false },
|
|
2245
|
+
...sdResult,
|
|
1987
2246
|
},
|
|
1988
2247
|
exitCode: EXIT_CODES.OK,
|
|
1989
2248
|
};
|
|
@@ -1992,7 +2251,7 @@ async function handleSeek({ options, args }) {
|
|
|
1992
2251
|
if (!activeCursor) {
|
|
1993
2252
|
throw usageError('No active cursor to save. Use --tick first.');
|
|
1994
2253
|
}
|
|
1995
|
-
await writeSavedCursor(persistence, graphName, seekSpec.name, activeCursor);
|
|
2254
|
+
await writeSavedCursor(persistence, graphName, /** @type {string} */ (seekSpec.name), activeCursor);
|
|
1996
2255
|
return {
|
|
1997
2256
|
payload: {
|
|
1998
2257
|
graph: graphName,
|
|
@@ -2004,11 +2263,20 @@ async function handleSeek({ options, args }) {
|
|
|
2004
2263
|
};
|
|
2005
2264
|
}
|
|
2006
2265
|
if (seekSpec.action === 'load') {
|
|
2007
|
-
const
|
|
2266
|
+
const loadName = /** @type {string} */ (seekSpec.name);
|
|
2267
|
+
const saved = await readSavedCursor(persistence, graphName, loadName);
|
|
2008
2268
|
if (!saved) {
|
|
2009
|
-
throw notFoundError(`Saved cursor not found: ${
|
|
2269
|
+
throw notFoundError(`Saved cursor not found: ${loadName}`);
|
|
2270
|
+
}
|
|
2271
|
+
const prevTick = activeCursor ? activeCursor.tick : null;
|
|
2272
|
+
let sdResult = null;
|
|
2273
|
+
if (seekSpec.diff) {
|
|
2274
|
+
sdResult = await computeStructuralDiff({ graph, prevTick, currentTick: saved.tick, diffLimit: seekSpec.diffLimit });
|
|
2275
|
+
}
|
|
2276
|
+
// When --diff already materialized at saved.tick, skip redundant call
|
|
2277
|
+
if (!sdResult) {
|
|
2278
|
+
await graph.materialize({ ceiling: saved.tick });
|
|
2010
2279
|
}
|
|
2011
|
-
await graph.materialize({ ceiling: saved.tick });
|
|
2012
2280
|
const nodes = await graph.getNodes();
|
|
2013
2281
|
const edges = await graph.getEdges();
|
|
2014
2282
|
await writeActiveCursor(persistence, graphName, { tick: saved.tick, mode: saved.mode ?? 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
|
|
@@ -2029,14 +2297,22 @@ async function handleSeek({ options, args }) {
|
|
|
2029
2297
|
diff,
|
|
2030
2298
|
tickReceipt,
|
|
2031
2299
|
cursor: { active: true, mode: saved.mode, tick: saved.tick, maxTick, name: seekSpec.name },
|
|
2300
|
+
...sdResult,
|
|
2032
2301
|
},
|
|
2033
2302
|
exitCode: EXIT_CODES.OK,
|
|
2034
2303
|
};
|
|
2035
2304
|
}
|
|
2036
2305
|
if (seekSpec.action === 'tick') {
|
|
2037
2306
|
const currentTick = activeCursor ? activeCursor.tick : null;
|
|
2038
|
-
const resolvedTick = resolveTickValue(seekSpec.tickValue, currentTick, ticks, maxTick);
|
|
2039
|
-
|
|
2307
|
+
const resolvedTick = resolveTickValue(/** @type {string} */ (seekSpec.tickValue), currentTick, ticks, maxTick);
|
|
2308
|
+
let sdResult = null;
|
|
2309
|
+
if (seekSpec.diff) {
|
|
2310
|
+
sdResult = await computeStructuralDiff({ graph, prevTick: currentTick, currentTick: resolvedTick, diffLimit: seekSpec.diffLimit });
|
|
2311
|
+
}
|
|
2312
|
+
// When --diff already materialized at resolvedTick, skip redundant call
|
|
2313
|
+
if (!sdResult) {
|
|
2314
|
+
await graph.materialize({ ceiling: resolvedTick });
|
|
2315
|
+
}
|
|
2040
2316
|
const nodes = await graph.getNodes();
|
|
2041
2317
|
const edges = await graph.getEdges();
|
|
2042
2318
|
await writeActiveCursor(persistence, graphName, { tick: resolvedTick, mode: 'lamport', nodes: nodes.length, edges: edges.length, frontierHash });
|
|
@@ -2056,12 +2332,22 @@ async function handleSeek({ options, args }) {
|
|
|
2056
2332
|
diff,
|
|
2057
2333
|
tickReceipt,
|
|
2058
2334
|
cursor: { active: true, mode: 'lamport', tick: resolvedTick, maxTick, name: 'active' },
|
|
2335
|
+
...sdResult,
|
|
2059
2336
|
},
|
|
2060
2337
|
exitCode: EXIT_CODES.OK,
|
|
2061
2338
|
};
|
|
2062
2339
|
}
|
|
2063
2340
|
|
|
2064
2341
|
// status (bare seek)
|
|
2342
|
+
return await handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash });
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* Handles the `status` sub-action of `seek` (bare seek with no action flag).
|
|
2347
|
+
* @param {{graph: WarpGraphInstance, graphName: string, persistence: Persistence, activeCursor: CursorBlob|null, ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>, frontierHash: string}} params
|
|
2348
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
2349
|
+
*/
|
|
2350
|
+
async function handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash }) {
|
|
2065
2351
|
if (activeCursor) {
|
|
2066
2352
|
await graph.materialize({ ceiling: activeCursor.tick });
|
|
2067
2353
|
const nodes = await graph.getNodes();
|
|
@@ -2117,11 +2403,11 @@ async function handleSeek({ options, args }) {
|
|
|
2117
2403
|
/**
|
|
2118
2404
|
* Converts the per-writer Map from discoverTicks() into a plain object for JSON output.
|
|
2119
2405
|
*
|
|
2120
|
-
* @
|
|
2121
|
-
* @
|
|
2122
|
-
* @returns {Object<string, {ticks: number[], tipSha: string|null}>} Plain object keyed by writer ID
|
|
2406
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer tick data
|
|
2407
|
+
* @returns {Record<string, WriterTickInfo>} Plain object keyed by writer ID
|
|
2123
2408
|
*/
|
|
2124
2409
|
function serializePerWriter(perWriter) {
|
|
2410
|
+
/** @type {Record<string, WriterTickInfo>} */
|
|
2125
2411
|
const result = {};
|
|
2126
2412
|
for (const [writerId, info] of perWriter) {
|
|
2127
2413
|
result[writerId] = { ticks: info.ticks, tipSha: info.tipSha, tickShas: info.tickShas };
|
|
@@ -2132,9 +2418,8 @@ function serializePerWriter(perWriter) {
|
|
|
2132
2418
|
/**
|
|
2133
2419
|
* Counts the total number of patches across all writers at or before the given tick.
|
|
2134
2420
|
*
|
|
2135
|
-
* @private
|
|
2136
2421
|
* @param {number} tick - Lamport tick ceiling (inclusive)
|
|
2137
|
-
* @param {Map<string,
|
|
2422
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer tick data
|
|
2138
2423
|
* @returns {number} Total patch count at or before the given tick
|
|
2139
2424
|
*/
|
|
2140
2425
|
function countPatchesAtTick(tick, perWriter) {
|
|
@@ -2155,11 +2440,11 @@ function countPatchesAtTick(tick, perWriter) {
|
|
|
2155
2440
|
* Used to suppress seek diffs when graph history may have changed since the
|
|
2156
2441
|
* previous cursor snapshot (e.g. new writers/patches, rewritten refs).
|
|
2157
2442
|
*
|
|
2158
|
-
* @
|
|
2159
|
-
* @param {Map<string, {tipSha: string|null}>} perWriter - Per-writer metadata from discoverTicks()
|
|
2443
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer metadata from discoverTicks()
|
|
2160
2444
|
* @returns {string} Hex digest of the frontier fingerprint
|
|
2161
2445
|
*/
|
|
2162
2446
|
function computeFrontierHash(perWriter) {
|
|
2447
|
+
/** @type {Record<string, string|null>} */
|
|
2163
2448
|
const tips = {};
|
|
2164
2449
|
for (const [writerId, info] of perWriter) {
|
|
2165
2450
|
tips[writerId] = info?.tipSha || null;
|
|
@@ -2173,8 +2458,7 @@ function computeFrontierHash(perWriter) {
|
|
|
2173
2458
|
* Counts may be missing for older cursors (pre-diff support). In that case
|
|
2174
2459
|
* callers should treat the counts as unknown and suppress diffs.
|
|
2175
2460
|
*
|
|
2176
|
-
* @
|
|
2177
|
-
* @param {Object|null} cursor - Parsed cursor blob object
|
|
2461
|
+
* @param {CursorBlob|null} cursor - Parsed cursor blob object
|
|
2178
2462
|
* @returns {{nodes: number|null, edges: number|null}} Parsed counts
|
|
2179
2463
|
*/
|
|
2180
2464
|
function readSeekCounts(cursor) {
|
|
@@ -2192,8 +2476,7 @@ function readSeekCounts(cursor) {
|
|
|
2192
2476
|
*
|
|
2193
2477
|
* Returns null if the previous cursor is missing cached counts.
|
|
2194
2478
|
*
|
|
2195
|
-
* @
|
|
2196
|
-
* @param {Object|null} prevCursor - Cursor object read before updating the position
|
|
2479
|
+
* @param {CursorBlob|null} prevCursor - Cursor object read before updating the position
|
|
2197
2480
|
* @param {{nodes: number, edges: number}} next - Current materialized counts
|
|
2198
2481
|
* @param {string} frontierHash - Frontier fingerprint of the current graph
|
|
2199
2482
|
* @returns {{nodes: number, edges: number}|null} Diff object or null when unknown
|
|
@@ -2220,22 +2503,19 @@ function computeSeekStateDiff(prevCursor, next, frontierHash) {
|
|
|
2220
2503
|
* summarizes patch ops. Typically only a handful of writers have a patch at any
|
|
2221
2504
|
* single Lamport tick.
|
|
2222
2505
|
*
|
|
2223
|
-
* @
|
|
2224
|
-
* @
|
|
2225
|
-
* @param {number} params.tick - Lamport tick to summarize
|
|
2226
|
-
* @param {Map<string, {tickShas?: Object}>} params.perWriter - Per-writer tick metadata from discoverTicks()
|
|
2227
|
-
* @param {Object} params.graph - WarpGraph instance
|
|
2228
|
-
* @returns {Promise<Object<string, Object>|null>} Map of writerId → { sha, opSummary }, or null if empty
|
|
2506
|
+
* @param {{tick: number, perWriter: Map<string, WriterTickInfo>, graph: WarpGraphInstance}} params
|
|
2507
|
+
* @returns {Promise<Record<string, {sha: string, opSummary: *}>|null>} Map of writerId to { sha, opSummary }, or null if empty
|
|
2229
2508
|
*/
|
|
2230
2509
|
async function buildTickReceipt({ tick, perWriter, graph }) {
|
|
2231
2510
|
if (!Number.isInteger(tick) || tick <= 0) {
|
|
2232
2511
|
return null;
|
|
2233
2512
|
}
|
|
2234
2513
|
|
|
2514
|
+
/** @type {Record<string, {sha: string, opSummary: *}>} */
|
|
2235
2515
|
const receipt = {};
|
|
2236
2516
|
|
|
2237
2517
|
for (const [writerId, info] of perWriter) {
|
|
2238
|
-
const sha = info?.tickShas?.[tick];
|
|
2518
|
+
const sha = /** @type {*} */ (info?.tickShas)?.[tick]; // TODO(ts-cleanup): type CLI payload
|
|
2239
2519
|
if (!sha) {
|
|
2240
2520
|
continue;
|
|
2241
2521
|
}
|
|
@@ -2248,17 +2528,89 @@ async function buildTickReceipt({ tick, perWriter, graph }) {
|
|
|
2248
2528
|
return Object.keys(receipt).length > 0 ? receipt : null;
|
|
2249
2529
|
}
|
|
2250
2530
|
|
|
2531
|
+
/**
|
|
2532
|
+
* Computes a structural diff between the state at a previous tick and
|
|
2533
|
+
* the state at the current tick.
|
|
2534
|
+
*
|
|
2535
|
+
* Materializes the baseline tick first, snapshots the state, then
|
|
2536
|
+
* materializes the target tick and calls diffStates() between the two.
|
|
2537
|
+
* Applies diffLimit truncation when the total change count exceeds the cap.
|
|
2538
|
+
*
|
|
2539
|
+
* @param {{graph: WarpGraphInstance, prevTick: number|null, currentTick: number, diffLimit: number}} params
|
|
2540
|
+
* @returns {Promise<{structuralDiff: *, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}>}
|
|
2541
|
+
*/
|
|
2542
|
+
async function computeStructuralDiff({ graph, prevTick, currentTick, diffLimit }) {
|
|
2543
|
+
let beforeState = null;
|
|
2544
|
+
let diffBaseline = 'empty';
|
|
2545
|
+
let baselineTick = null;
|
|
2546
|
+
|
|
2547
|
+
// Short-circuit: same tick produces an empty diff
|
|
2548
|
+
if (prevTick !== null && prevTick === currentTick) {
|
|
2549
|
+
const empty = { nodes: { added: [], removed: [] }, edges: { added: [], removed: [] }, props: { set: [], removed: [] } };
|
|
2550
|
+
return { structuralDiff: empty, diffBaseline: 'tick', baselineTick: prevTick, truncated: false, totalChanges: 0, shownChanges: 0 };
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
if (prevTick !== null && prevTick > 0) {
|
|
2554
|
+
await graph.materialize({ ceiling: prevTick });
|
|
2555
|
+
beforeState = await graph.getStateSnapshot();
|
|
2556
|
+
diffBaseline = 'tick';
|
|
2557
|
+
baselineTick = prevTick;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
await graph.materialize({ ceiling: currentTick });
|
|
2561
|
+
const afterState = /** @type {*} */ (await graph.getStateSnapshot()); // TODO(ts-cleanup): narrow WarpStateV5
|
|
2562
|
+
const diff = diffStates(beforeState, afterState);
|
|
2563
|
+
|
|
2564
|
+
return applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Applies truncation limits to a structural diff result.
|
|
2569
|
+
*
|
|
2570
|
+
* @param {*} diff
|
|
2571
|
+
* @param {string} diffBaseline
|
|
2572
|
+
* @param {number|null} baselineTick
|
|
2573
|
+
* @param {number} diffLimit
|
|
2574
|
+
* @returns {{structuralDiff: *, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}}
|
|
2575
|
+
*/
|
|
2576
|
+
function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
|
|
2577
|
+
const totalChanges =
|
|
2578
|
+
diff.nodes.added.length + diff.nodes.removed.length +
|
|
2579
|
+
diff.edges.added.length + diff.edges.removed.length +
|
|
2580
|
+
diff.props.set.length + diff.props.removed.length;
|
|
2581
|
+
|
|
2582
|
+
if (totalChanges <= diffLimit) {
|
|
2583
|
+
return { structuralDiff: diff, diffBaseline, baselineTick, truncated: false, totalChanges, shownChanges: totalChanges };
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Truncate sequentially (nodes → edges → props), keeping sort order within each category
|
|
2587
|
+
let remaining = diffLimit;
|
|
2588
|
+
const cap = (/** @type {any[]} */ arr) => {
|
|
2589
|
+
const take = Math.min(arr.length, remaining);
|
|
2590
|
+
remaining -= take;
|
|
2591
|
+
return arr.slice(0, take);
|
|
2592
|
+
};
|
|
2593
|
+
|
|
2594
|
+
const capped = {
|
|
2595
|
+
nodes: { added: cap(diff.nodes.added), removed: cap(diff.nodes.removed) },
|
|
2596
|
+
edges: { added: cap(diff.edges.added), removed: cap(diff.edges.removed) },
|
|
2597
|
+
props: { set: cap(diff.props.set), removed: cap(diff.props.removed) },
|
|
2598
|
+
};
|
|
2599
|
+
|
|
2600
|
+
const shownChanges = diffLimit - remaining;
|
|
2601
|
+
return { structuralDiff: capped, diffBaseline, baselineTick, truncated: true, totalChanges, shownChanges };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2251
2604
|
/**
|
|
2252
2605
|
* Renders a seek command payload as a human-readable string for terminal output.
|
|
2253
2606
|
*
|
|
2254
2607
|
* Handles all seek actions: list, drop, save, latest, load, tick, and status.
|
|
2255
2608
|
*
|
|
2256
|
-
* @
|
|
2257
|
-
* @param {Object} payload - Seek result payload from handleSeek
|
|
2609
|
+
* @param {*} payload - Seek result payload from handleSeek
|
|
2258
2610
|
* @returns {string} Formatted output string (includes trailing newline)
|
|
2259
2611
|
*/
|
|
2260
2612
|
function renderSeek(payload) {
|
|
2261
|
-
const formatDelta = (n) => {
|
|
2613
|
+
const formatDelta = (/** @type {*} */ n) => { // TODO(ts-cleanup): type CLI payload
|
|
2262
2614
|
if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
|
|
2263
2615
|
return '';
|
|
2264
2616
|
}
|
|
@@ -2266,7 +2618,7 @@ function renderSeek(payload) {
|
|
|
2266
2618
|
return ` (${sign}${n})`;
|
|
2267
2619
|
};
|
|
2268
2620
|
|
|
2269
|
-
const formatOpSummaryPlain = (summary) => {
|
|
2621
|
+
const formatOpSummaryPlain = (/** @type {*} */ summary) => { // TODO(ts-cleanup): type CLI payload
|
|
2270
2622
|
const order = [
|
|
2271
2623
|
['NodeAdd', '+', 'node'],
|
|
2272
2624
|
['EdgeAdd', '+', 'edge'],
|
|
@@ -2286,7 +2638,7 @@ function renderSeek(payload) {
|
|
|
2286
2638
|
return parts.length > 0 ? parts.join(' ') : '(empty)';
|
|
2287
2639
|
};
|
|
2288
2640
|
|
|
2289
|
-
const appendReceiptSummary = (baseLine) => {
|
|
2641
|
+
const appendReceiptSummary = (/** @type {string} */ baseLine) => {
|
|
2290
2642
|
const tickReceipt = payload?.tickReceipt;
|
|
2291
2643
|
if (!tickReceipt || typeof tickReceipt !== 'object') {
|
|
2292
2644
|
return `${baseLine}\n`;
|
|
@@ -2322,6 +2674,10 @@ function renderSeek(payload) {
|
|
|
2322
2674
|
};
|
|
2323
2675
|
};
|
|
2324
2676
|
|
|
2677
|
+
if (payload.action === 'clear-cache') {
|
|
2678
|
+
return `${payload.message}\n`;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2325
2681
|
if (payload.action === 'list') {
|
|
2326
2682
|
if (payload.cursors.length === 0) {
|
|
2327
2683
|
return 'No saved cursors.\n';
|
|
@@ -2344,26 +2700,29 @@ function renderSeek(payload) {
|
|
|
2344
2700
|
|
|
2345
2701
|
if (payload.action === 'latest') {
|
|
2346
2702
|
const { nodesStr, edgesStr } = buildStateStrings();
|
|
2347
|
-
|
|
2703
|
+
const base = appendReceiptSummary(
|
|
2348
2704
|
`${payload.graph}: returned to present (tick ${payload.maxTick}, ${nodesStr}, ${edgesStr})`,
|
|
2349
2705
|
);
|
|
2706
|
+
return base + formatStructuralDiff(payload);
|
|
2350
2707
|
}
|
|
2351
2708
|
|
|
2352
2709
|
if (payload.action === 'load') {
|
|
2353
2710
|
const { nodesStr, edgesStr } = buildStateStrings();
|
|
2354
|
-
|
|
2711
|
+
const base = appendReceiptSummary(
|
|
2355
2712
|
`${payload.graph}: loaded cursor "${payload.name}" at tick ${payload.tick} of ${payload.maxTick} (${nodesStr}, ${edgesStr})`,
|
|
2356
2713
|
);
|
|
2714
|
+
return base + formatStructuralDiff(payload);
|
|
2357
2715
|
}
|
|
2358
2716
|
|
|
2359
2717
|
if (payload.action === 'tick') {
|
|
2360
2718
|
const { nodesStr, edgesStr, patchesStr } = buildStateStrings();
|
|
2361
|
-
|
|
2719
|
+
const base = appendReceiptSummary(
|
|
2362
2720
|
`${payload.graph}: tick ${payload.tick} of ${payload.maxTick} (${nodesStr}, ${edgesStr}, ${patchesStr})`,
|
|
2363
2721
|
);
|
|
2722
|
+
return base + formatStructuralDiff(payload);
|
|
2364
2723
|
}
|
|
2365
2724
|
|
|
2366
|
-
// status
|
|
2725
|
+
// status (structuralDiff is never populated here; no formatStructuralDiff call)
|
|
2367
2726
|
if (payload.cursor && payload.cursor.active) {
|
|
2368
2727
|
const { nodesStr, edgesStr, patchesStr } = buildStateStrings();
|
|
2369
2728
|
return appendReceiptSummary(
|
|
@@ -2381,9 +2740,8 @@ function renderSeek(payload) {
|
|
|
2381
2740
|
* Called by non-seek commands (query, path, check, etc.) that should
|
|
2382
2741
|
* honour an active seek cursor.
|
|
2383
2742
|
*
|
|
2384
|
-
* @
|
|
2385
|
-
* @param {
|
|
2386
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
2743
|
+
* @param {WarpGraphInstance} graph - WarpGraph instance
|
|
2744
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
2387
2745
|
* @param {string} graphName - Name of the WARP graph
|
|
2388
2746
|
* @returns {Promise<{active: boolean, tick: number|null, maxTick: number|null}>} Cursor info — maxTick is always null; non-seek commands intentionally skip discoverTicks() for performance
|
|
2389
2747
|
*/
|
|
@@ -2405,7 +2763,6 @@ async function applyCursorCeiling(graph, persistence, graphName) {
|
|
|
2405
2763
|
* maxTick to avoid the cost of discoverTicks(); the banner then omits the
|
|
2406
2764
|
* "of {maxTick}" suffix. Only the seek handler itself populates maxTick.
|
|
2407
2765
|
*
|
|
2408
|
-
* @private
|
|
2409
2766
|
* @param {{active: boolean, tick: number|null, maxTick: number|null}} cursorInfo - Result from applyCursorCeiling
|
|
2410
2767
|
* @param {number|null} maxTick - Maximum Lamport tick (from discoverTicks), or null if unknown
|
|
2411
2768
|
* @returns {void}
|
|
@@ -2417,6 +2774,10 @@ function emitCursorWarning(cursorInfo, maxTick) {
|
|
|
2417
2774
|
}
|
|
2418
2775
|
}
|
|
2419
2776
|
|
|
2777
|
+
/**
|
|
2778
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
2779
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
2780
|
+
*/
|
|
2420
2781
|
async function handleView({ options, args }) {
|
|
2421
2782
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2422
2783
|
throw usageError('view command requires an interactive terminal (TTY)');
|
|
@@ -2427,13 +2788,14 @@ async function handleView({ options, args }) {
|
|
|
2427
2788
|
: 'list';
|
|
2428
2789
|
|
|
2429
2790
|
try {
|
|
2791
|
+
// @ts-expect-error — optional peer dependency, may not be installed
|
|
2430
2792
|
const { startTui } = await import('@git-stunts/git-warp-tui');
|
|
2431
2793
|
await startTui({
|
|
2432
2794
|
repo: options.repo || '.',
|
|
2433
2795
|
graph: options.graph || 'default',
|
|
2434
2796
|
mode: viewMode,
|
|
2435
2797
|
});
|
|
2436
|
-
} catch (err) {
|
|
2798
|
+
} catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
|
|
2437
2799
|
if (err.code === 'ERR_MODULE_NOT_FOUND' || (err.message && err.message.includes('Cannot find module'))) {
|
|
2438
2800
|
throw usageError(
|
|
2439
2801
|
'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
|
|
@@ -2445,7 +2807,8 @@ async function handleView({ options, args }) {
|
|
|
2445
2807
|
return { payload: undefined, exitCode: 0 };
|
|
2446
2808
|
}
|
|
2447
2809
|
|
|
2448
|
-
|
|
2810
|
+
/** @type {Map<string, Function>} */
|
|
2811
|
+
const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
|
|
2449
2812
|
['info', handleInfo],
|
|
2450
2813
|
['query', handleQuery],
|
|
2451
2814
|
['path', handlePath],
|
|
@@ -2455,7 +2818,7 @@ const COMMANDS = new Map([
|
|
|
2455
2818
|
['seek', handleSeek],
|
|
2456
2819
|
['view', handleView],
|
|
2457
2820
|
['install-hooks', handleInstallHooks],
|
|
2458
|
-
]);
|
|
2821
|
+
]));
|
|
2459
2822
|
|
|
2460
2823
|
/**
|
|
2461
2824
|
* CLI entry point. Parses arguments, dispatches to the appropriate command handler,
|
|
@@ -2492,12 +2855,13 @@ async function main() {
|
|
|
2492
2855
|
throw usageError(`--view is not supported for '${command}'. Supported commands: ${VIEW_SUPPORTED_COMMANDS.join(', ')}`);
|
|
2493
2856
|
}
|
|
2494
2857
|
|
|
2495
|
-
const result = await handler({
|
|
2858
|
+
const result = await /** @type {Function} */ (handler)({
|
|
2496
2859
|
command,
|
|
2497
2860
|
args: positionals.slice(1),
|
|
2498
2861
|
options,
|
|
2499
2862
|
});
|
|
2500
2863
|
|
|
2864
|
+
/** @type {{payload: *, exitCode: number}} */
|
|
2501
2865
|
const normalized = result && typeof result === 'object' && 'payload' in result
|
|
2502
2866
|
? result
|
|
2503
2867
|
: { payload: result, exitCode: EXIT_CODES.OK };
|
|
@@ -2505,13 +2869,15 @@ async function main() {
|
|
|
2505
2869
|
if (normalized.payload !== undefined) {
|
|
2506
2870
|
emit(normalized.payload, { json: options.json, command, view: options.view });
|
|
2507
2871
|
}
|
|
2508
|
-
process.
|
|
2872
|
+
// Use process.exit() to avoid waiting for fire-and-forget I/O (e.g. seek cache writes).
|
|
2873
|
+
process.exit(normalized.exitCode ?? EXIT_CODES.OK);
|
|
2509
2874
|
}
|
|
2510
2875
|
|
|
2511
2876
|
main().catch((error) => {
|
|
2512
2877
|
const exitCode = error instanceof CliError ? error.exitCode : EXIT_CODES.INTERNAL;
|
|
2513
2878
|
const code = error instanceof CliError ? error.code : 'E_INTERNAL';
|
|
2514
2879
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
2880
|
+
/** @type {{error: {code: string, message: string, cause?: *}}} */
|
|
2515
2881
|
const payload = { error: { code, message } };
|
|
2516
2882
|
|
|
2517
2883
|
if (error && error.cause) {
|
|
@@ -2523,5 +2889,5 @@ main().catch((error) => {
|
|
|
2523
2889
|
} else {
|
|
2524
2890
|
process.stderr.write(renderError(payload));
|
|
2525
2891
|
}
|
|
2526
|
-
process.exitCode
|
|
2892
|
+
process.exit(exitCode);
|
|
2527
2893
|
});
|