@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.
Files changed (104) hide show
  1. package/README.md +6 -3
  2. package/bin/warp-graph.js +371 -141
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +8 -3
  6. package/src/domain/WarpGraph.js +263 -147
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +19 -0
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/roaring.js +5 -5
  67. package/src/domain/utils/seekCacheKey.js +32 -0
  68. package/src/domain/warp/PatchSession.js +3 -3
  69. package/src/domain/warp/Writer.js +2 -2
  70. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  71. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  72. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  73. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  74. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  75. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  76. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  77. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  78. package/src/infrastructure/codecs/CborCodec.js +16 -8
  79. package/src/ports/BlobPort.js +2 -2
  80. package/src/ports/CodecPort.js +2 -2
  81. package/src/ports/CommitPort.js +8 -21
  82. package/src/ports/ConfigPort.js +3 -3
  83. package/src/ports/CryptoPort.js +7 -7
  84. package/src/ports/GraphPersistencePort.js +12 -14
  85. package/src/ports/HttpServerPort.js +1 -5
  86. package/src/ports/IndexStoragePort.js +1 -0
  87. package/src/ports/LoggerPort.js +9 -9
  88. package/src/ports/RefPort.js +5 -5
  89. package/src/ports/SeekCachePort.js +73 -0
  90. package/src/ports/TreePort.js +3 -3
  91. package/src/visualization/layouts/converters.js +14 -7
  92. package/src/visualization/layouts/elkAdapter.js +17 -4
  93. package/src/visualization/layouts/elkLayout.js +23 -7
  94. package/src/visualization/layouts/index.js +3 -3
  95. package/src/visualization/renderers/ascii/check.js +30 -17
  96. package/src/visualization/renderers/ascii/graph.js +92 -1
  97. package/src/visualization/renderers/ascii/history.js +28 -26
  98. package/src/visualization/renderers/ascii/info.js +9 -7
  99. package/src/visualization/renderers/ascii/materialize.js +20 -16
  100. package/src/visualization/renderers/ascii/opSummary.js +15 -7
  101. package/src/visualization/renderers/ascii/path.js +1 -1
  102. package/src/visualization/renderers/ascii/seek.js +19 -5
  103. package/src/visualization/renderers/ascii/table.js +1 -1
  104. 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
- options[matched.key] = result.value;
224
- return { consumed: result.consumed };
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 {Object} persistence - GraphPersistencePort adapter
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<Object>} Graph info object
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 {Object} options - Parsed CLI options
368
- * @param {string} [options.repo] - Repository path
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 {Object} payload - Command result payload
807
- * @param {Object} options
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 {Object} params
929
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
978
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
1071
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
1121
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
1258
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
1303
- * @param {Object} params.persistence - GraphPersistencePort adapter
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 {Object} params
1342
- * @param {Object} params.options - Parsed CLI options
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 {Object} params
1543
- * @param {Object} params.options - Parsed CLI options
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
- * @private
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<{tick: number, mode?: string}|null>} Cursor object, or null if no active cursor
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
- * @private
1628
- * @param {Object} persistence - GraphPersistencePort adapter
1829
+ * @param {Persistence} persistence - GraphPersistencePort adapter
1629
1830
  * @param {string} graphName - Name of the WARP graph
1630
- * @param {{tick: number, mode?: string}} cursor - Cursor state to persist
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
- * @private
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
- * @private
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<{tick: number, mode?: string}|null>} Cursor object, or null if not found
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
- * @private
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 {{tick: number, mode?: string}} cursor - Cursor state to persist
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
- * @private
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
- * @private
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 {{action: string, tickValue: string|null, name: string|null}} Parsed spec
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
- * Dispatches to the appropriate logic based on the parsed action:
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 existing = await readSavedCursor(persistence, graphName, seekSpec.name);
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: ${seekSpec.name}`);
2178
+ throw notFoundError(`Saved cursor not found: ${dropName}`);
1954
2179
  }
1955
- await deleteSavedCursor(persistence, graphName, seekSpec.name);
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 saved = await readSavedCursor(persistence, graphName, seekSpec.name);
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: ${seekSpec.name}`);
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
- * @private
2121
- * @param {Map<string, {ticks: number[], tipSha: string|null}>} perWriter - Per-writer tick data
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, {ticks: number[], tipSha: string|null}>} perWriter - Per-writer tick data
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
- * @private
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
- * @private
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
- * @private
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
- * @private
2224
- * @param {Object} params
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
- * @private
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
- * @private
2385
- * @param {Object} graph - WarpGraph instance
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
- const COMMANDS = new Map([
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.exitCode = normalized.exitCode ?? EXIT_CODES.OK;
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 = exitCode;
2756
+ process.exit(exitCode);
2527
2757
  });