@git-stunts/git-warp 10.3.2 → 10.4.2
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/bin/warp-graph.js +371 -141
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +8 -3
- package/src/domain/WarpGraph.js +263 -147
- 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 +15 -14
- 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/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 +16 -27
- 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/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 +19 -5
- 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';
|
|
@@ -34,6 +36,85 @@ import { renderGraphView } from '../src/visualization/renderers/ascii/graph.js';
|
|
|
34
36
|
import { renderSvg } from '../src/visualization/renderers/svg/index.js';
|
|
35
37
|
import { layoutGraph, queryResultToGraphData, pathResultToGraphData } from '../src/visualization/layouts/index.js';
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} Persistence
|
|
41
|
+
* @property {(prefix: string) => Promise<string[]>} listRefs
|
|
42
|
+
* @property {(ref: string) => Promise<string|null>} readRef
|
|
43
|
+
* @property {(ref: string, oid: string) => Promise<void>} updateRef
|
|
44
|
+
* @property {(ref: string) => Promise<void>} deleteRef
|
|
45
|
+
* @property {(oid: string) => Promise<Buffer>} readBlob
|
|
46
|
+
* @property {(buf: Buffer) => Promise<string>} writeBlob
|
|
47
|
+
* @property {(sha: string) => Promise<{date?: string|null}>} getNodeInfo
|
|
48
|
+
* @property {(sha: string, coverageSha: string) => Promise<boolean>} isAncestor
|
|
49
|
+
* @property {() => Promise<{ok: boolean}>} ping
|
|
50
|
+
* @property {*} plumbing
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} WarpGraphInstance
|
|
55
|
+
* @property {(opts?: {ceiling?: number}) => Promise<void>} materialize
|
|
56
|
+
* @property {() => Promise<Array<{id: string}>>} getNodes
|
|
57
|
+
* @property {() => Promise<Array<{from: string, to: string, label?: string}>>} getEdges
|
|
58
|
+
* @property {() => Promise<string|null>} createCheckpoint
|
|
59
|
+
* @property {() => *} query
|
|
60
|
+
* @property {{ shortestPath: Function }} traverse
|
|
61
|
+
* @property {(writerId: string) => Promise<Array<{patch: any, sha: string}>>} getWriterPatches
|
|
62
|
+
* @property {() => Promise<{frontier: Record<string, any>}>} status
|
|
63
|
+
* @property {() => Promise<Map<string, any>>} getFrontier
|
|
64
|
+
* @property {() => {totalTombstones: number, tombstoneRatio: number}} getGCMetrics
|
|
65
|
+
* @property {() => Promise<number>} getPropertyCount
|
|
66
|
+
* @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
|
|
67
|
+
* @property {(sha: string) => Promise<{ops?: any[]}>} loadPatchBySha
|
|
68
|
+
* @property {(cache: any) => void} setSeekCache
|
|
69
|
+
* @property {*} seekCache
|
|
70
|
+
* @property {number} [_seekCeiling]
|
|
71
|
+
* @property {boolean} [_provenanceDegraded]
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} WriterTickInfo
|
|
76
|
+
* @property {number[]} ticks
|
|
77
|
+
* @property {string|null} tipSha
|
|
78
|
+
* @property {Record<number, string>} [tickShas]
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {Object} CursorBlob
|
|
83
|
+
* @property {number} tick
|
|
84
|
+
* @property {string} [mode]
|
|
85
|
+
* @property {number} [nodes]
|
|
86
|
+
* @property {number} [edges]
|
|
87
|
+
* @property {string} [frontierHash]
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {Object} CliOptions
|
|
92
|
+
* @property {string} repo
|
|
93
|
+
* @property {boolean} json
|
|
94
|
+
* @property {string|null} view
|
|
95
|
+
* @property {string|null} graph
|
|
96
|
+
* @property {string} writer
|
|
97
|
+
* @property {boolean} help
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {Object} GraphInfoResult
|
|
102
|
+
* @property {string} name
|
|
103
|
+
* @property {{count: number, ids?: string[]}} writers
|
|
104
|
+
* @property {{ref: string, sha: string|null, date?: string|null}} [checkpoint]
|
|
105
|
+
* @property {{ref: string, sha: string|null}} [coverage]
|
|
106
|
+
* @property {Record<string, number>} [writerPatches]
|
|
107
|
+
* @property {{active: boolean, tick?: number, mode?: string}} [cursor]
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {Object} SeekSpec
|
|
112
|
+
* @property {string} action
|
|
113
|
+
* @property {string|null} tickValue
|
|
114
|
+
* @property {string|null} name
|
|
115
|
+
* @property {boolean} noPersistentCache
|
|
116
|
+
*/
|
|
117
|
+
|
|
37
118
|
const EXIT_CODES = {
|
|
38
119
|
OK: 0,
|
|
39
120
|
USAGE: 1,
|
|
@@ -111,20 +192,25 @@ class CliError extends Error {
|
|
|
111
192
|
}
|
|
112
193
|
}
|
|
113
194
|
|
|
195
|
+
/** @param {string} message */
|
|
114
196
|
function usageError(message) {
|
|
115
197
|
return new CliError(message, { code: 'E_USAGE', exitCode: EXIT_CODES.USAGE });
|
|
116
198
|
}
|
|
117
199
|
|
|
200
|
+
/** @param {string} message */
|
|
118
201
|
function notFoundError(message) {
|
|
119
202
|
return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
|
|
120
203
|
}
|
|
121
204
|
|
|
205
|
+
/** @param {*} value */
|
|
122
206
|
function stableStringify(value) {
|
|
207
|
+
/** @param {*} input @returns {*} */
|
|
123
208
|
const normalize = (input) => {
|
|
124
209
|
if (Array.isArray(input)) {
|
|
125
210
|
return input.map(normalize);
|
|
126
211
|
}
|
|
127
212
|
if (input && typeof input === 'object') {
|
|
213
|
+
/** @type {Record<string, *>} */
|
|
128
214
|
const sorted = {};
|
|
129
215
|
for (const key of Object.keys(input).sort()) {
|
|
130
216
|
sorted[key] = normalize(input[key]);
|
|
@@ -137,8 +223,10 @@ function stableStringify(value) {
|
|
|
137
223
|
return JSON.stringify(normalize(value), null, 2);
|
|
138
224
|
}
|
|
139
225
|
|
|
226
|
+
/** @param {string[]} argv */
|
|
140
227
|
function parseArgs(argv) {
|
|
141
228
|
const options = createDefaultOptions();
|
|
229
|
+
/** @type {string[]} */
|
|
142
230
|
const positionals = [];
|
|
143
231
|
const optionDefs = [
|
|
144
232
|
{ flag: '--repo', shortFlag: '-r', key: 'repo' },
|
|
@@ -169,6 +257,14 @@ function createDefaultOptions() {
|
|
|
169
257
|
};
|
|
170
258
|
}
|
|
171
259
|
|
|
260
|
+
/**
|
|
261
|
+
* @param {Object} params
|
|
262
|
+
* @param {string[]} params.argv
|
|
263
|
+
* @param {number} params.index
|
|
264
|
+
* @param {Record<string, *>} params.options
|
|
265
|
+
* @param {Array<{flag: string, shortFlag?: string, key: string}>} params.optionDefs
|
|
266
|
+
* @param {string[]} params.positionals
|
|
267
|
+
*/
|
|
172
268
|
function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
173
269
|
const arg = argv[index];
|
|
174
270
|
|
|
@@ -220,8 +316,10 @@ function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
|
220
316
|
shortFlag: matched.shortFlag,
|
|
221
317
|
allowEmpty: false,
|
|
222
318
|
});
|
|
223
|
-
|
|
224
|
-
|
|
319
|
+
if (result) {
|
|
320
|
+
options[matched.key] = result.value;
|
|
321
|
+
return { consumed: result.consumed };
|
|
322
|
+
}
|
|
225
323
|
}
|
|
226
324
|
|
|
227
325
|
if (arg.startsWith('-')) {
|
|
@@ -232,6 +330,10 @@ function consumeBaseArg({ argv, index, options, optionDefs, positionals }) {
|
|
|
232
330
|
return { consumed: argv.length - index - 1, done: true };
|
|
233
331
|
}
|
|
234
332
|
|
|
333
|
+
/**
|
|
334
|
+
* @param {string} arg
|
|
335
|
+
* @param {Array<{flag: string, shortFlag?: string, key: string}>} optionDefs
|
|
336
|
+
*/
|
|
235
337
|
function matchOptionDef(arg, optionDefs) {
|
|
236
338
|
return optionDefs.find((def) =>
|
|
237
339
|
arg === def.flag ||
|
|
@@ -240,6 +342,7 @@ function matchOptionDef(arg, optionDefs) {
|
|
|
240
342
|
);
|
|
241
343
|
}
|
|
242
344
|
|
|
345
|
+
/** @param {string} repoPath @returns {Promise<{persistence: Persistence}>} */
|
|
243
346
|
async function createPersistence(repoPath) {
|
|
244
347
|
const runner = ShellRunnerFactory.create();
|
|
245
348
|
const plumbing = new GitPlumbing({ cwd: repoPath, runner });
|
|
@@ -251,6 +354,7 @@ async function createPersistence(repoPath) {
|
|
|
251
354
|
return { persistence };
|
|
252
355
|
}
|
|
253
356
|
|
|
357
|
+
/** @param {Persistence} persistence @returns {Promise<string[]>} */
|
|
254
358
|
async function listGraphNames(persistence) {
|
|
255
359
|
if (typeof persistence.listRefs !== 'function') {
|
|
256
360
|
return [];
|
|
@@ -273,6 +377,11 @@ async function listGraphNames(persistence) {
|
|
|
273
377
|
return [...names].sort();
|
|
274
378
|
}
|
|
275
379
|
|
|
380
|
+
/**
|
|
381
|
+
* @param {Persistence} persistence
|
|
382
|
+
* @param {string|null} explicitGraph
|
|
383
|
+
* @returns {Promise<string>}
|
|
384
|
+
*/
|
|
276
385
|
async function resolveGraphName(persistence, explicitGraph) {
|
|
277
386
|
if (explicitGraph) {
|
|
278
387
|
return explicitGraph;
|
|
@@ -289,14 +398,14 @@ async function resolveGraphName(persistence, explicitGraph) {
|
|
|
289
398
|
|
|
290
399
|
/**
|
|
291
400
|
* Collects metadata about a single graph (writer count, refs, patches, checkpoint).
|
|
292
|
-
* @param {
|
|
401
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
293
402
|
* @param {string} graphName - Name of the graph to inspect
|
|
294
403
|
* @param {Object} [options]
|
|
295
404
|
* @param {boolean} [options.includeWriterIds=false] - Include writer ID list
|
|
296
405
|
* @param {boolean} [options.includeRefs=false] - Include checkpoint/coverage refs
|
|
297
406
|
* @param {boolean} [options.includeWriterPatches=false] - Include per-writer patch counts
|
|
298
407
|
* @param {boolean} [options.includeCheckpointDate=false] - Include checkpoint date
|
|
299
|
-
* @returns {Promise<
|
|
408
|
+
* @returns {Promise<GraphInfoResult>} Graph info object
|
|
300
409
|
*/
|
|
301
410
|
async function getGraphInfo(persistence, graphName, {
|
|
302
411
|
includeWriterIds = false,
|
|
@@ -308,11 +417,12 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
308
417
|
const writerRefs = typeof persistence.listRefs === 'function'
|
|
309
418
|
? await persistence.listRefs(writersPrefix)
|
|
310
419
|
: [];
|
|
311
|
-
const writerIds = writerRefs
|
|
420
|
+
const writerIds = /** @type {string[]} */ (writerRefs
|
|
312
421
|
.map((ref) => parseWriterIdFromRef(ref))
|
|
313
422
|
.filter(Boolean)
|
|
314
|
-
.sort();
|
|
423
|
+
.sort());
|
|
315
424
|
|
|
425
|
+
/** @type {GraphInfoResult} */
|
|
316
426
|
const info = {
|
|
317
427
|
name: graphName,
|
|
318
428
|
writers: {
|
|
@@ -328,6 +438,7 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
328
438
|
const checkpointRef = buildCheckpointRef(graphName);
|
|
329
439
|
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
330
440
|
|
|
441
|
+
/** @type {{ref: string, sha: string|null, date?: string|null}} */
|
|
331
442
|
const checkpoint = { ref: checkpointRef, sha: checkpointSha || null };
|
|
332
443
|
|
|
333
444
|
if (includeCheckpointDate && checkpointSha) {
|
|
@@ -351,10 +462,11 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
351
462
|
writerId: 'cli',
|
|
352
463
|
crypto: new NodeCryptoAdapter(),
|
|
353
464
|
});
|
|
465
|
+
/** @type {Record<string, number>} */
|
|
354
466
|
const writerPatches = {};
|
|
355
467
|
for (const writerId of writerIds) {
|
|
356
468
|
const patches = await graph.getWriterPatches(writerId);
|
|
357
|
-
writerPatches[writerId] = patches.length;
|
|
469
|
+
writerPatches[/** @type {string} */ (writerId)] = patches.length;
|
|
358
470
|
}
|
|
359
471
|
info.writerPatches = writerPatches;
|
|
360
472
|
}
|
|
@@ -364,11 +476,8 @@ async function getGraphInfo(persistence, graphName, {
|
|
|
364
476
|
|
|
365
477
|
/**
|
|
366
478
|
* 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}>}
|
|
479
|
+
* @param {CliOptions} options - Parsed CLI options
|
|
480
|
+
* @returns {Promise<{graph: WarpGraphInstance, graphName: string, persistence: Persistence}>}
|
|
372
481
|
* @throws {CliError} If the specified graph is not found
|
|
373
482
|
*/
|
|
374
483
|
async function openGraph(options) {
|
|
@@ -380,15 +489,16 @@ async function openGraph(options) {
|
|
|
380
489
|
throw notFoundError(`Graph not found: ${options.graph}`);
|
|
381
490
|
}
|
|
382
491
|
}
|
|
383
|
-
const graph = await WarpGraph.open({
|
|
492
|
+
const graph = /** @type {WarpGraphInstance} */ (/** @type {*} */ (await WarpGraph.open({ // TODO(ts-cleanup): narrow port type
|
|
384
493
|
persistence,
|
|
385
494
|
graphName,
|
|
386
495
|
writerId: options.writer,
|
|
387
496
|
crypto: new NodeCryptoAdapter(),
|
|
388
|
-
});
|
|
497
|
+
})));
|
|
389
498
|
return { graph, graphName, persistence };
|
|
390
499
|
}
|
|
391
500
|
|
|
501
|
+
/** @param {string[]} args */
|
|
392
502
|
function parseQueryArgs(args) {
|
|
393
503
|
const spec = {
|
|
394
504
|
match: null,
|
|
@@ -407,6 +517,11 @@ function parseQueryArgs(args) {
|
|
|
407
517
|
return spec;
|
|
408
518
|
}
|
|
409
519
|
|
|
520
|
+
/**
|
|
521
|
+
* @param {string[]} args
|
|
522
|
+
* @param {number} index
|
|
523
|
+
* @param {{match: string|null, select: string[]|null, steps: Array<{type: string, label?: string, key?: string, value?: string}>}} spec
|
|
524
|
+
*/
|
|
410
525
|
function consumeQueryArg(args, index, spec) {
|
|
411
526
|
const stepResult = readTraversalStep(args, index);
|
|
412
527
|
if (stepResult) {
|
|
@@ -450,6 +565,7 @@ function consumeQueryArg(args, index, spec) {
|
|
|
450
565
|
return null;
|
|
451
566
|
}
|
|
452
567
|
|
|
568
|
+
/** @param {string} value */
|
|
453
569
|
function parseWhereProp(value) {
|
|
454
570
|
const [key, ...rest] = value.split('=');
|
|
455
571
|
if (!key || rest.length === 0) {
|
|
@@ -458,6 +574,7 @@ function parseWhereProp(value) {
|
|
|
458
574
|
return { type: 'where-prop', key, value: rest.join('=') };
|
|
459
575
|
}
|
|
460
576
|
|
|
577
|
+
/** @param {string} value */
|
|
461
578
|
function parseSelectFields(value) {
|
|
462
579
|
if (value === '') {
|
|
463
580
|
return [];
|
|
@@ -465,6 +582,10 @@ function parseSelectFields(value) {
|
|
|
465
582
|
return value.split(',').map((field) => field.trim()).filter(Boolean);
|
|
466
583
|
}
|
|
467
584
|
|
|
585
|
+
/**
|
|
586
|
+
* @param {string[]} args
|
|
587
|
+
* @param {number} index
|
|
588
|
+
*/
|
|
468
589
|
function readTraversalStep(args, index) {
|
|
469
590
|
const arg = args[index];
|
|
470
591
|
if (arg !== '--outgoing' && arg !== '--incoming') {
|
|
@@ -476,6 +597,9 @@ function readTraversalStep(args, index) {
|
|
|
476
597
|
return { step: { type: arg.slice(2), label }, consumed };
|
|
477
598
|
}
|
|
478
599
|
|
|
600
|
+
/**
|
|
601
|
+
* @param {{args: string[], index: number, flag: string, shortFlag?: string, allowEmpty?: boolean}} params
|
|
602
|
+
*/
|
|
479
603
|
function readOptionValue({ args, index, flag, shortFlag, allowEmpty = false }) {
|
|
480
604
|
const arg = args[index];
|
|
481
605
|
if (matchesOptionFlag(arg, flag, shortFlag)) {
|
|
@@ -489,10 +613,16 @@ function readOptionValue({ args, index, flag, shortFlag, allowEmpty = false }) {
|
|
|
489
613
|
return null;
|
|
490
614
|
}
|
|
491
615
|
|
|
616
|
+
/**
|
|
617
|
+
* @param {string} arg
|
|
618
|
+
* @param {string} flag
|
|
619
|
+
* @param {string} [shortFlag]
|
|
620
|
+
*/
|
|
492
621
|
function matchesOptionFlag(arg, flag, shortFlag) {
|
|
493
622
|
return arg === flag || (shortFlag && arg === shortFlag);
|
|
494
623
|
}
|
|
495
624
|
|
|
625
|
+
/** @param {{args: string[], index: number, flag: string, allowEmpty?: boolean}} params */
|
|
496
626
|
function readNextOptionValue({ args, index, flag, allowEmpty }) {
|
|
497
627
|
const value = args[index + 1];
|
|
498
628
|
if (value === undefined || (!allowEmpty && value === '')) {
|
|
@@ -501,6 +631,7 @@ function readNextOptionValue({ args, index, flag, allowEmpty }) {
|
|
|
501
631
|
return { value, consumed: 1 };
|
|
502
632
|
}
|
|
503
633
|
|
|
634
|
+
/** @param {{arg: string, flag: string, allowEmpty?: boolean}} params */
|
|
504
635
|
function readInlineOptionValue({ arg, flag, allowEmpty }) {
|
|
505
636
|
const value = arg.slice(flag.length + 1);
|
|
506
637
|
if (!allowEmpty && value === '') {
|
|
@@ -509,9 +640,12 @@ function readInlineOptionValue({ arg, flag, allowEmpty }) {
|
|
|
509
640
|
return { value, consumed: 0 };
|
|
510
641
|
}
|
|
511
642
|
|
|
643
|
+
/** @param {string[]} args */
|
|
512
644
|
function parsePathArgs(args) {
|
|
513
645
|
const options = createPathOptions();
|
|
646
|
+
/** @type {string[]} */
|
|
514
647
|
const labels = [];
|
|
648
|
+
/** @type {string[]} */
|
|
515
649
|
const positionals = [];
|
|
516
650
|
|
|
517
651
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -523,6 +657,7 @@ function parsePathArgs(args) {
|
|
|
523
657
|
return options;
|
|
524
658
|
}
|
|
525
659
|
|
|
660
|
+
/** @returns {{from: string|null, to: string|null, dir: string|undefined, labelFilter: string|string[]|undefined, maxDepth: number|undefined}} */
|
|
526
661
|
function createPathOptions() {
|
|
527
662
|
return {
|
|
528
663
|
from: null,
|
|
@@ -533,8 +668,12 @@ function createPathOptions() {
|
|
|
533
668
|
};
|
|
534
669
|
}
|
|
535
670
|
|
|
671
|
+
/**
|
|
672
|
+
* @param {{args: string[], index: number, options: ReturnType<typeof createPathOptions>, labels: string[], positionals: string[]}} params
|
|
673
|
+
*/
|
|
536
674
|
function consumePathArg({ args, index, options, labels, positionals }) {
|
|
537
675
|
const arg = args[index];
|
|
676
|
+
/** @type {Array<{flag: string, apply: (value: string) => void}>} */
|
|
538
677
|
const handlers = [
|
|
539
678
|
{ flag: '--from', apply: (value) => { options.from = value; } },
|
|
540
679
|
{ flag: '--to', apply: (value) => { options.to = value; } },
|
|
@@ -559,6 +698,11 @@ function consumePathArg({ args, index, options, labels, positionals }) {
|
|
|
559
698
|
return { consumed: 0 };
|
|
560
699
|
}
|
|
561
700
|
|
|
701
|
+
/**
|
|
702
|
+
* @param {ReturnType<typeof createPathOptions>} options
|
|
703
|
+
* @param {string[]} labels
|
|
704
|
+
* @param {string[]} positionals
|
|
705
|
+
*/
|
|
562
706
|
function finalizePathOptions(options, labels, positionals) {
|
|
563
707
|
if (!options.from) {
|
|
564
708
|
options.from = positionals[0] || null;
|
|
@@ -579,10 +723,12 @@ function finalizePathOptions(options, labels, positionals) {
|
|
|
579
723
|
}
|
|
580
724
|
}
|
|
581
725
|
|
|
726
|
+
/** @param {string} value */
|
|
582
727
|
function parseLabels(value) {
|
|
583
728
|
return value.split(',').map((label) => label.trim()).filter(Boolean);
|
|
584
729
|
}
|
|
585
730
|
|
|
731
|
+
/** @param {string} value */
|
|
586
732
|
function parseMaxDepth(value) {
|
|
587
733
|
const parsed = Number.parseInt(value, 10);
|
|
588
734
|
if (Number.isNaN(parsed)) {
|
|
@@ -591,7 +737,9 @@ function parseMaxDepth(value) {
|
|
|
591
737
|
return parsed;
|
|
592
738
|
}
|
|
593
739
|
|
|
740
|
+
/** @param {string[]} args */
|
|
594
741
|
function parseHistoryArgs(args) {
|
|
742
|
+
/** @type {{node: string|null}} */
|
|
595
743
|
const options = { node: null };
|
|
596
744
|
|
|
597
745
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -622,6 +770,10 @@ function parseHistoryArgs(args) {
|
|
|
622
770
|
return options;
|
|
623
771
|
}
|
|
624
772
|
|
|
773
|
+
/**
|
|
774
|
+
* @param {*} patch
|
|
775
|
+
* @param {string} nodeId
|
|
776
|
+
*/
|
|
625
777
|
function patchTouchesNode(patch, nodeId) {
|
|
626
778
|
const ops = Array.isArray(patch?.ops) ? patch.ops : [];
|
|
627
779
|
for (const op of ops) {
|
|
@@ -635,6 +787,7 @@ function patchTouchesNode(patch, nodeId) {
|
|
|
635
787
|
return false;
|
|
636
788
|
}
|
|
637
789
|
|
|
790
|
+
/** @param {*} payload */
|
|
638
791
|
function renderInfo(payload) {
|
|
639
792
|
const lines = [`Repo: ${payload.repo}`];
|
|
640
793
|
lines.push(`Graphs: ${payload.graphs.length}`);
|
|
@@ -654,6 +807,7 @@ function renderInfo(payload) {
|
|
|
654
807
|
return `${lines.join('\n')}\n`;
|
|
655
808
|
}
|
|
656
809
|
|
|
810
|
+
/** @param {*} payload */
|
|
657
811
|
function renderQuery(payload) {
|
|
658
812
|
const lines = [
|
|
659
813
|
`Graph: ${payload.graph}`,
|
|
@@ -672,6 +826,7 @@ function renderQuery(payload) {
|
|
|
672
826
|
return `${lines.join('\n')}\n`;
|
|
673
827
|
}
|
|
674
828
|
|
|
829
|
+
/** @param {*} payload */
|
|
675
830
|
function renderPath(payload) {
|
|
676
831
|
const lines = [
|
|
677
832
|
`Graph: ${payload.graph}`,
|
|
@@ -694,6 +849,7 @@ const ANSI_RED = '\x1b[31m';
|
|
|
694
849
|
const ANSI_DIM = '\x1b[2m';
|
|
695
850
|
const ANSI_RESET = '\x1b[0m';
|
|
696
851
|
|
|
852
|
+
/** @param {string} state */
|
|
697
853
|
function colorCachedState(state) {
|
|
698
854
|
if (state === 'fresh') {
|
|
699
855
|
return `${ANSI_GREEN}${state}${ANSI_RESET}`;
|
|
@@ -704,6 +860,7 @@ function colorCachedState(state) {
|
|
|
704
860
|
return `${ANSI_RED}${ANSI_DIM}${state}${ANSI_RESET}`;
|
|
705
861
|
}
|
|
706
862
|
|
|
863
|
+
/** @param {*} payload */
|
|
707
864
|
function renderCheck(payload) {
|
|
708
865
|
const lines = [
|
|
709
866
|
`Graph: ${payload.graph}`,
|
|
@@ -754,6 +911,7 @@ function renderCheck(payload) {
|
|
|
754
911
|
return `${lines.join('\n')}\n`;
|
|
755
912
|
}
|
|
756
913
|
|
|
914
|
+
/** @param {*} hook */
|
|
757
915
|
function formatHookStatusLine(hook) {
|
|
758
916
|
if (!hook.installed && hook.foreign) {
|
|
759
917
|
return "Hook: foreign hook present — run 'git warp install-hooks'";
|
|
@@ -767,6 +925,7 @@ function formatHookStatusLine(hook) {
|
|
|
767
925
|
return `Hook: installed (v${hook.version}) — upgrade available, run 'git warp install-hooks'`;
|
|
768
926
|
}
|
|
769
927
|
|
|
928
|
+
/** @param {*} payload */
|
|
770
929
|
function renderHistory(payload) {
|
|
771
930
|
const lines = [
|
|
772
931
|
`Graph: ${payload.graph}`,
|
|
@@ -785,6 +944,7 @@ function renderHistory(payload) {
|
|
|
785
944
|
return `${lines.join('\n')}\n`;
|
|
786
945
|
}
|
|
787
946
|
|
|
947
|
+
/** @param {*} payload */
|
|
788
948
|
function renderError(payload) {
|
|
789
949
|
return `Error: ${payload.error.message}\n`;
|
|
790
950
|
}
|
|
@@ -803,11 +963,8 @@ function writeHtmlExport(filePath, svgContent) {
|
|
|
803
963
|
* Writes a command result to stdout/stderr in the appropriate format.
|
|
804
964
|
* Dispatches to JSON, SVG file, HTML file, ASCII view, or plain text
|
|
805
965
|
* 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')
|
|
966
|
+
* @param {*} payload - Command result payload
|
|
967
|
+
* @param {{json: boolean, command: string, view: string|null}} options
|
|
811
968
|
*/
|
|
812
969
|
function emit(payload, { json, command, view }) {
|
|
813
970
|
if (json) {
|
|
@@ -925,9 +1082,8 @@ function emit(payload, { json, command, view }) {
|
|
|
925
1082
|
|
|
926
1083
|
/**
|
|
927
1084
|
* Handles the `info` command: summarizes graphs in the repository.
|
|
928
|
-
* @param {
|
|
929
|
-
* @
|
|
930
|
-
* @returns {Promise<{repo: string, graphs: Object[]}>} Info payload
|
|
1085
|
+
* @param {{options: CliOptions}} params
|
|
1086
|
+
* @returns {Promise<{repo: string, graphs: GraphInfoResult[]}>} Info payload
|
|
931
1087
|
* @throws {CliError} If the specified graph is not found
|
|
932
1088
|
*/
|
|
933
1089
|
async function handleInfo({ options }) {
|
|
@@ -974,10 +1130,8 @@ async function handleInfo({ options }) {
|
|
|
974
1130
|
|
|
975
1131
|
/**
|
|
976
1132
|
* 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
|
|
1133
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1134
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Query result payload
|
|
981
1135
|
* @throws {CliError} On invalid query options or query execution errors
|
|
982
1136
|
*/
|
|
983
1137
|
async function handleQuery({ options, args }) {
|
|
@@ -1021,6 +1175,10 @@ async function handleQuery({ options, args }) {
|
|
|
1021
1175
|
}
|
|
1022
1176
|
}
|
|
1023
1177
|
|
|
1178
|
+
/**
|
|
1179
|
+
* @param {*} builder
|
|
1180
|
+
* @param {Array<{type: string, label?: string, key?: string, value?: string}>} steps
|
|
1181
|
+
*/
|
|
1024
1182
|
function applyQuerySteps(builder, steps) {
|
|
1025
1183
|
let current = builder;
|
|
1026
1184
|
for (const step of steps) {
|
|
@@ -1029,6 +1187,10 @@ function applyQuerySteps(builder, steps) {
|
|
|
1029
1187
|
return current;
|
|
1030
1188
|
}
|
|
1031
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* @param {*} builder
|
|
1192
|
+
* @param {{type: string, label?: string, key?: string, value?: string}} step
|
|
1193
|
+
*/
|
|
1032
1194
|
function applyQueryStep(builder, step) {
|
|
1033
1195
|
if (step.type === 'outgoing') {
|
|
1034
1196
|
return builder.outgoing(step.label);
|
|
@@ -1037,11 +1199,16 @@ function applyQueryStep(builder, step) {
|
|
|
1037
1199
|
return builder.incoming(step.label);
|
|
1038
1200
|
}
|
|
1039
1201
|
if (step.type === 'where-prop') {
|
|
1040
|
-
return builder.where((node) => matchesPropFilter(node, step.key, step.value));
|
|
1202
|
+
return builder.where((/** @type {*} */ node) => matchesPropFilter(node, /** @type {string} */ (step.key), /** @type {string} */ (step.value))); // TODO(ts-cleanup): type CLI payload
|
|
1041
1203
|
}
|
|
1042
1204
|
return builder;
|
|
1043
1205
|
}
|
|
1044
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* @param {*} node
|
|
1209
|
+
* @param {string} key
|
|
1210
|
+
* @param {string} value
|
|
1211
|
+
*/
|
|
1045
1212
|
function matchesPropFilter(node, key, value) {
|
|
1046
1213
|
const props = node.props || {};
|
|
1047
1214
|
if (!Object.prototype.hasOwnProperty.call(props, key)) {
|
|
@@ -1050,6 +1217,11 @@ function matchesPropFilter(node, key, value) {
|
|
|
1050
1217
|
return String(props[key]) === value;
|
|
1051
1218
|
}
|
|
1052
1219
|
|
|
1220
|
+
/**
|
|
1221
|
+
* @param {string} graphName
|
|
1222
|
+
* @param {*} result
|
|
1223
|
+
* @returns {{graph: string, stateHash: *, nodes: *, _renderedSvg?: string, _renderedAscii?: string}}
|
|
1224
|
+
*/
|
|
1053
1225
|
function buildQueryPayload(graphName, result) {
|
|
1054
1226
|
return {
|
|
1055
1227
|
graph: graphName,
|
|
@@ -1058,6 +1230,7 @@ function buildQueryPayload(graphName, result) {
|
|
|
1058
1230
|
};
|
|
1059
1231
|
}
|
|
1060
1232
|
|
|
1233
|
+
/** @param {*} error */
|
|
1061
1234
|
function mapQueryError(error) {
|
|
1062
1235
|
if (error && error.code && String(error.code).startsWith('E_QUERY')) {
|
|
1063
1236
|
throw usageError(error.message);
|
|
@@ -1067,10 +1240,8 @@ function mapQueryError(error) {
|
|
|
1067
1240
|
|
|
1068
1241
|
/**
|
|
1069
1242
|
* 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
|
|
1243
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1244
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Path result payload
|
|
1074
1245
|
* @throws {CliError} If --from/--to are missing or a node is not found
|
|
1075
1246
|
*/
|
|
1076
1247
|
async function handlePath({ options, args }) {
|
|
@@ -1107,7 +1278,7 @@ async function handlePath({ options, args }) {
|
|
|
1107
1278
|
payload,
|
|
1108
1279
|
exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NOT_FOUND,
|
|
1109
1280
|
};
|
|
1110
|
-
} catch (error) {
|
|
1281
|
+
} catch (/** @type {*} */ error) { // TODO(ts-cleanup): type error
|
|
1111
1282
|
if (error && error.code === 'NODE_NOT_FOUND') {
|
|
1112
1283
|
throw notFoundError(error.message);
|
|
1113
1284
|
}
|
|
@@ -1117,9 +1288,8 @@ async function handlePath({ options, args }) {
|
|
|
1117
1288
|
|
|
1118
1289
|
/**
|
|
1119
1290
|
* Handles the `check` command: reports graph health, GC, and hook status.
|
|
1120
|
-
* @param {
|
|
1121
|
-
* @
|
|
1122
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Health check payload
|
|
1291
|
+
* @param {{options: CliOptions}} params
|
|
1292
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Health check payload
|
|
1123
1293
|
*/
|
|
1124
1294
|
async function handleCheck({ options }) {
|
|
1125
1295
|
const { graph, graphName, persistence } = await openGraph(options);
|
|
@@ -1149,17 +1319,20 @@ async function handleCheck({ options }) {
|
|
|
1149
1319
|
};
|
|
1150
1320
|
}
|
|
1151
1321
|
|
|
1322
|
+
/** @param {Persistence} persistence */
|
|
1152
1323
|
async function getHealth(persistence) {
|
|
1153
1324
|
const clock = ClockAdapter.node();
|
|
1154
|
-
const healthService = new HealthCheckService({ persistence, clock });
|
|
1325
|
+
const healthService = new HealthCheckService({ persistence: /** @type {*} */ (persistence), clock }); // TODO(ts-cleanup): narrow port type
|
|
1155
1326
|
return await healthService.getHealth();
|
|
1156
1327
|
}
|
|
1157
1328
|
|
|
1329
|
+
/** @param {WarpGraphInstance} graph */
|
|
1158
1330
|
async function getGcMetrics(graph) {
|
|
1159
1331
|
await graph.materialize();
|
|
1160
1332
|
return graph.getGCMetrics();
|
|
1161
1333
|
}
|
|
1162
1334
|
|
|
1335
|
+
/** @param {WarpGraphInstance} graph */
|
|
1163
1336
|
async function collectWriterHeads(graph) {
|
|
1164
1337
|
const frontier = await graph.getFrontier();
|
|
1165
1338
|
return [...frontier.entries()]
|
|
@@ -1167,6 +1340,10 @@ async function collectWriterHeads(graph) {
|
|
|
1167
1340
|
.map(([writerId, sha]) => ({ writerId, sha }));
|
|
1168
1341
|
}
|
|
1169
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* @param {Persistence} persistence
|
|
1345
|
+
* @param {string} graphName
|
|
1346
|
+
*/
|
|
1170
1347
|
async function loadCheckpointInfo(persistence, graphName) {
|
|
1171
1348
|
const checkpointRef = buildCheckpointRef(graphName);
|
|
1172
1349
|
const checkpointSha = await persistence.readRef(checkpointRef);
|
|
@@ -1181,6 +1358,10 @@ async function loadCheckpointInfo(persistence, graphName) {
|
|
|
1181
1358
|
};
|
|
1182
1359
|
}
|
|
1183
1360
|
|
|
1361
|
+
/**
|
|
1362
|
+
* @param {Persistence} persistence
|
|
1363
|
+
* @param {string|null} checkpointSha
|
|
1364
|
+
*/
|
|
1184
1365
|
async function readCheckpointDate(persistence, checkpointSha) {
|
|
1185
1366
|
if (!checkpointSha) {
|
|
1186
1367
|
return null;
|
|
@@ -1189,6 +1370,7 @@ async function readCheckpointDate(persistence, checkpointSha) {
|
|
|
1189
1370
|
return info.date || null;
|
|
1190
1371
|
}
|
|
1191
1372
|
|
|
1373
|
+
/** @param {string|null} checkpointDate */
|
|
1192
1374
|
function computeAgeSeconds(checkpointDate) {
|
|
1193
1375
|
if (!checkpointDate) {
|
|
1194
1376
|
return null;
|
|
@@ -1200,6 +1382,11 @@ function computeAgeSeconds(checkpointDate) {
|
|
|
1200
1382
|
return Math.max(0, Math.floor((Date.now() - parsed) / 1000));
|
|
1201
1383
|
}
|
|
1202
1384
|
|
|
1385
|
+
/**
|
|
1386
|
+
* @param {Persistence} persistence
|
|
1387
|
+
* @param {string} graphName
|
|
1388
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
1389
|
+
*/
|
|
1203
1390
|
async function loadCoverageInfo(persistence, graphName, writerHeads) {
|
|
1204
1391
|
const coverageRef = buildCoverageRef(graphName);
|
|
1205
1392
|
const coverageSha = await persistence.readRef(coverageRef);
|
|
@@ -1214,6 +1401,11 @@ async function loadCoverageInfo(persistence, graphName, writerHeads) {
|
|
|
1214
1401
|
};
|
|
1215
1402
|
}
|
|
1216
1403
|
|
|
1404
|
+
/**
|
|
1405
|
+
* @param {Persistence} persistence
|
|
1406
|
+
* @param {Array<{writerId: string, sha: string}>} writerHeads
|
|
1407
|
+
* @param {string} coverageSha
|
|
1408
|
+
*/
|
|
1217
1409
|
async function findMissingWriters(persistence, writerHeads, coverageSha) {
|
|
1218
1410
|
const missing = [];
|
|
1219
1411
|
for (const head of writerHeads) {
|
|
@@ -1225,6 +1417,9 @@ async function findMissingWriters(persistence, writerHeads, coverageSha) {
|
|
|
1225
1417
|
return missing;
|
|
1226
1418
|
}
|
|
1227
1419
|
|
|
1420
|
+
/**
|
|
1421
|
+
* @param {{repo: string, graphName: string, health: *, checkpoint: *, writerHeads: Array<{writerId: string, sha: string}>, coverage: *, gcMetrics: *, hook: *|null, status: *|null}} params
|
|
1422
|
+
*/
|
|
1228
1423
|
function buildCheckPayload({
|
|
1229
1424
|
repo,
|
|
1230
1425
|
graphName,
|
|
@@ -1254,10 +1449,8 @@ function buildCheckPayload({
|
|
|
1254
1449
|
|
|
1255
1450
|
/**
|
|
1256
1451
|
* 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
|
|
1452
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1453
|
+
* @returns {Promise<{payload: *, exitCode: number}>} History payload
|
|
1261
1454
|
* @throws {CliError} If no patches are found for the writer
|
|
1262
1455
|
*/
|
|
1263
1456
|
async function handleHistory({ options, args }) {
|
|
@@ -1269,15 +1462,15 @@ async function handleHistory({ options, args }) {
|
|
|
1269
1462
|
const writerId = options.writer;
|
|
1270
1463
|
let patches = await graph.getWriterPatches(writerId);
|
|
1271
1464
|
if (cursorInfo.active) {
|
|
1272
|
-
patches = patches.filter(({ patch }) => patch.lamport <= cursorInfo.tick);
|
|
1465
|
+
patches = patches.filter((/** @type {*} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick)); // TODO(ts-cleanup): type CLI payload
|
|
1273
1466
|
}
|
|
1274
1467
|
if (patches.length === 0) {
|
|
1275
1468
|
throw notFoundError(`No patches found for writer: ${writerId}`);
|
|
1276
1469
|
}
|
|
1277
1470
|
|
|
1278
1471
|
const entries = patches
|
|
1279
|
-
.filter(({ patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node))
|
|
1280
|
-
.map(({ patch, sha }) => ({
|
|
1472
|
+
.filter((/** @type {*} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node)) // TODO(ts-cleanup): type CLI payload
|
|
1473
|
+
.map((/** @type {*} */ { patch, sha }) => ({ // TODO(ts-cleanup): type CLI payload
|
|
1281
1474
|
sha,
|
|
1282
1475
|
schema: patch.schema,
|
|
1283
1476
|
lamport: patch.lamport,
|
|
@@ -1299,12 +1492,8 @@ async function handleHistory({ options, args }) {
|
|
|
1299
1492
|
* Materializes a single graph, creates a checkpoint, and returns summary stats.
|
|
1300
1493
|
* When a ceiling tick is provided (seek cursor active), the checkpoint step is
|
|
1301
1494
|
* 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}>}
|
|
1495
|
+
* @param {{persistence: Persistence, graphName: string, writerId: string, ceiling?: number}} params
|
|
1496
|
+
* @returns {Promise<{graph: string, nodes: number, edges: number, properties: number, checkpoint: string|null, writers: Record<string, number>, patchCount: number}>}
|
|
1308
1497
|
*/
|
|
1309
1498
|
async function materializeOneGraph({ persistence, graphName, writerId, ceiling }) {
|
|
1310
1499
|
const graph = await WarpGraph.open({ persistence, graphName, writerId, crypto: new NodeCryptoAdapter() });
|
|
@@ -1315,6 +1504,7 @@ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }
|
|
|
1315
1504
|
const status = await graph.status();
|
|
1316
1505
|
|
|
1317
1506
|
// Build per-writer patch counts for the view renderer
|
|
1507
|
+
/** @type {Record<string, number>} */
|
|
1318
1508
|
const writers = {};
|
|
1319
1509
|
let totalPatchCount = 0;
|
|
1320
1510
|
for (const wId of Object.keys(status.frontier)) {
|
|
@@ -1338,9 +1528,8 @@ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }
|
|
|
1338
1528
|
|
|
1339
1529
|
/**
|
|
1340
1530
|
* Handles the `materialize` command: materializes and checkpoints all graphs.
|
|
1341
|
-
* @param {
|
|
1342
|
-
* @
|
|
1343
|
-
* @returns {Promise<{payload: Object, exitCode: number}>} Materialize result payload
|
|
1531
|
+
* @param {{options: CliOptions}} params
|
|
1532
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Materialize result payload
|
|
1344
1533
|
* @throws {CliError} If the specified graph is not found
|
|
1345
1534
|
*/
|
|
1346
1535
|
async function handleMaterialize({ options }) {
|
|
@@ -1387,13 +1576,14 @@ async function handleMaterialize({ options }) {
|
|
|
1387
1576
|
}
|
|
1388
1577
|
}
|
|
1389
1578
|
|
|
1390
|
-
const allFailed = results.every((r) => r.error);
|
|
1579
|
+
const allFailed = results.every((r) => /** @type {*} */ (r).error); // TODO(ts-cleanup): type CLI payload
|
|
1391
1580
|
return {
|
|
1392
1581
|
payload: { graphs: results },
|
|
1393
1582
|
exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
|
|
1394
1583
|
};
|
|
1395
1584
|
}
|
|
1396
1585
|
|
|
1586
|
+
/** @param {*} payload */
|
|
1397
1587
|
function renderMaterialize(payload) {
|
|
1398
1588
|
if (payload.graphs.length === 0) {
|
|
1399
1589
|
return 'No graphs found in repo.\n';
|
|
@@ -1410,6 +1600,7 @@ function renderMaterialize(payload) {
|
|
|
1410
1600
|
return `${lines.join('\n')}\n`;
|
|
1411
1601
|
}
|
|
1412
1602
|
|
|
1603
|
+
/** @param {*} payload */
|
|
1413
1604
|
function renderInstallHooks(payload) {
|
|
1414
1605
|
if (payload.action === 'up-to-date') {
|
|
1415
1606
|
return `Hook: already up to date (v${payload.version}) at ${payload.hookPath}\n`;
|
|
@@ -1430,7 +1621,7 @@ function createHookInstaller() {
|
|
|
1430
1621
|
const templateDir = path.resolve(__dirname, '..', 'hooks');
|
|
1431
1622
|
const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
1432
1623
|
return new HookInstaller({
|
|
1433
|
-
fs,
|
|
1624
|
+
fs: /** @type {*} */ (fs), // TODO(ts-cleanup): narrow port type
|
|
1434
1625
|
execGitConfig: execGitConfigValue,
|
|
1435
1626
|
version,
|
|
1436
1627
|
templateDir,
|
|
@@ -1438,6 +1629,11 @@ function createHookInstaller() {
|
|
|
1438
1629
|
});
|
|
1439
1630
|
}
|
|
1440
1631
|
|
|
1632
|
+
/**
|
|
1633
|
+
* @param {string} repoPath
|
|
1634
|
+
* @param {string} key
|
|
1635
|
+
* @returns {string|null}
|
|
1636
|
+
*/
|
|
1441
1637
|
function execGitConfigValue(repoPath, key) {
|
|
1442
1638
|
try {
|
|
1443
1639
|
if (key === '--git-dir') {
|
|
@@ -1457,6 +1653,7 @@ function isInteractive() {
|
|
|
1457
1653
|
return Boolean(process.stderr.isTTY);
|
|
1458
1654
|
}
|
|
1459
1655
|
|
|
1656
|
+
/** @param {string} question @returns {Promise<string>} */
|
|
1460
1657
|
function promptUser(question) {
|
|
1461
1658
|
const rl = readline.createInterface({
|
|
1462
1659
|
input: process.stdin,
|
|
@@ -1470,6 +1667,7 @@ function promptUser(question) {
|
|
|
1470
1667
|
});
|
|
1471
1668
|
}
|
|
1472
1669
|
|
|
1670
|
+
/** @param {string[]} args */
|
|
1473
1671
|
function parseInstallHooksArgs(args) {
|
|
1474
1672
|
const options = { force: false };
|
|
1475
1673
|
for (const arg of args) {
|
|
@@ -1482,6 +1680,10 @@ function parseInstallHooksArgs(args) {
|
|
|
1482
1680
|
return options;
|
|
1483
1681
|
}
|
|
1484
1682
|
|
|
1683
|
+
/**
|
|
1684
|
+
* @param {*} classification
|
|
1685
|
+
* @param {{force: boolean}} hookOptions
|
|
1686
|
+
*/
|
|
1485
1687
|
async function resolveStrategy(classification, hookOptions) {
|
|
1486
1688
|
if (hookOptions.force) {
|
|
1487
1689
|
return 'replace';
|
|
@@ -1498,6 +1700,7 @@ async function resolveStrategy(classification, hookOptions) {
|
|
|
1498
1700
|
return await promptForForeignStrategy();
|
|
1499
1701
|
}
|
|
1500
1702
|
|
|
1703
|
+
/** @param {*} classification */
|
|
1501
1704
|
async function promptForOursStrategy(classification) {
|
|
1502
1705
|
const installer = createHookInstaller();
|
|
1503
1706
|
if (classification.version === installer._version) {
|
|
@@ -1539,10 +1742,8 @@ async function promptForForeignStrategy() {
|
|
|
1539
1742
|
|
|
1540
1743
|
/**
|
|
1541
1744
|
* 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
|
|
1745
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
1746
|
+
* @returns {Promise<{payload: *, exitCode: number}>} Install result payload
|
|
1546
1747
|
* @throws {CliError} If an existing hook is found and the session is not interactive
|
|
1547
1748
|
*/
|
|
1548
1749
|
async function handleInstallHooks({ options, args }) {
|
|
@@ -1578,6 +1779,7 @@ async function handleInstallHooks({ options, args }) {
|
|
|
1578
1779
|
};
|
|
1579
1780
|
}
|
|
1580
1781
|
|
|
1782
|
+
/** @param {string} hookPath */
|
|
1581
1783
|
function readHookContent(hookPath) {
|
|
1582
1784
|
try {
|
|
1583
1785
|
return fs.readFileSync(hookPath, 'utf8');
|
|
@@ -1586,6 +1788,7 @@ function readHookContent(hookPath) {
|
|
|
1586
1788
|
}
|
|
1587
1789
|
}
|
|
1588
1790
|
|
|
1791
|
+
/** @param {string} repoPath */
|
|
1589
1792
|
function getHookStatusForCheck(repoPath) {
|
|
1590
1793
|
try {
|
|
1591
1794
|
const installer = createHookInstaller();
|
|
@@ -1602,10 +1805,9 @@ function getHookStatusForCheck(repoPath) {
|
|
|
1602
1805
|
/**
|
|
1603
1806
|
* Reads the active seek cursor for a graph from Git ref storage.
|
|
1604
1807
|
*
|
|
1605
|
-
* @
|
|
1606
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1808
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1607
1809
|
* @param {string} graphName - Name of the WARP graph
|
|
1608
|
-
* @returns {Promise<
|
|
1810
|
+
* @returns {Promise<CursorBlob|null>} Cursor object, or null if no active cursor
|
|
1609
1811
|
* @throws {Error} If the stored blob is corrupted or not valid JSON
|
|
1610
1812
|
*/
|
|
1611
1813
|
async function readActiveCursor(persistence, graphName) {
|
|
@@ -1624,10 +1826,9 @@ async function readActiveCursor(persistence, graphName) {
|
|
|
1624
1826
|
* Serializes the cursor as JSON, stores it as a Git blob, and points
|
|
1625
1827
|
* the active cursor ref at that blob.
|
|
1626
1828
|
*
|
|
1627
|
-
* @
|
|
1628
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1829
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1629
1830
|
* @param {string} graphName - Name of the WARP graph
|
|
1630
|
-
* @param {
|
|
1831
|
+
* @param {CursorBlob} cursor - Cursor state to persist
|
|
1631
1832
|
* @returns {Promise<void>}
|
|
1632
1833
|
*/
|
|
1633
1834
|
async function writeActiveCursor(persistence, graphName, cursor) {
|
|
@@ -1642,8 +1843,7 @@ async function writeActiveCursor(persistence, graphName, cursor) {
|
|
|
1642
1843
|
*
|
|
1643
1844
|
* No-op if no active cursor exists.
|
|
1644
1845
|
*
|
|
1645
|
-
* @
|
|
1646
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1846
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1647
1847
|
* @param {string} graphName - Name of the WARP graph
|
|
1648
1848
|
* @returns {Promise<void>}
|
|
1649
1849
|
*/
|
|
@@ -1658,11 +1858,10 @@ async function clearActiveCursor(persistence, graphName) {
|
|
|
1658
1858
|
/**
|
|
1659
1859
|
* Reads a named saved cursor from Git ref storage.
|
|
1660
1860
|
*
|
|
1661
|
-
* @
|
|
1662
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1861
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1663
1862
|
* @param {string} graphName - Name of the WARP graph
|
|
1664
1863
|
* @param {string} name - Saved cursor name
|
|
1665
|
-
* @returns {Promise<
|
|
1864
|
+
* @returns {Promise<CursorBlob|null>} Cursor object, or null if not found
|
|
1666
1865
|
* @throws {Error} If the stored blob is corrupted or not valid JSON
|
|
1667
1866
|
*/
|
|
1668
1867
|
async function readSavedCursor(persistence, graphName, name) {
|
|
@@ -1681,11 +1880,10 @@ async function readSavedCursor(persistence, graphName, name) {
|
|
|
1681
1880
|
* Serializes the cursor as JSON, stores it as a Git blob, and points
|
|
1682
1881
|
* the named saved-cursor ref at that blob.
|
|
1683
1882
|
*
|
|
1684
|
-
* @
|
|
1685
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1883
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1686
1884
|
* @param {string} graphName - Name of the WARP graph
|
|
1687
1885
|
* @param {string} name - Saved cursor name
|
|
1688
|
-
* @param {
|
|
1886
|
+
* @param {CursorBlob} cursor - Cursor state to persist
|
|
1689
1887
|
* @returns {Promise<void>}
|
|
1690
1888
|
*/
|
|
1691
1889
|
async function writeSavedCursor(persistence, graphName, name, cursor) {
|
|
@@ -1700,8 +1898,7 @@ async function writeSavedCursor(persistence, graphName, name, cursor) {
|
|
|
1700
1898
|
*
|
|
1701
1899
|
* No-op if the named cursor does not exist.
|
|
1702
1900
|
*
|
|
1703
|
-
* @
|
|
1704
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1901
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1705
1902
|
* @param {string} graphName - Name of the WARP graph
|
|
1706
1903
|
* @param {string} name - Saved cursor name to delete
|
|
1707
1904
|
* @returns {Promise<void>}
|
|
@@ -1717,8 +1914,7 @@ async function deleteSavedCursor(persistence, graphName, name) {
|
|
|
1717
1914
|
/**
|
|
1718
1915
|
* Lists all saved cursors for a graph, reading each blob to include full cursor state.
|
|
1719
1916
|
*
|
|
1720
|
-
* @
|
|
1721
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
1917
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
1722
1918
|
* @param {string} graphName - Name of the WARP graph
|
|
1723
1919
|
* @returns {Promise<Array<{name: string, tick: number, mode?: string}>>} Array of saved cursors with their names
|
|
1724
1920
|
* @throws {Error} If any stored blob is corrupted or not valid JSON
|
|
@@ -1745,23 +1941,33 @@ async function listSavedCursors(persistence, graphName) {
|
|
|
1745
1941
|
// Seek Arg Parser
|
|
1746
1942
|
// ============================================================================
|
|
1747
1943
|
|
|
1944
|
+
/**
|
|
1945
|
+
* @param {string} arg
|
|
1946
|
+
* @param {SeekSpec} spec
|
|
1947
|
+
*/
|
|
1948
|
+
function handleSeekBooleanFlag(arg, spec) {
|
|
1949
|
+
if (arg === '--clear-cache') {
|
|
1950
|
+
if (spec.action !== 'status') {
|
|
1951
|
+
throw usageError('--clear-cache cannot be combined with other seek flags');
|
|
1952
|
+
}
|
|
1953
|
+
spec.action = 'clear-cache';
|
|
1954
|
+
} else if (arg === '--no-persistent-cache') {
|
|
1955
|
+
spec.noPersistentCache = true;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1748
1959
|
/**
|
|
1749
1960
|
* 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
1961
|
* @param {string[]} args - Raw CLI arguments following the `seek` subcommand
|
|
1757
|
-
* @returns {
|
|
1758
|
-
* @throws {CliError} If arguments are invalid or flags are combined
|
|
1962
|
+
* @returns {SeekSpec} Parsed spec
|
|
1759
1963
|
*/
|
|
1760
1964
|
function parseSeekArgs(args) {
|
|
1965
|
+
/** @type {SeekSpec} */
|
|
1761
1966
|
const spec = {
|
|
1762
|
-
action: 'status', // status, tick, latest, save, load, list, drop
|
|
1967
|
+
action: 'status', // status, tick, latest, save, load, list, drop, clear-cache
|
|
1763
1968
|
tickValue: null,
|
|
1764
1969
|
name: null,
|
|
1970
|
+
noPersistentCache: false,
|
|
1765
1971
|
};
|
|
1766
1972
|
|
|
1767
1973
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -1854,6 +2060,8 @@ function parseSeekArgs(args) {
|
|
|
1854
2060
|
if (!spec.name) {
|
|
1855
2061
|
throw usageError('Missing name for --drop');
|
|
1856
2062
|
}
|
|
2063
|
+
} else if (arg === '--clear-cache' || arg === '--no-persistent-cache') {
|
|
2064
|
+
handleSeekBooleanFlag(arg, spec);
|
|
1857
2065
|
} else if (arg.startsWith('-')) {
|
|
1858
2066
|
throw usageError(`Unknown seek option: ${arg}`);
|
|
1859
2067
|
}
|
|
@@ -1909,28 +2117,44 @@ function resolveTickValue(tickValue, currentTick, ticks, maxTick) {
|
|
|
1909
2117
|
// Seek Handler
|
|
1910
2118
|
// ============================================================================
|
|
1911
2119
|
|
|
2120
|
+
/**
|
|
2121
|
+
* @param {WarpGraphInstance} graph
|
|
2122
|
+
* @param {Persistence} persistence
|
|
2123
|
+
* @param {string} graphName
|
|
2124
|
+
* @param {SeekSpec} seekSpec
|
|
2125
|
+
*/
|
|
2126
|
+
function wireSeekCache(graph, persistence, graphName, seekSpec) {
|
|
2127
|
+
if (seekSpec.noPersistentCache) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
graph.setSeekCache(new CasSeekCacheAdapter({
|
|
2131
|
+
persistence,
|
|
2132
|
+
plumbing: persistence.plumbing,
|
|
2133
|
+
graphName,
|
|
2134
|
+
}));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1912
2137
|
/**
|
|
1913
2138
|
* 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
|
|
2139
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
2140
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
1930
2141
|
*/
|
|
1931
2142
|
async function handleSeek({ options, args }) {
|
|
1932
2143
|
const seekSpec = parseSeekArgs(args);
|
|
1933
2144
|
const { graph, graphName, persistence } = await openGraph(options);
|
|
2145
|
+
void wireSeekCache(graph, persistence, graphName, seekSpec);
|
|
2146
|
+
|
|
2147
|
+
// Handle --clear-cache before discovering ticks (no materialization needed)
|
|
2148
|
+
if (seekSpec.action === 'clear-cache') {
|
|
2149
|
+
if (graph.seekCache) {
|
|
2150
|
+
await graph.seekCache.clear();
|
|
2151
|
+
}
|
|
2152
|
+
return {
|
|
2153
|
+
payload: { graph: graphName, action: 'clear-cache', message: 'Seek cache cleared.' },
|
|
2154
|
+
exitCode: EXIT_CODES.OK,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1934
2158
|
const activeCursor = await readActiveCursor(persistence, graphName);
|
|
1935
2159
|
const { ticks, maxTick, perWriter } = await graph.discoverTicks();
|
|
1936
2160
|
const frontierHash = computeFrontierHash(perWriter);
|
|
@@ -1948,11 +2172,12 @@ async function handleSeek({ options, args }) {
|
|
|
1948
2172
|
};
|
|
1949
2173
|
}
|
|
1950
2174
|
if (seekSpec.action === 'drop') {
|
|
1951
|
-
const
|
|
2175
|
+
const dropName = /** @type {string} */ (seekSpec.name);
|
|
2176
|
+
const existing = await readSavedCursor(persistence, graphName, dropName);
|
|
1952
2177
|
if (!existing) {
|
|
1953
|
-
throw notFoundError(`Saved cursor not found: ${
|
|
2178
|
+
throw notFoundError(`Saved cursor not found: ${dropName}`);
|
|
1954
2179
|
}
|
|
1955
|
-
await deleteSavedCursor(persistence, graphName,
|
|
2180
|
+
await deleteSavedCursor(persistence, graphName, dropName);
|
|
1956
2181
|
return {
|
|
1957
2182
|
payload: {
|
|
1958
2183
|
graph: graphName,
|
|
@@ -1992,7 +2217,7 @@ async function handleSeek({ options, args }) {
|
|
|
1992
2217
|
if (!activeCursor) {
|
|
1993
2218
|
throw usageError('No active cursor to save. Use --tick first.');
|
|
1994
2219
|
}
|
|
1995
|
-
await writeSavedCursor(persistence, graphName, seekSpec.name, activeCursor);
|
|
2220
|
+
await writeSavedCursor(persistence, graphName, /** @type {string} */ (seekSpec.name), activeCursor);
|
|
1996
2221
|
return {
|
|
1997
2222
|
payload: {
|
|
1998
2223
|
graph: graphName,
|
|
@@ -2004,9 +2229,10 @@ async function handleSeek({ options, args }) {
|
|
|
2004
2229
|
};
|
|
2005
2230
|
}
|
|
2006
2231
|
if (seekSpec.action === 'load') {
|
|
2007
|
-
const
|
|
2232
|
+
const loadName = /** @type {string} */ (seekSpec.name);
|
|
2233
|
+
const saved = await readSavedCursor(persistence, graphName, loadName);
|
|
2008
2234
|
if (!saved) {
|
|
2009
|
-
throw notFoundError(`Saved cursor not found: ${
|
|
2235
|
+
throw notFoundError(`Saved cursor not found: ${loadName}`);
|
|
2010
2236
|
}
|
|
2011
2237
|
await graph.materialize({ ceiling: saved.tick });
|
|
2012
2238
|
const nodes = await graph.getNodes();
|
|
@@ -2035,7 +2261,7 @@ async function handleSeek({ options, args }) {
|
|
|
2035
2261
|
}
|
|
2036
2262
|
if (seekSpec.action === 'tick') {
|
|
2037
2263
|
const currentTick = activeCursor ? activeCursor.tick : null;
|
|
2038
|
-
const resolvedTick = resolveTickValue(seekSpec.tickValue, currentTick, ticks, maxTick);
|
|
2264
|
+
const resolvedTick = resolveTickValue(/** @type {string} */ (seekSpec.tickValue), currentTick, ticks, maxTick);
|
|
2039
2265
|
await graph.materialize({ ceiling: resolvedTick });
|
|
2040
2266
|
const nodes = await graph.getNodes();
|
|
2041
2267
|
const edges = await graph.getEdges();
|
|
@@ -2117,11 +2343,11 @@ async function handleSeek({ options, args }) {
|
|
|
2117
2343
|
/**
|
|
2118
2344
|
* Converts the per-writer Map from discoverTicks() into a plain object for JSON output.
|
|
2119
2345
|
*
|
|
2120
|
-
* @
|
|
2121
|
-
* @
|
|
2122
|
-
* @returns {Object<string, {ticks: number[], tipSha: string|null}>} Plain object keyed by writer ID
|
|
2346
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer tick data
|
|
2347
|
+
* @returns {Record<string, WriterTickInfo>} Plain object keyed by writer ID
|
|
2123
2348
|
*/
|
|
2124
2349
|
function serializePerWriter(perWriter) {
|
|
2350
|
+
/** @type {Record<string, WriterTickInfo>} */
|
|
2125
2351
|
const result = {};
|
|
2126
2352
|
for (const [writerId, info] of perWriter) {
|
|
2127
2353
|
result[writerId] = { ticks: info.ticks, tipSha: info.tipSha, tickShas: info.tickShas };
|
|
@@ -2132,9 +2358,8 @@ function serializePerWriter(perWriter) {
|
|
|
2132
2358
|
/**
|
|
2133
2359
|
* Counts the total number of patches across all writers at or before the given tick.
|
|
2134
2360
|
*
|
|
2135
|
-
* @private
|
|
2136
2361
|
* @param {number} tick - Lamport tick ceiling (inclusive)
|
|
2137
|
-
* @param {Map<string,
|
|
2362
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer tick data
|
|
2138
2363
|
* @returns {number} Total patch count at or before the given tick
|
|
2139
2364
|
*/
|
|
2140
2365
|
function countPatchesAtTick(tick, perWriter) {
|
|
@@ -2155,11 +2380,11 @@ function countPatchesAtTick(tick, perWriter) {
|
|
|
2155
2380
|
* Used to suppress seek diffs when graph history may have changed since the
|
|
2156
2381
|
* previous cursor snapshot (e.g. new writers/patches, rewritten refs).
|
|
2157
2382
|
*
|
|
2158
|
-
* @
|
|
2159
|
-
* @param {Map<string, {tipSha: string|null}>} perWriter - Per-writer metadata from discoverTicks()
|
|
2383
|
+
* @param {Map<string, WriterTickInfo>} perWriter - Per-writer metadata from discoverTicks()
|
|
2160
2384
|
* @returns {string} Hex digest of the frontier fingerprint
|
|
2161
2385
|
*/
|
|
2162
2386
|
function computeFrontierHash(perWriter) {
|
|
2387
|
+
/** @type {Record<string, string|null>} */
|
|
2163
2388
|
const tips = {};
|
|
2164
2389
|
for (const [writerId, info] of perWriter) {
|
|
2165
2390
|
tips[writerId] = info?.tipSha || null;
|
|
@@ -2173,8 +2398,7 @@ function computeFrontierHash(perWriter) {
|
|
|
2173
2398
|
* Counts may be missing for older cursors (pre-diff support). In that case
|
|
2174
2399
|
* callers should treat the counts as unknown and suppress diffs.
|
|
2175
2400
|
*
|
|
2176
|
-
* @
|
|
2177
|
-
* @param {Object|null} cursor - Parsed cursor blob object
|
|
2401
|
+
* @param {CursorBlob|null} cursor - Parsed cursor blob object
|
|
2178
2402
|
* @returns {{nodes: number|null, edges: number|null}} Parsed counts
|
|
2179
2403
|
*/
|
|
2180
2404
|
function readSeekCounts(cursor) {
|
|
@@ -2192,8 +2416,7 @@ function readSeekCounts(cursor) {
|
|
|
2192
2416
|
*
|
|
2193
2417
|
* Returns null if the previous cursor is missing cached counts.
|
|
2194
2418
|
*
|
|
2195
|
-
* @
|
|
2196
|
-
* @param {Object|null} prevCursor - Cursor object read before updating the position
|
|
2419
|
+
* @param {CursorBlob|null} prevCursor - Cursor object read before updating the position
|
|
2197
2420
|
* @param {{nodes: number, edges: number}} next - Current materialized counts
|
|
2198
2421
|
* @param {string} frontierHash - Frontier fingerprint of the current graph
|
|
2199
2422
|
* @returns {{nodes: number, edges: number}|null} Diff object or null when unknown
|
|
@@ -2220,22 +2443,19 @@ function computeSeekStateDiff(prevCursor, next, frontierHash) {
|
|
|
2220
2443
|
* summarizes patch ops. Typically only a handful of writers have a patch at any
|
|
2221
2444
|
* single Lamport tick.
|
|
2222
2445
|
*
|
|
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
|
|
2446
|
+
* @param {{tick: number, perWriter: Map<string, WriterTickInfo>, graph: WarpGraphInstance}} params
|
|
2447
|
+
* @returns {Promise<Record<string, {sha: string, opSummary: *}>|null>} Map of writerId to { sha, opSummary }, or null if empty
|
|
2229
2448
|
*/
|
|
2230
2449
|
async function buildTickReceipt({ tick, perWriter, graph }) {
|
|
2231
2450
|
if (!Number.isInteger(tick) || tick <= 0) {
|
|
2232
2451
|
return null;
|
|
2233
2452
|
}
|
|
2234
2453
|
|
|
2454
|
+
/** @type {Record<string, {sha: string, opSummary: *}>} */
|
|
2235
2455
|
const receipt = {};
|
|
2236
2456
|
|
|
2237
2457
|
for (const [writerId, info] of perWriter) {
|
|
2238
|
-
const sha = info?.tickShas?.[tick];
|
|
2458
|
+
const sha = /** @type {*} */ (info?.tickShas)?.[tick]; // TODO(ts-cleanup): type CLI payload
|
|
2239
2459
|
if (!sha) {
|
|
2240
2460
|
continue;
|
|
2241
2461
|
}
|
|
@@ -2253,12 +2473,11 @@ async function buildTickReceipt({ tick, perWriter, graph }) {
|
|
|
2253
2473
|
*
|
|
2254
2474
|
* Handles all seek actions: list, drop, save, latest, load, tick, and status.
|
|
2255
2475
|
*
|
|
2256
|
-
* @
|
|
2257
|
-
* @param {Object} payload - Seek result payload from handleSeek
|
|
2476
|
+
* @param {*} payload - Seek result payload from handleSeek
|
|
2258
2477
|
* @returns {string} Formatted output string (includes trailing newline)
|
|
2259
2478
|
*/
|
|
2260
2479
|
function renderSeek(payload) {
|
|
2261
|
-
const formatDelta = (n) => {
|
|
2480
|
+
const formatDelta = (/** @type {*} */ n) => { // TODO(ts-cleanup): type CLI payload
|
|
2262
2481
|
if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
|
|
2263
2482
|
return '';
|
|
2264
2483
|
}
|
|
@@ -2266,7 +2485,7 @@ function renderSeek(payload) {
|
|
|
2266
2485
|
return ` (${sign}${n})`;
|
|
2267
2486
|
};
|
|
2268
2487
|
|
|
2269
|
-
const formatOpSummaryPlain = (summary) => {
|
|
2488
|
+
const formatOpSummaryPlain = (/** @type {*} */ summary) => { // TODO(ts-cleanup): type CLI payload
|
|
2270
2489
|
const order = [
|
|
2271
2490
|
['NodeAdd', '+', 'node'],
|
|
2272
2491
|
['EdgeAdd', '+', 'edge'],
|
|
@@ -2286,7 +2505,7 @@ function renderSeek(payload) {
|
|
|
2286
2505
|
return parts.length > 0 ? parts.join(' ') : '(empty)';
|
|
2287
2506
|
};
|
|
2288
2507
|
|
|
2289
|
-
const appendReceiptSummary = (baseLine) => {
|
|
2508
|
+
const appendReceiptSummary = (/** @type {string} */ baseLine) => {
|
|
2290
2509
|
const tickReceipt = payload?.tickReceipt;
|
|
2291
2510
|
if (!tickReceipt || typeof tickReceipt !== 'object') {
|
|
2292
2511
|
return `${baseLine}\n`;
|
|
@@ -2322,6 +2541,10 @@ function renderSeek(payload) {
|
|
|
2322
2541
|
};
|
|
2323
2542
|
};
|
|
2324
2543
|
|
|
2544
|
+
if (payload.action === 'clear-cache') {
|
|
2545
|
+
return `${payload.message}\n`;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2325
2548
|
if (payload.action === 'list') {
|
|
2326
2549
|
if (payload.cursors.length === 0) {
|
|
2327
2550
|
return 'No saved cursors.\n';
|
|
@@ -2381,9 +2604,8 @@ function renderSeek(payload) {
|
|
|
2381
2604
|
* Called by non-seek commands (query, path, check, etc.) that should
|
|
2382
2605
|
* honour an active seek cursor.
|
|
2383
2606
|
*
|
|
2384
|
-
* @
|
|
2385
|
-
* @param {
|
|
2386
|
-
* @param {Object} persistence - GraphPersistencePort adapter
|
|
2607
|
+
* @param {WarpGraphInstance} graph - WarpGraph instance
|
|
2608
|
+
* @param {Persistence} persistence - GraphPersistencePort adapter
|
|
2387
2609
|
* @param {string} graphName - Name of the WARP graph
|
|
2388
2610
|
* @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
2611
|
*/
|
|
@@ -2405,7 +2627,6 @@ async function applyCursorCeiling(graph, persistence, graphName) {
|
|
|
2405
2627
|
* maxTick to avoid the cost of discoverTicks(); the banner then omits the
|
|
2406
2628
|
* "of {maxTick}" suffix. Only the seek handler itself populates maxTick.
|
|
2407
2629
|
*
|
|
2408
|
-
* @private
|
|
2409
2630
|
* @param {{active: boolean, tick: number|null, maxTick: number|null}} cursorInfo - Result from applyCursorCeiling
|
|
2410
2631
|
* @param {number|null} maxTick - Maximum Lamport tick (from discoverTicks), or null if unknown
|
|
2411
2632
|
* @returns {void}
|
|
@@ -2417,6 +2638,10 @@ function emitCursorWarning(cursorInfo, maxTick) {
|
|
|
2417
2638
|
}
|
|
2418
2639
|
}
|
|
2419
2640
|
|
|
2641
|
+
/**
|
|
2642
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
2643
|
+
* @returns {Promise<{payload: *, exitCode: number}>}
|
|
2644
|
+
*/
|
|
2420
2645
|
async function handleView({ options, args }) {
|
|
2421
2646
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2422
2647
|
throw usageError('view command requires an interactive terminal (TTY)');
|
|
@@ -2427,13 +2652,14 @@ async function handleView({ options, args }) {
|
|
|
2427
2652
|
: 'list';
|
|
2428
2653
|
|
|
2429
2654
|
try {
|
|
2655
|
+
// @ts-expect-error — optional peer dependency, may not be installed
|
|
2430
2656
|
const { startTui } = await import('@git-stunts/git-warp-tui');
|
|
2431
2657
|
await startTui({
|
|
2432
2658
|
repo: options.repo || '.',
|
|
2433
2659
|
graph: options.graph || 'default',
|
|
2434
2660
|
mode: viewMode,
|
|
2435
2661
|
});
|
|
2436
|
-
} catch (err) {
|
|
2662
|
+
} catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
|
|
2437
2663
|
if (err.code === 'ERR_MODULE_NOT_FOUND' || (err.message && err.message.includes('Cannot find module'))) {
|
|
2438
2664
|
throw usageError(
|
|
2439
2665
|
'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
|
|
@@ -2445,7 +2671,8 @@ async function handleView({ options, args }) {
|
|
|
2445
2671
|
return { payload: undefined, exitCode: 0 };
|
|
2446
2672
|
}
|
|
2447
2673
|
|
|
2448
|
-
|
|
2674
|
+
/** @type {Map<string, Function>} */
|
|
2675
|
+
const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
|
|
2449
2676
|
['info', handleInfo],
|
|
2450
2677
|
['query', handleQuery],
|
|
2451
2678
|
['path', handlePath],
|
|
@@ -2455,7 +2682,7 @@ const COMMANDS = new Map([
|
|
|
2455
2682
|
['seek', handleSeek],
|
|
2456
2683
|
['view', handleView],
|
|
2457
2684
|
['install-hooks', handleInstallHooks],
|
|
2458
|
-
]);
|
|
2685
|
+
]));
|
|
2459
2686
|
|
|
2460
2687
|
/**
|
|
2461
2688
|
* CLI entry point. Parses arguments, dispatches to the appropriate command handler,
|
|
@@ -2492,12 +2719,13 @@ async function main() {
|
|
|
2492
2719
|
throw usageError(`--view is not supported for '${command}'. Supported commands: ${VIEW_SUPPORTED_COMMANDS.join(', ')}`);
|
|
2493
2720
|
}
|
|
2494
2721
|
|
|
2495
|
-
const result = await handler({
|
|
2722
|
+
const result = await /** @type {Function} */ (handler)({
|
|
2496
2723
|
command,
|
|
2497
2724
|
args: positionals.slice(1),
|
|
2498
2725
|
options,
|
|
2499
2726
|
});
|
|
2500
2727
|
|
|
2728
|
+
/** @type {{payload: *, exitCode: number}} */
|
|
2501
2729
|
const normalized = result && typeof result === 'object' && 'payload' in result
|
|
2502
2730
|
? result
|
|
2503
2731
|
: { payload: result, exitCode: EXIT_CODES.OK };
|
|
@@ -2505,13 +2733,15 @@ async function main() {
|
|
|
2505
2733
|
if (normalized.payload !== undefined) {
|
|
2506
2734
|
emit(normalized.payload, { json: options.json, command, view: options.view });
|
|
2507
2735
|
}
|
|
2508
|
-
process.
|
|
2736
|
+
// Use process.exit() to avoid waiting for fire-and-forget I/O (e.g. seek cache writes).
|
|
2737
|
+
process.exit(normalized.exitCode ?? EXIT_CODES.OK);
|
|
2509
2738
|
}
|
|
2510
2739
|
|
|
2511
2740
|
main().catch((error) => {
|
|
2512
2741
|
const exitCode = error instanceof CliError ? error.exitCode : EXIT_CODES.INTERNAL;
|
|
2513
2742
|
const code = error instanceof CliError ? error.code : 'E_INTERNAL';
|
|
2514
2743
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
2744
|
+
/** @type {{error: {code: string, message: string, cause?: *}}} */
|
|
2515
2745
|
const payload = { error: { code, message } };
|
|
2516
2746
|
|
|
2517
2747
|
if (error && error.cause) {
|
|
@@ -2523,5 +2753,5 @@ main().catch((error) => {
|
|
|
2523
2753
|
} else {
|
|
2524
2754
|
process.stderr.write(renderError(payload));
|
|
2525
2755
|
}
|
|
2526
|
-
process.exitCode
|
|
2756
|
+
process.exit(exitCode);
|
|
2527
2757
|
});
|