@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
@@ -17,13 +17,14 @@ import { ProvenancePayload } from './services/ProvenancePayload.js';
17
17
  import { diffStates, isEmptyDiff } from './services/StateDiff.js';
18
18
  import { orsetContains, orsetElements } from './crdt/ORSet.js';
19
19
  import defaultCodec from './utils/defaultCodec.js';
20
+ import defaultCrypto from './utils/defaultCrypto.js';
20
21
  import { decodePatchMessage, detectMessageKind, encodeAnchorMessage } from './services/WarpMessageCodec.js';
21
22
  import { loadCheckpoint, materializeIncremental, create as createCheckpointCommit } from './services/CheckpointService.js';
22
23
  import { createFrontier, updateFrontier } from './services/Frontier.js';
23
24
  import { createVersionVector, vvClone, vvIncrement } from './crdt/VersionVector.js';
24
25
  import { DEFAULT_GC_POLICY, shouldRunGC, executeGC } from './services/GCPolicy.js';
25
26
  import { collectGCMetrics } from './services/GCMetrics.js';
26
- import { computeAppliedVV } from './services/CheckpointSerializerV5.js';
27
+ import { computeAppliedVV, serializeFullStateV5, deserializeFullStateV5 } from './services/CheckpointSerializerV5.js';
27
28
  import { computeStateHashV5 } from './services/StateSerializerV5.js';
28
29
  import {
29
30
  createSyncRequest,
@@ -48,8 +49,13 @@ import OperationAbortedError from './errors/OperationAbortedError.js';
48
49
  import { compareEventIds } from './utils/EventId.js';
49
50
  import { TemporalQuery } from './services/TemporalQuery.js';
50
51
  import HttpSyncServer from './services/HttpSyncServer.js';
52
+ import { buildSeekCacheKey } from './utils/seekCacheKey.js';
51
53
  import defaultClock from './utils/defaultClock.js';
52
54
 
55
+ /**
56
+ * @typedef {import('../ports/GraphPersistencePort.js').default & import('../ports/RefPort.js').default & import('../ports/CommitPort.js').default & import('../ports/BlobPort.js').default & import('../ports/TreePort.js').default & import('../ports/ConfigPort.js').default} FullPersistence
57
+ */
58
+
53
59
  const DEFAULT_SYNC_SERVER_MAX_BYTES = 4 * 1024 * 1024;
54
60
  const DEFAULT_SYNC_WITH_RETRIES = 3;
55
61
  const DEFAULT_SYNC_WITH_BASE_DELAY_MS = 250;
@@ -99,10 +105,11 @@ export default class WarpGraph {
99
105
  * @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
100
106
  * @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
101
107
  * @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
108
+ * @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
102
109
  */
103
- constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec }) {
104
- /** @type {import('../ports/GraphPersistencePort.js').default} */
105
- this._persistence = persistence;
110
+ constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec, seekCache }) {
111
+ /** @type {FullPersistence} */
112
+ this._persistence = /** @type {FullPersistence} */ (persistence);
106
113
 
107
114
  /** @type {string} */
108
115
  this._graphName = graphName;
@@ -146,7 +153,7 @@ export default class WarpGraph {
146
153
  /** @type {MaterializedGraph|null} */
147
154
  this._materializedGraph = null;
148
155
 
149
- /** @type {import('./utils/LRUCache.js').default|null} */
156
+ /** @type {import('./utils/LRUCache.js').default<string, {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}>|null} */
150
157
  this._adjacencyCache = adjacencyCacheSize > 0 ? new LRUCache(adjacencyCacheSize) : null;
151
158
 
152
159
  /** @type {Map<string, string>|null} */
@@ -158,8 +165,8 @@ export default class WarpGraph {
158
165
  /** @type {import('../ports/ClockPort.js').default} */
159
166
  this._clock = clock || defaultClock;
160
167
 
161
- /** @type {import('../ports/CryptoPort.js').default|undefined} */
162
- this._crypto = crypto;
168
+ /** @type {import('../ports/CryptoPort.js').default} */
169
+ this._crypto = crypto || defaultCrypto;
163
170
 
164
171
  /** @type {import('../ports/CodecPort.js').default} */
165
172
  this._codec = codec || defaultCodec;
@@ -167,7 +174,7 @@ export default class WarpGraph {
167
174
  /** @type {'reject'|'cascade'|'warn'} */
168
175
  this._onDeleteWithData = onDeleteWithData;
169
176
 
170
- /** @type {Array<{onChange: Function, onError?: Function}>} */
177
+ /** @type {Array<{onChange: Function, onError?: Function, pendingReplay?: boolean}>} */
171
178
  this._subscribers = [];
172
179
 
173
180
  /** @type {import('./services/JoinReducer.js').WarpStateV5|null} */
@@ -187,6 +194,32 @@ export default class WarpGraph {
187
194
 
188
195
  /** @type {Map<string, string>|null} */
189
196
  this._cachedFrontier = null;
197
+
198
+ /** @type {import('../ports/SeekCachePort.js').default|null} */
199
+ this._seekCache = seekCache || null;
200
+
201
+ /** @type {boolean} */
202
+ this._provenanceDegraded = false;
203
+ }
204
+
205
+ /**
206
+ * Returns the attached seek cache, or null if none is set.
207
+ * @returns {import('../ports/SeekCachePort.js').default|null}
208
+ */
209
+ get seekCache() {
210
+ return this._seekCache;
211
+ }
212
+
213
+ /**
214
+ * Attaches a persistent seek cache after construction.
215
+ *
216
+ * Useful when the cache adapter cannot be created until after the
217
+ * graph is opened (e.g. the CLI wires it based on flags).
218
+ *
219
+ * @param {import('../ports/SeekCachePort.js').default} cache - SeekCachePort implementation
220
+ */
221
+ setSeekCache(cache) {
222
+ this._seekCache = cache;
190
223
  }
191
224
 
192
225
  /**
@@ -227,6 +260,7 @@ export default class WarpGraph {
227
260
  * @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
228
261
  * @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
229
262
  * @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
263
+ * @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
230
264
  * @returns {Promise<WarpGraph>} The opened graph instance
231
265
  * @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
232
266
  *
@@ -237,7 +271,7 @@ export default class WarpGraph {
237
271
  * writerId: 'node-1'
238
272
  * });
239
273
  */
240
- static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec }) {
274
+ static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache }) {
241
275
  // Validate inputs
242
276
  validateGraphName(graphName);
243
277
  validateWriterId(writerId);
@@ -269,7 +303,7 @@ export default class WarpGraph {
269
303
  }
270
304
  }
271
305
 
272
- const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec });
306
+ const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache });
273
307
 
274
308
  // Validate migration boundary
275
309
  await graph._validateMigrationBoundary();
@@ -295,7 +329,7 @@ export default class WarpGraph {
295
329
 
296
330
  /**
297
331
  * Gets the persistence adapter.
298
- * @returns {import('../ports/GraphPersistencePort.js').default} The persistence adapter
332
+ * @returns {FullPersistence} The persistence adapter
299
333
  */
300
334
  get persistence() {
301
335
  return this._persistence;
@@ -337,9 +371,9 @@ export default class WarpGraph {
337
371
  getCurrentState: () => this._cachedState,
338
372
  expectedParentSha: parentSha,
339
373
  onDeleteWithData: this._onDeleteWithData,
340
- onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
374
+ onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
341
375
  codec: this._codec,
342
- logger: this._logger,
376
+ logger: this._logger || undefined,
343
377
  });
344
378
  }
345
379
 
@@ -348,7 +382,7 @@ export default class WarpGraph {
348
382
  *
349
383
  * @param {string} writerId - The writer ID to load patches for
350
384
  * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
351
- * @returns {Promise<Array<{patch: import('./types/WarpTypes.js').PatchV1, sha: string}>>} Array of patches
385
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
352
386
  */
353
387
  async getWriterPatches(writerId, stopAtSha = null) {
354
388
  return await this._loadWriterPatches(writerId, stopAtSha);
@@ -399,7 +433,7 @@ export default class WarpGraph {
399
433
  *
400
434
  * @param {string} writerId - The writer ID to load patches for
401
435
  * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
402
- * @returns {Promise<Array<{patch: import('./types/WarpTypes.js').PatchV1, sha: string}>>} Array of patches
436
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
403
437
  * @private
404
438
  */
405
439
  async _loadWriterPatches(writerId, stopAtSha = null) {
@@ -430,7 +464,7 @@ export default class WarpGraph {
430
464
 
431
465
  // Read the patch blob
432
466
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
433
- const patch = this._codec.decode(patchBuffer);
467
+ const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
434
468
 
435
469
  patches.push({ patch, sha: currentSha });
436
470
 
@@ -474,8 +508,8 @@ export default class WarpGraph {
474
508
  incoming.get(to).push({ neighborId: from, label });
475
509
  }
476
510
 
477
- const sortNeighbors = (list) => {
478
- list.sort((a, b) => {
511
+ const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
512
+ list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
479
513
  if (a.neighborId !== b.neighborId) {
480
514
  return a.neighborId < b.neighborId ? -1 : 1;
481
515
  }
@@ -529,7 +563,7 @@ export default class WarpGraph {
529
563
  * provenance index, and frontier tracking.
530
564
  *
531
565
  * @param {string} writerId - The writer ID that committed the patch
532
- * @param {{patch?: Object, sha?: string}} [opts] - Commit details
566
+ * @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
533
567
  * @private
534
568
  */
535
569
  async _onPatchCommitted(writerId, { patch, sha } = {}) {
@@ -538,11 +572,11 @@ export default class WarpGraph {
538
572
  // Eager re-materialize: apply the just-committed patch to cached state
539
573
  // Only when the cache is clean — applying a patch to stale state would be incorrect
540
574
  if (this._cachedState && !this._stateDirty && patch && sha) {
541
- joinPatch(this._cachedState, patch, sha);
575
+ joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
542
576
  await this._setMaterializedState(this._cachedState);
543
577
  // Update provenance index with new patch
544
578
  if (this._provenanceIndex) {
545
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
579
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
546
580
  }
547
581
  // Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
548
582
  if (this._lastFrontier) {
@@ -561,9 +595,9 @@ export default class WarpGraph {
561
595
  async _materializeGraph() {
562
596
  const state = await this.materialize();
563
597
  if (!this._materializedGraph || this._materializedGraph.state !== state) {
564
- await this._setMaterializedState(state);
598
+ await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
565
599
  }
566
- return this._materializedGraph;
600
+ return /** @type {MaterializedGraph} */ (this._materializedGraph);
567
601
  }
568
602
 
569
603
  /**
@@ -602,14 +636,16 @@ export default class WarpGraph {
602
636
 
603
637
  // When ceiling is active, delegate to ceiling-aware path (with its own cache)
604
638
  if (ceiling !== null) {
605
- return await this._materializeWithCeiling(ceiling, collectReceipts, t0);
639
+ return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
606
640
  }
607
641
 
608
642
  try {
609
643
  // Check for checkpoint
610
644
  const checkpoint = await this._loadLatestCheckpoint();
611
645
 
646
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
612
647
  let state;
648
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
613
649
  let receipts;
614
650
  let patchCount = 0;
615
651
 
@@ -617,20 +653,21 @@ export default class WarpGraph {
617
653
  if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
618
654
  const patches = await this._loadPatchesSince(checkpoint);
619
655
  if (collectReceipts) {
620
- const result = reduceV5(patches, checkpoint.state, { receipts: true });
656
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (patches), /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (checkpoint.state), { receipts: true })); // TODO(ts-cleanup): type patch array
621
657
  state = result.state;
622
658
  receipts = result.receipts;
623
659
  } else {
624
- state = reduceV5(patches, checkpoint.state);
660
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (patches), /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (checkpoint.state))); // TODO(ts-cleanup): type patch array
625
661
  }
626
662
  patchCount = patches.length;
627
663
 
628
664
  // Build provenance index: start from checkpoint index if present, then add new patches
629
- this._provenanceIndex = checkpoint.provenanceIndex
630
- ? checkpoint.provenanceIndex.clone()
665
+ const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
666
+ this._provenanceIndex = ckPI
667
+ ? ckPI.clone()
631
668
  : new ProvenanceIndex();
632
669
  for (const { patch, sha } of patches) {
633
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
670
+ /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
634
671
  }
635
672
  } else {
636
673
  // 1. Discover all writers
@@ -661,11 +698,11 @@ export default class WarpGraph {
661
698
  } else {
662
699
  // 5. Reduce all patches to state
663
700
  if (collectReceipts) {
664
- const result = reduceV5(allPatches, undefined, { receipts: true });
701
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (allPatches), undefined, { receipts: true })); // TODO(ts-cleanup): type patch array
665
702
  state = result.state;
666
703
  receipts = result.receipts;
667
704
  } else {
668
- state = reduceV5(allPatches);
705
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
669
706
  }
670
707
  patchCount = allPatches.length;
671
708
 
@@ -679,6 +716,7 @@ export default class WarpGraph {
679
716
  }
680
717
 
681
718
  await this._setMaterializedState(state);
719
+ this._provenanceDegraded = false;
682
720
  this._cachedCeiling = null;
683
721
  this._cachedFrontier = null;
684
722
  this._lastFrontier = await this.getFrontier();
@@ -712,11 +750,11 @@ export default class WarpGraph {
712
750
  this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
713
751
 
714
752
  if (collectReceipts) {
715
- return { state, receipts };
753
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
716
754
  }
717
755
  return state;
718
756
  } catch (err) {
719
- this._logTiming('materialize', t0, { error: err });
757
+ this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
720
758
  throw err;
721
759
  }
722
760
  }
@@ -770,12 +808,13 @@ export default class WarpGraph {
770
808
 
771
809
  // Cache hit: same ceiling, clean state, AND frontier unchanged.
772
810
  // Bypass cache when collectReceipts is true — cached path has no receipts.
811
+ const cf = this._cachedFrontier;
773
812
  if (
774
813
  this._cachedState && !this._stateDirty &&
775
814
  ceiling === this._cachedCeiling && !collectReceipts &&
776
- this._cachedFrontier !== null &&
777
- this._cachedFrontier.size === frontier.size &&
778
- [...frontier].every(([w, sha]) => this._cachedFrontier.get(w) === sha)
815
+ cf !== null &&
816
+ cf.size === frontier.size &&
817
+ [...frontier].every(([w, sha]) => cf.get(w) === sha)
779
818
  ) {
780
819
  return this._cachedState;
781
820
  }
@@ -785,6 +824,7 @@ export default class WarpGraph {
785
824
  if (writerIds.length === 0 || ceiling <= 0) {
786
825
  const state = createEmptyStateV5();
787
826
  this._provenanceIndex = new ProvenanceIndex();
827
+ this._provenanceDegraded = false;
788
828
  await this._setMaterializedState(state);
789
829
  this._cachedCeiling = ceiling;
790
830
  this._cachedFrontier = frontier;
@@ -795,6 +835,32 @@ export default class WarpGraph {
795
835
  return state;
796
836
  }
797
837
 
838
+ // Persistent cache check — skip when collectReceipts is requested
839
+ let cacheKey;
840
+ if (this._seekCache && !collectReceipts) {
841
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
842
+ try {
843
+ const cached = await this._seekCache.get(cacheKey);
844
+ if (cached) {
845
+ try {
846
+ const state = deserializeFullStateV5(cached, { codec: this._codec });
847
+ this._provenanceIndex = new ProvenanceIndex();
848
+ this._provenanceDegraded = true;
849
+ await this._setMaterializedState(state);
850
+ this._cachedCeiling = ceiling;
851
+ this._cachedFrontier = frontier;
852
+ this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
853
+ return state;
854
+ } catch {
855
+ // Corrupted payload — self-heal by removing the bad entry
856
+ try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
857
+ }
858
+ }
859
+ } catch {
860
+ // Cache read failed — fall through to full materialization
861
+ }
862
+ }
863
+
798
864
  const allPatches = [];
799
865
  for (const writerId of writerIds) {
800
866
  const writerPatches = await this._loadWriterPatches(writerId);
@@ -805,7 +871,9 @@ export default class WarpGraph {
805
871
  }
806
872
  }
807
873
 
874
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
808
875
  let state;
876
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
809
877
  let receipts;
810
878
 
811
879
  if (allPatches.length === 0) {
@@ -814,27 +882,37 @@ export default class WarpGraph {
814
882
  receipts = [];
815
883
  }
816
884
  } else if (collectReceipts) {
817
- const result = reduceV5(allPatches, undefined, { receipts: true });
885
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (allPatches), undefined, { receipts: true })); // TODO(ts-cleanup): type patch array
818
886
  state = result.state;
819
887
  receipts = result.receipts;
820
888
  } else {
821
- state = reduceV5(allPatches);
889
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
822
890
  }
823
891
 
824
892
  this._provenanceIndex = new ProvenanceIndex();
825
893
  for (const { patch, sha } of allPatches) {
826
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
894
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
827
895
  }
896
+ this._provenanceDegraded = false;
828
897
 
829
898
  await this._setMaterializedState(state);
830
899
  this._cachedCeiling = ceiling;
831
900
  this._cachedFrontier = frontier;
832
901
 
902
+ // Store to persistent cache (fire-and-forget — failure is non-fatal)
903
+ if (this._seekCache && !collectReceipts && allPatches.length > 0) {
904
+ if (!cacheKey) {
905
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
906
+ }
907
+ const buf = serializeFullStateV5(state, { codec: this._codec });
908
+ this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
909
+ }
910
+
833
911
  // Skip auto-checkpoint and GC — this is an exploratory read
834
912
  this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
835
913
 
836
914
  if (collectReceipts) {
837
- return { state, receipts };
915
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
838
916
  }
839
917
  return state;
840
918
  }
@@ -883,16 +961,16 @@ export default class WarpGraph {
883
961
  }
884
962
 
885
963
  // Capture pre-merge counts for receipt
886
- const beforeNodes = this._cachedState.nodeAlive.elements.size;
887
- const beforeEdges = this._cachedState.edgeAlive.elements.size;
964
+ const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
965
+ const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
888
966
  const beforeFrontierSize = this._cachedState.observedFrontier.size;
889
967
 
890
968
  // Perform the join
891
969
  const mergedState = joinStates(this._cachedState, otherState);
892
970
 
893
971
  // Calculate receipt
894
- const afterNodes = mergedState.nodeAlive.elements.size;
895
- const afterEdges = mergedState.edgeAlive.elements.size;
972
+ const afterNodes = orsetElements(mergedState.nodeAlive).length;
973
+ const afterEdges = orsetElements(mergedState.edgeAlive).length;
896
974
  const afterFrontierSize = mergedState.observedFrontier.size;
897
975
 
898
976
  // Count property changes (keys that existed in both but have different values)
@@ -954,7 +1032,7 @@ export default class WarpGraph {
954
1032
  * @example
955
1033
  * // Time-travel to a previous checkpoint
956
1034
  * const oldState = await graph.materializeAt('abc123');
957
- * console.log('Nodes at checkpoint:', [...oldState.nodeAlive.elements.keys()]);
1035
+ * console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
958
1036
  */
959
1037
  async materializeAt(checkpointSha) {
960
1038
  // 1. Discover current writers to build target frontier
@@ -971,7 +1049,7 @@ export default class WarpGraph {
971
1049
  }
972
1050
 
973
1051
  // 3. Create a patch loader function for incremental materialization
974
- const patchLoader = async (writerId, fromSha, toSha) => {
1052
+ const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
975
1053
  // Load patches from fromSha (exclusive) to toSha (inclusive)
976
1054
  // Walk from toSha back to fromSha
977
1055
  const patches = [];
@@ -1004,7 +1082,7 @@ export default class WarpGraph {
1004
1082
 
1005
1083
  // 4. Call materializeIncremental with the checkpoint and target frontier
1006
1084
  const state = await materializeIncremental({
1007
- persistence: this._persistence,
1085
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1008
1086
  graphName: this._graphName,
1009
1087
  checkpointSha,
1010
1088
  targetFrontier,
@@ -1048,23 +1126,24 @@ export default class WarpGraph {
1048
1126
  // 3. Materialize current state (reuse cached if fresh, guard against recursion)
1049
1127
  const prevCheckpointing = this._checkpointing;
1050
1128
  this._checkpointing = true;
1129
+ /** @type {import('./services/JoinReducer.js').WarpStateV5} */
1051
1130
  let state;
1052
1131
  try {
1053
- state = (this._cachedState && !this._stateDirty)
1132
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
1054
1133
  ? this._cachedState
1055
- : await this.materialize();
1134
+ : await this.materialize());
1056
1135
  } finally {
1057
1136
  this._checkpointing = prevCheckpointing;
1058
1137
  }
1059
1138
 
1060
1139
  // 4. Call CheckpointService.create() with provenance index if available
1061
1140
  const checkpointSha = await createCheckpointCommit({
1062
- persistence: this._persistence,
1141
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1063
1142
  graphName: this._graphName,
1064
1143
  state,
1065
1144
  frontier,
1066
1145
  parents,
1067
- provenanceIndex: this._provenanceIndex,
1146
+ provenanceIndex: this._provenanceIndex || undefined,
1068
1147
  crypto: this._crypto,
1069
1148
  codec: this._codec,
1070
1149
  });
@@ -1078,7 +1157,7 @@ export default class WarpGraph {
1078
1157
  // 6. Return checkpoint SHA
1079
1158
  return checkpointSha;
1080
1159
  } catch (err) {
1081
- this._logTiming('createCheckpoint', t0, { error: err });
1160
+ this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
1082
1161
  throw err;
1083
1162
  }
1084
1163
  }
@@ -1172,6 +1251,7 @@ export default class WarpGraph {
1172
1251
  */
1173
1252
  async discoverTicks() {
1174
1253
  const writerIds = await this.discoverWriters();
1254
+ /** @type {Set<number>} */
1175
1255
  const globalTickSet = new Set();
1176
1256
  const perWriter = new Map();
1177
1257
 
@@ -1179,6 +1259,7 @@ export default class WarpGraph {
1179
1259
  const writerRef = buildWriterRef(this._graphName, writerId);
1180
1260
  const tipSha = await this._persistence.readRef(writerRef);
1181
1261
  const writerTicks = [];
1262
+ /** @type {Record<number, string>} */
1182
1263
  const tickShas = {};
1183
1264
 
1184
1265
  if (tipSha) {
@@ -1256,7 +1337,7 @@ export default class WarpGraph {
1256
1337
  /**
1257
1338
  * Loads the latest checkpoint for this graph.
1258
1339
  *
1259
- * @returns {Promise<{state: Object, frontier: Map, stateHash: string, schema: number}|null>} The checkpoint or null
1340
+ * @returns {Promise<{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number, provenanceIndex?: import('./services/ProvenanceIndex.js').ProvenanceIndex}|null>} The checkpoint or null
1260
1341
  * @private
1261
1342
  */
1262
1343
  async _loadLatestCheckpoint() {
@@ -1298,7 +1379,7 @@ export default class WarpGraph {
1298
1379
  if (kind === 'patch') {
1299
1380
  const patchMeta = decodePatchMessage(nodeInfo.message);
1300
1381
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
1301
- const patch = this._codec.decode(patchBuffer);
1382
+ const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
1302
1383
 
1303
1384
  // If any patch has schema:1, we have v1 history
1304
1385
  if (patch.schema === 1 || patch.schema === undefined) {
@@ -1313,8 +1394,8 @@ export default class WarpGraph {
1313
1394
  /**
1314
1395
  * Loads patches since a checkpoint for incremental materialization.
1315
1396
  *
1316
- * @param {{state: Object, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
1317
- * @returns {Promise<Array<{patch: import('./types/WarpTypes.js').PatchV1, sha: string}>>} Patches since checkpoint
1397
+ * @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
1398
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Patches since checkpoint
1318
1399
  * @private
1319
1400
  */
1320
1401
  async _loadPatchesSince(checkpoint) {
@@ -1396,7 +1477,7 @@ export default class WarpGraph {
1396
1477
  *
1397
1478
  * @param {string} writerId - The writer ID for this patch
1398
1479
  * @param {string} incomingSha - The incoming patch commit SHA
1399
- * @param {{state: Object, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
1480
+ * @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
1400
1481
  * @returns {Promise<void>}
1401
1482
  * @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
1402
1483
  * @throws {Error} If patch does not extend checkpoint head (writer fork detected)
@@ -1444,18 +1525,19 @@ export default class WarpGraph {
1444
1525
  _maybeRunGC(state) {
1445
1526
  try {
1446
1527
  const metrics = collectGCMetrics(state);
1528
+ /** @type {import('./services/GCPolicy.js').GCInputMetrics} */
1447
1529
  const inputMetrics = {
1448
1530
  ...metrics,
1449
1531
  patchesSinceCompaction: this._patchesSinceGC,
1450
1532
  timeSinceCompaction: Date.now() - this._lastGCTime,
1451
1533
  };
1452
- const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
1534
+ const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1453
1535
 
1454
1536
  if (!shouldRun) {
1455
1537
  return;
1456
1538
  }
1457
1539
 
1458
- if (this._gcPolicy.enabled) {
1540
+ if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
1459
1541
  const appliedVV = computeAppliedVV(state);
1460
1542
  const result = executeGC(state, appliedVV);
1461
1543
  this._lastGCTime = Date.now();
@@ -1494,11 +1576,15 @@ export default class WarpGraph {
1494
1576
  return { ran: false, result: null, reasons: [] };
1495
1577
  }
1496
1578
 
1497
- const metrics = collectGCMetrics(this._cachedState);
1498
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1499
- metrics.lastCompactionTime = this._lastGCTime;
1579
+ const rawMetrics = collectGCMetrics(this._cachedState);
1580
+ /** @type {import('./services/GCPolicy.js').GCInputMetrics} */
1581
+ const metrics = {
1582
+ ...rawMetrics,
1583
+ patchesSinceCompaction: this._patchesSinceGC,
1584
+ timeSinceCompaction: this._lastGCTime > 0 ? Date.now() - this._lastGCTime : 0,
1585
+ };
1500
1586
 
1501
- const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
1587
+ const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1502
1588
 
1503
1589
  if (!shouldRun) {
1504
1590
  return { ran: false, result: null, reasons: [] };
@@ -1545,7 +1631,7 @@ export default class WarpGraph {
1545
1631
 
1546
1632
  return result;
1547
1633
  } catch (err) {
1548
- this._logTiming('runGC', t0, { error: err });
1634
+ this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
1549
1635
  throw err;
1550
1636
  }
1551
1637
  }
@@ -1567,10 +1653,15 @@ export default class WarpGraph {
1567
1653
  return null;
1568
1654
  }
1569
1655
 
1570
- const metrics = collectGCMetrics(this._cachedState);
1571
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1572
- metrics.lastCompactionTime = this._lastGCTime;
1573
- return metrics;
1656
+ const rawMetrics = collectGCMetrics(this._cachedState);
1657
+ return {
1658
+ ...rawMetrics,
1659
+ nodeCount: rawMetrics.nodeLiveDots,
1660
+ edgeCount: rawMetrics.edgeLiveDots,
1661
+ tombstoneCount: rawMetrics.totalTombstones,
1662
+ patchesSinceCompaction: this._patchesSinceGC,
1663
+ lastCompactionTime: this._lastGCTime,
1664
+ };
1574
1665
  }
1575
1666
 
1576
1667
  /**
@@ -1653,6 +1744,7 @@ export default class WarpGraph {
1653
1744
  */
1654
1745
  async status() {
1655
1746
  // Determine cachedState
1747
+ /** @type {'fresh' | 'stale' | 'none'} */
1656
1748
  let cachedState;
1657
1749
  if (this._cachedState === null) {
1658
1750
  cachedState = 'none';
@@ -1702,7 +1794,7 @@ export default class WarpGraph {
1702
1794
  * One handler's error does not prevent other handlers from being called.
1703
1795
  *
1704
1796
  * @param {Object} options - Subscription options
1705
- * @param {(diff: import('./services/StateDiff.js').StateDiff) => void} options.onChange - Called with diff when graph changes
1797
+ * @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with diff when graph changes
1706
1798
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1707
1799
  * @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
1708
1800
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1745,7 +1837,7 @@ export default class WarpGraph {
1745
1837
  } catch (err) {
1746
1838
  if (onError) {
1747
1839
  try {
1748
- onError(err);
1840
+ onError(/** @type {Error} */ (err));
1749
1841
  } catch {
1750
1842
  // onError itself threw — swallow to prevent cascade
1751
1843
  }
@@ -1782,7 +1874,7 @@ export default class WarpGraph {
1782
1874
  *
1783
1875
  * @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
1784
1876
  * @param {Object} options - Watch options
1785
- * @param {(diff: import('./services/StateDiff.js').StateDiff) => void} options.onChange - Called with filtered diff when matching changes occur
1877
+ * @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with filtered diff when matching changes occur
1786
1878
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1787
1879
  * @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
1788
1880
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1823,31 +1915,32 @@ export default class WarpGraph {
1823
1915
 
1824
1916
  // Pattern matching: same logic as QueryBuilder.match()
1825
1917
  // Pre-compile pattern matcher once for performance
1918
+ /** @type {(nodeId: string) => boolean} */
1826
1919
  let matchesPattern;
1827
1920
  if (pattern === '*') {
1828
1921
  matchesPattern = () => true;
1829
1922
  } else if (pattern.includes('*')) {
1830
1923
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
1831
1924
  const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
1832
- matchesPattern = (nodeId) => regex.test(nodeId);
1925
+ matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
1833
1926
  } else {
1834
- matchesPattern = (nodeId) => nodeId === pattern;
1927
+ matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
1835
1928
  }
1836
1929
 
1837
1930
  // Filtered onChange that only passes matching changes
1838
- const filteredOnChange = (diff) => {
1931
+ const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
1839
1932
  const filteredDiff = {
1840
1933
  nodes: {
1841
1934
  added: diff.nodes.added.filter(matchesPattern),
1842
1935
  removed: diff.nodes.removed.filter(matchesPattern),
1843
1936
  },
1844
1937
  edges: {
1845
- added: diff.edges.added.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
1846
- removed: diff.edges.removed.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
1938
+ added: diff.edges.added.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
1939
+ removed: diff.edges.removed.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
1847
1940
  },
1848
1941
  props: {
1849
- set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
1850
- removed: diff.props.removed.filter(p => matchesPattern(p.nodeId)),
1942
+ set: diff.props.set.filter((/** @type {import('./services/StateDiff.js').PropSet} */ p) => matchesPattern(p.nodeId)),
1943
+ removed: diff.props.removed.filter((/** @type {import('./services/StateDiff.js').PropRemoved} */ p) => matchesPattern(p.nodeId)),
1851
1944
  },
1852
1945
  };
1853
1946
 
@@ -1869,6 +1962,7 @@ export default class WarpGraph {
1869
1962
  const subscription = this.subscribe({ onChange: filteredOnChange, onError });
1870
1963
 
1871
1964
  // Polling: periodically check frontier and auto-materialize if changed
1965
+ /** @type {ReturnType<typeof setInterval>|null} */
1872
1966
  let pollIntervalId = null;
1873
1967
  let pollInFlight = false;
1874
1968
  if (poll) {
@@ -1950,7 +2044,7 @@ export default class WarpGraph {
1950
2044
  * Creates a sync request to send to a remote peer.
1951
2045
  * The request contains the local frontier for comparison.
1952
2046
  *
1953
- * @returns {Promise<{type: 'sync-request', frontier: Map<string, string>}>} The sync request
2047
+ * @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
1954
2048
  * @throws {Error} If listing refs fails
1955
2049
  *
1956
2050
  * @example
@@ -1965,8 +2059,8 @@ export default class WarpGraph {
1965
2059
  /**
1966
2060
  * Processes an incoming sync request and returns patches the requester needs.
1967
2061
  *
1968
- * @param {{type: 'sync-request', frontier: Map<string, string>}} request - The incoming sync request
1969
- * @returns {Promise<{type: 'sync-response', frontier: Map, patches: Map}>} The sync response
2062
+ * @param {import('./services/SyncProtocol.js').SyncRequest} request - The incoming sync request
2063
+ * @returns {Promise<import('./services/SyncProtocol.js').SyncResponse>} The sync response
1970
2064
  * @throws {Error} If listing refs or reading patches fails
1971
2065
  *
1972
2066
  * @example
@@ -1979,7 +2073,7 @@ export default class WarpGraph {
1979
2073
  return await processSyncRequest(
1980
2074
  request,
1981
2075
  localFrontier,
1982
- this._persistence,
2076
+ /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1983
2077
  this._graphName,
1984
2078
  { codec: this._codec }
1985
2079
  );
@@ -1991,8 +2085,8 @@ export default class WarpGraph {
1991
2085
  *
1992
2086
  * **Requires a cached state.**
1993
2087
  *
1994
- * @param {{type: 'sync-response', frontier: Map, patches: Map}} response - The sync response
1995
- * @returns {{state: Object, frontier: Map, applied: number}} Result with updated state
2088
+ * @param {import('./services/SyncProtocol.js').SyncResponse} response - The sync response
2089
+ * @returns {{state: import('./services/JoinReducer.js').WarpStateV5, applied: number}} Result with updated state
1996
2090
  * @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
1997
2091
  *
1998
2092
  * @example
@@ -2007,8 +2101,8 @@ export default class WarpGraph {
2007
2101
  });
2008
2102
  }
2009
2103
 
2010
- const currentFrontier = this._cachedState.observedFrontier;
2011
- const result = applySyncResponse(response, this._cachedState, currentFrontier);
2104
+ const currentFrontier = /** @type {any} */ (this._cachedState.observedFrontier); // TODO(ts-cleanup): narrow port type
2105
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} */ (applySyncResponse(response, this._cachedState, currentFrontier));
2012
2106
 
2013
2107
  // Update cached state
2014
2108
  this._cachedState = result.state;
@@ -2079,7 +2173,7 @@ export default class WarpGraph {
2079
2173
  let targetUrl = null;
2080
2174
  if (!isDirectPeer) {
2081
2175
  try {
2082
- targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(remote);
2176
+ targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(/** @type {string} */ (remote));
2083
2177
  } catch {
2084
2178
  throw new SyncError('Invalid remote URL', {
2085
2179
  code: 'E_SYNC_REMOTE_URL',
@@ -2104,13 +2198,13 @@ export default class WarpGraph {
2104
2198
  }
2105
2199
 
2106
2200
  let attempt = 0;
2107
- const emit = (type, payload = {}) => {
2201
+ const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
2108
2202
  if (typeof onStatus === 'function') {
2109
- onStatus({ type, attempt, ...payload });
2203
+ onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
2110
2204
  }
2111
2205
  };
2112
2206
 
2113
- const shouldRetry = (err) => {
2207
+ const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
2114
2208
  if (isDirectPeer) { return false; }
2115
2209
  if (err instanceof SyncError) {
2116
2210
  return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
@@ -2140,7 +2234,7 @@ export default class WarpGraph {
2140
2234
  const combinedSignal = signal
2141
2235
  ? AbortSignal.any([timeoutSignal, signal])
2142
2236
  : timeoutSignal;
2143
- return fetch(targetUrl.toString(), {
2237
+ return fetch(/** @type {URL} */ (targetUrl).toString(), {
2144
2238
  method: 'POST',
2145
2239
  headers: {
2146
2240
  'content-type': 'application/json',
@@ -2151,7 +2245,7 @@ export default class WarpGraph {
2151
2245
  });
2152
2246
  });
2153
2247
  } catch (err) {
2154
- if (err?.name === 'AbortError') {
2248
+ if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
2155
2249
  throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
2156
2250
  }
2157
2251
  if (err instanceof TimeoutError) {
@@ -2162,7 +2256,7 @@ export default class WarpGraph {
2162
2256
  }
2163
2257
  throw new SyncError('Network error', {
2164
2258
  code: 'E_SYNC_NETWORK',
2165
- context: { message: err?.message },
2259
+ context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
2166
2260
  });
2167
2261
  }
2168
2262
 
@@ -2223,9 +2317,9 @@ export default class WarpGraph {
2223
2317
  jitter: 'decorrelated',
2224
2318
  signal,
2225
2319
  shouldRetry,
2226
- onRetry: (error, attemptNumber, delayMs) => {
2320
+ onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
2227
2321
  if (typeof onStatus === 'function') {
2228
- onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
2322
+ onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
2229
2323
  }
2230
2324
  },
2231
2325
  });
@@ -2234,12 +2328,12 @@ export default class WarpGraph {
2234
2328
 
2235
2329
  if (materializeAfterSync) {
2236
2330
  if (!this._cachedState) { await this.materialize(); }
2237
- return { ...syncResult, state: this._cachedState };
2331
+ return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
2238
2332
  }
2239
2333
  return syncResult;
2240
2334
  } catch (err) {
2241
- this._logTiming('syncWith', t0, { error: err });
2242
- if (err?.name === 'AbortError') {
2335
+ this._logTiming('syncWith', t0, { error: /** @type {Error} */ (err) });
2336
+ if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
2243
2337
  const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
2244
2338
  if (typeof onStatus === 'function') {
2245
2339
  onStatus({ type: 'failed', attempt, error: abortedError });
@@ -2247,14 +2341,14 @@ export default class WarpGraph {
2247
2341
  throw abortedError;
2248
2342
  }
2249
2343
  if (err instanceof RetryExhaustedError) {
2250
- const cause = err.cause || err;
2344
+ const cause = /** @type {Error} */ (err.cause || err);
2251
2345
  if (typeof onStatus === 'function') {
2252
2346
  onStatus({ type: 'failed', attempt: err.attempts, error: cause });
2253
2347
  }
2254
2348
  throw cause;
2255
2349
  }
2256
2350
  if (typeof onStatus === 'function') {
2257
- onStatus({ type: 'failed', attempt, error: err });
2351
+ onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
2258
2352
  }
2259
2353
  throw err;
2260
2354
  }
@@ -2273,7 +2367,7 @@ export default class WarpGraph {
2273
2367
  * @throws {Error} If port is not a number
2274
2368
  * @throws {Error} If httpPort adapter is not provided
2275
2369
  */
2276
- async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = {}) {
2370
+ async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = /** @type {any} */ ({})) { // TODO(ts-cleanup): needs options type
2277
2371
  if (typeof port !== 'number') {
2278
2372
  throw new Error('serve() requires a numeric port');
2279
2373
  }
@@ -2318,8 +2412,8 @@ export default class WarpGraph {
2318
2412
  */
2319
2413
  async writer(writerId) {
2320
2414
  // Build config adapters for resolveWriterId
2321
- const configGet = async (key) => await this._persistence.configGet(key);
2322
- const configSet = async (key, value) => await this._persistence.configSet(key, value);
2415
+ const configGet = async (/** @type {string} */ key) => await this._persistence.configGet(key);
2416
+ const configSet = async (/** @type {string} */ key, /** @type {string} */ value) => await this._persistence.configSet(key, value);
2323
2417
 
2324
2418
  // Resolve the writer ID
2325
2419
  const resolvedWriterId = await resolveWriterId({
@@ -2330,13 +2424,13 @@ export default class WarpGraph {
2330
2424
  });
2331
2425
 
2332
2426
  return new Writer({
2333
- persistence: this._persistence,
2427
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2334
2428
  graphName: this._graphName,
2335
2429
  writerId: resolvedWriterId,
2336
2430
  versionVector: this._versionVector,
2337
- getCurrentState: () => this._cachedState,
2431
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2338
2432
  onDeleteWithData: this._onDeleteWithData,
2339
- onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
2433
+ onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
2340
2434
  codec: this._codec,
2341
2435
  });
2342
2436
  }
@@ -2384,13 +2478,13 @@ export default class WarpGraph {
2384
2478
  }
2385
2479
 
2386
2480
  return new Writer({
2387
- persistence: this._persistence,
2481
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2388
2482
  graphName: this._graphName,
2389
2483
  writerId: freshWriterId,
2390
2484
  versionVector: this._versionVector,
2391
- getCurrentState: () => this._cachedState,
2485
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2392
2486
  onDeleteWithData: this._onDeleteWithData,
2393
- onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
2487
+ onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
2394
2488
  codec: this._codec,
2395
2489
  });
2396
2490
  }
@@ -2545,7 +2639,8 @@ export default class WarpGraph {
2545
2639
  */
2546
2640
  async hasNode(nodeId) {
2547
2641
  await this._ensureFreshState();
2548
- return orsetContains(this._cachedState.nodeAlive, nodeId);
2642
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2643
+ return orsetContains(s.nodeAlive, nodeId);
2549
2644
  }
2550
2645
 
2551
2646
  /**
@@ -2570,15 +2665,16 @@ export default class WarpGraph {
2570
2665
  */
2571
2666
  async getNodeProps(nodeId) {
2572
2667
  await this._ensureFreshState();
2668
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2573
2669
 
2574
2670
  // Check if node exists
2575
- if (!orsetContains(this._cachedState.nodeAlive, nodeId)) {
2671
+ if (!orsetContains(s.nodeAlive, nodeId)) {
2576
2672
  return null;
2577
2673
  }
2578
2674
 
2579
2675
  // Collect all properties for this node
2580
2676
  const props = new Map();
2581
- for (const [propKey, register] of this._cachedState.prop) {
2677
+ for (const [propKey, register] of s.prop) {
2582
2678
  const decoded = decodePropKey(propKey);
2583
2679
  if (decoded.nodeId === nodeId) {
2584
2680
  props.set(decoded.propKey, register.value);
@@ -2612,26 +2708,28 @@ export default class WarpGraph {
2612
2708
  */
2613
2709
  async getEdgeProps(from, to, label) {
2614
2710
  await this._ensureFreshState();
2711
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2615
2712
 
2616
2713
  // Check if edge exists
2617
2714
  const edgeKey = encodeEdgeKey(from, to, label);
2618
- if (!orsetContains(this._cachedState.edgeAlive, edgeKey)) {
2715
+ if (!orsetContains(s.edgeAlive, edgeKey)) {
2619
2716
  return null;
2620
2717
  }
2621
2718
 
2622
2719
  // Check node liveness for both endpoints
2623
- if (!orsetContains(this._cachedState.nodeAlive, from) ||
2624
- !orsetContains(this._cachedState.nodeAlive, to)) {
2720
+ if (!orsetContains(s.nodeAlive, from) ||
2721
+ !orsetContains(s.nodeAlive, to)) {
2625
2722
  return null;
2626
2723
  }
2627
2724
 
2628
2725
  // Determine the birth EventId for clean-slate filtering
2629
- const birthEvent = this._cachedState.edgeBirthEvent?.get(edgeKey);
2726
+ const birthEvent = s.edgeBirthEvent?.get(edgeKey);
2630
2727
 
2631
2728
  // Collect all properties for this edge, filtering out stale props
2632
2729
  // (props set before the edge's most recent re-add)
2730
+ /** @type {Record<string, any>} */
2633
2731
  const props = {};
2634
- for (const [propKey, register] of this._cachedState.prop) {
2732
+ for (const [propKey, register] of s.prop) {
2635
2733
  if (!isEdgePropKey(propKey)) {
2636
2734
  continue;
2637
2735
  }
@@ -2673,11 +2771,13 @@ export default class WarpGraph {
2673
2771
  */
2674
2772
  async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
2675
2773
  await this._ensureFreshState();
2774
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2676
2775
 
2776
+ /** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
2677
2777
  const neighbors = [];
2678
2778
 
2679
2779
  // Iterate over all visible edges
2680
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2780
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2681
2781
  const { from, to, label } = decodeEdgeKey(edgeKey);
2682
2782
 
2683
2783
  // Filter by label if specified
@@ -2688,15 +2788,15 @@ export default class WarpGraph {
2688
2788
  // Check edge direction and collect neighbors
2689
2789
  if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
2690
2790
  // Ensure target node is visible
2691
- if (orsetContains(this._cachedState.nodeAlive, to)) {
2692
- neighbors.push({ nodeId: to, label, direction: 'outgoing' });
2791
+ if (orsetContains(s.nodeAlive, to)) {
2792
+ neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
2693
2793
  }
2694
2794
  }
2695
2795
 
2696
2796
  if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
2697
2797
  // Ensure source node is visible
2698
- if (orsetContains(this._cachedState.nodeAlive, from)) {
2699
- neighbors.push({ nodeId: from, label, direction: 'incoming' });
2798
+ if (orsetContains(s.nodeAlive, from)) {
2799
+ neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
2700
2800
  }
2701
2801
  }
2702
2802
  }
@@ -2721,7 +2821,8 @@ export default class WarpGraph {
2721
2821
  */
2722
2822
  async getNodes() {
2723
2823
  await this._ensureFreshState();
2724
- return [...orsetElements(this._cachedState.nodeAlive)];
2824
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2825
+ return [...orsetElements(s.nodeAlive)];
2725
2826
  }
2726
2827
 
2727
2828
  /**
@@ -2744,12 +2845,13 @@ export default class WarpGraph {
2744
2845
  */
2745
2846
  async getEdges() {
2746
2847
  await this._ensureFreshState();
2848
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2747
2849
 
2748
2850
  // Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
2749
2851
  // Filters out stale props using full EventId ordering via compareEventIds
2750
2852
  // against the edge's birth EventId (clean-slate semantics on re-add)
2751
2853
  const edgePropsByKey = new Map();
2752
- for (const [propKey, register] of this._cachedState.prop) {
2854
+ for (const [propKey, register] of s.prop) {
2753
2855
  if (!isEdgePropKey(propKey)) {
2754
2856
  continue;
2755
2857
  }
@@ -2757,7 +2859,7 @@ export default class WarpGraph {
2757
2859
  const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
2758
2860
 
2759
2861
  // Clean-slate filter: skip props from before the edge's current incarnation
2760
- const birthEvent = this._cachedState.edgeBirthEvent?.get(ek);
2862
+ const birthEvent = s.edgeBirthEvent?.get(ek);
2761
2863
  if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
2762
2864
  continue;
2763
2865
  }
@@ -2771,11 +2873,11 @@ export default class WarpGraph {
2771
2873
  }
2772
2874
 
2773
2875
  const edges = [];
2774
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2876
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2775
2877
  const { from, to, label } = decodeEdgeKey(edgeKey);
2776
2878
  // Only include edges where both endpoints are visible
2777
- if (orsetContains(this._cachedState.nodeAlive, from) &&
2778
- orsetContains(this._cachedState.nodeAlive, to)) {
2879
+ if (orsetContains(s.nodeAlive, from) &&
2880
+ orsetContains(s.nodeAlive, to)) {
2779
2881
  const props = edgePropsByKey.get(edgeKey) || {};
2780
2882
  edges.push({ from, to, label, props });
2781
2883
  }
@@ -2794,7 +2896,8 @@ export default class WarpGraph {
2794
2896
  */
2795
2897
  async getPropertyCount() {
2796
2898
  await this._ensureFreshState();
2797
- return this._cachedState.prop.size;
2899
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2900
+ return s.prop.size;
2798
2901
  }
2799
2902
 
2800
2903
  // ============================================================================
@@ -2913,9 +3016,9 @@ export default class WarpGraph {
2913
3016
  try {
2914
3017
  validateGraphName(resolvedForkName);
2915
3018
  } catch (err) {
2916
- throw new ForkError(`Invalid fork name: ${err.message}`, {
3019
+ throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
2917
3020
  code: 'E_FORK_NAME_INVALID',
2918
- context: { forkName: resolvedForkName, originalError: err.message },
3021
+ context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
2919
3022
  });
2920
3023
  }
2921
3024
 
@@ -2934,9 +3037,9 @@ export default class WarpGraph {
2934
3037
  try {
2935
3038
  validateWriterId(resolvedForkWriterId);
2936
3039
  } catch (err) {
2937
- throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
3040
+ throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
2938
3041
  code: 'E_FORK_WRITER_ID_INVALID',
2939
- context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
3042
+ context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
2940
3043
  });
2941
3044
  }
2942
3045
 
@@ -2951,10 +3054,10 @@ export default class WarpGraph {
2951
3054
  writerId: resolvedForkWriterId,
2952
3055
  gcPolicy: this._gcPolicy,
2953
3056
  adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
2954
- checkpointPolicy: this._checkpointPolicy,
3057
+ checkpointPolicy: this._checkpointPolicy || undefined,
2955
3058
  autoMaterialize: this._autoMaterialize,
2956
3059
  onDeleteWithData: this._onDeleteWithData,
2957
- logger: this._logger,
3060
+ logger: this._logger || undefined,
2958
3061
  clock: this._clock,
2959
3062
  crypto: this._crypto,
2960
3063
  codec: this._codec,
@@ -2966,7 +3069,7 @@ export default class WarpGraph {
2966
3069
 
2967
3070
  return forkGraph;
2968
3071
  } catch (err) {
2969
- this._logTiming('fork', t0, { error: err });
3072
+ this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
2970
3073
  throw err;
2971
3074
  }
2972
3075
  }
@@ -3020,13 +3123,13 @@ export default class WarpGraph {
3020
3123
  const t0 = this._clock.now();
3021
3124
 
3022
3125
  try {
3023
- const wormhole = await createWormholeImpl({
3126
+ const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
3024
3127
  persistence: this._persistence,
3025
3128
  graphName: this._graphName,
3026
3129
  fromSha,
3027
3130
  toSha,
3028
3131
  codec: this._codec,
3029
- });
3132
+ }));
3030
3133
 
3031
3134
  this._logTiming('createWormhole', t0, {
3032
3135
  metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
@@ -3034,7 +3137,7 @@ export default class WarpGraph {
3034
3137
 
3035
3138
  return wormhole;
3036
3139
  } catch (err) {
3037
- this._logTiming('createWormhole', t0, { error: err });
3140
+ this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
3038
3141
  throw err;
3039
3142
  }
3040
3143
  }
@@ -3068,6 +3171,12 @@ export default class WarpGraph {
3068
3171
  async patchesFor(entityId) {
3069
3172
  await this._ensureFreshState();
3070
3173
 
3174
+ if (this._provenanceDegraded) {
3175
+ throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
3176
+ code: 'E_PROVENANCE_DEGRADED',
3177
+ });
3178
+ }
3179
+
3071
3180
  if (!this._provenanceIndex) {
3072
3181
  throw new QueryError('No provenance index. Call materialize() first.', {
3073
3182
  code: 'E_NO_STATE',
@@ -3129,6 +3238,12 @@ export default class WarpGraph {
3129
3238
  // Ensure fresh state before accessing provenance index
3130
3239
  await this._ensureFreshState();
3131
3240
 
3241
+ if (this._provenanceDegraded) {
3242
+ throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
3243
+ code: 'E_PROVENANCE_DEGRADED',
3244
+ });
3245
+ }
3246
+
3132
3247
  if (!this._provenanceIndex) {
3133
3248
  throw new QueryError('No provenance index. Call materialize() first.', {
3134
3249
  code: 'E_NO_STATE',
@@ -3163,7 +3278,7 @@ export default class WarpGraph {
3163
3278
  this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
3164
3279
 
3165
3280
  if (collectReceipts) {
3166
- const result = reduceV5(sortedPatches, undefined, { receipts: true });
3281
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(sortedPatches, undefined, { receipts: true }));
3167
3282
  return {
3168
3283
  state: result.state,
3169
3284
  patchCount: sortedPatches.length,
@@ -3177,7 +3292,7 @@ export default class WarpGraph {
3177
3292
  patchCount: sortedPatches.length,
3178
3293
  };
3179
3294
  } catch (err) {
3180
- this._logTiming('materializeSlice', t0, { error: err });
3295
+ this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
3181
3296
  throw err;
3182
3297
  }
3183
3298
  }
@@ -3214,7 +3329,7 @@ export default class WarpGraph {
3214
3329
  visited.add(entityId);
3215
3330
 
3216
3331
  // Get all patches that affected this entity
3217
- const patchShas = this._provenanceIndex.patchesFor(entityId);
3332
+ const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
3218
3333
 
3219
3334
  for (const sha of patchShas) {
3220
3335
  if (cone.has(sha)) {
@@ -3226,8 +3341,9 @@ export default class WarpGraph {
3226
3341
  cone.set(sha, patch);
3227
3342
 
3228
3343
  // Add read dependencies to the queue
3229
- if (patch && patch.reads) {
3230
- for (const readEntity of patch.reads) {
3344
+ const patchReads = /** @type {any} */ (patch)?.reads; // TODO(ts-cleanup): type patch array
3345
+ if (patchReads) {
3346
+ for (const readEntity of patchReads) {
3231
3347
  if (!visited.has(readEntity)) {
3232
3348
  queue.push(readEntity);
3233
3349
  }
@@ -3274,7 +3390,7 @@ export default class WarpGraph {
3274
3390
 
3275
3391
  const patchMeta = decodePatchMessage(nodeInfo.message);
3276
3392
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
3277
- return this._codec.decode(patchBuffer);
3393
+ return /** @type {Object} */ (this._codec.decode(patchBuffer));
3278
3394
  }
3279
3395
 
3280
3396
  /**
@@ -3302,8 +3418,8 @@ export default class WarpGraph {
3302
3418
  * Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
3303
3419
  * This ensures deterministic ordering regardless of discovery order.
3304
3420
  *
3305
- * @param {Array<{patch: Object, sha: string}>} patches - Unsorted patch entries
3306
- * @returns {Array<{patch: Object, sha: string}>} Sorted patch entries
3421
+ * @param {Array<{patch: any, sha: string}>} patches - Unsorted patch entries
3422
+ * @returns {Array<{patch: any, sha: string}>} Sorted patch entries
3307
3423
  * @private
3308
3424
  */
3309
3425
  _sortPatchesCausally(patches) {