@git-stunts/git-warp 10.1.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 (106) hide show
  1. package/README.md +31 -4
  2. package/bin/warp-graph.js +1242 -59
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +13 -3
  6. package/src/domain/WarpGraph.js +487 -140
  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 +106 -15
  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/parseCursorBlob.js +51 -0
  67. package/src/domain/utils/roaring.js +5 -5
  68. package/src/domain/utils/seekCacheKey.js +32 -0
  69. package/src/domain/warp/PatchSession.js +3 -3
  70. package/src/domain/warp/Writer.js +2 -2
  71. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  72. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  73. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  74. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  75. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  76. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  77. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  78. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  79. package/src/infrastructure/codecs/CborCodec.js +16 -8
  80. package/src/ports/BlobPort.js +2 -2
  81. package/src/ports/CodecPort.js +2 -2
  82. package/src/ports/CommitPort.js +8 -21
  83. package/src/ports/ConfigPort.js +3 -3
  84. package/src/ports/CryptoPort.js +7 -7
  85. package/src/ports/GraphPersistencePort.js +12 -14
  86. package/src/ports/HttpServerPort.js +1 -5
  87. package/src/ports/IndexStoragePort.js +1 -0
  88. package/src/ports/LoggerPort.js +9 -9
  89. package/src/ports/RefPort.js +5 -5
  90. package/src/ports/SeekCachePort.js +73 -0
  91. package/src/ports/TreePort.js +3 -3
  92. package/src/visualization/layouts/converters.js +14 -7
  93. package/src/visualization/layouts/elkAdapter.js +24 -11
  94. package/src/visualization/layouts/elkLayout.js +23 -7
  95. package/src/visualization/layouts/index.js +3 -3
  96. package/src/visualization/renderers/ascii/check.js +30 -17
  97. package/src/visualization/renderers/ascii/graph.js +122 -16
  98. package/src/visualization/renderers/ascii/history.js +29 -90
  99. package/src/visualization/renderers/ascii/index.js +1 -1
  100. package/src/visualization/renderers/ascii/info.js +9 -7
  101. package/src/visualization/renderers/ascii/materialize.js +20 -16
  102. package/src/visualization/renderers/ascii/opSummary.js +81 -0
  103. package/src/visualization/renderers/ascii/path.js +1 -1
  104. package/src/visualization/renderers/ascii/seek.js +344 -0
  105. package/src/visualization/renderers/ascii/table.js +1 -1
  106. 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} */
@@ -178,6 +185,41 @@ export default class WarpGraph {
178
185
 
179
186
  /** @type {import('./services/TemporalQuery.js').TemporalQuery|null} */
180
187
  this._temporalQuery = null;
188
+
189
+ /** @type {number|null} */
190
+ this._seekCeiling = null;
191
+
192
+ /** @type {number|null} */
193
+ this._cachedCeiling = null;
194
+
195
+ /** @type {Map<string, string>|null} */
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;
181
223
  }
182
224
 
183
225
  /**
@@ -218,6 +260,7 @@ export default class WarpGraph {
218
260
  * @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
219
261
  * @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
220
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)
221
264
  * @returns {Promise<WarpGraph>} The opened graph instance
222
265
  * @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
223
266
  *
@@ -228,7 +271,7 @@ export default class WarpGraph {
228
271
  * writerId: 'node-1'
229
272
  * });
230
273
  */
231
- 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 }) {
232
275
  // Validate inputs
233
276
  validateGraphName(graphName);
234
277
  validateWriterId(writerId);
@@ -260,7 +303,7 @@ export default class WarpGraph {
260
303
  }
261
304
  }
262
305
 
263
- 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 });
264
307
 
265
308
  // Validate migration boundary
266
309
  await graph._validateMigrationBoundary();
@@ -286,7 +329,7 @@ export default class WarpGraph {
286
329
 
287
330
  /**
288
331
  * Gets the persistence adapter.
289
- * @returns {import('../ports/GraphPersistencePort.js').default} The persistence adapter
332
+ * @returns {FullPersistence} The persistence adapter
290
333
  */
291
334
  get persistence() {
292
335
  return this._persistence;
@@ -328,9 +371,9 @@ export default class WarpGraph {
328
371
  getCurrentState: () => this._cachedState,
329
372
  expectedParentSha: parentSha,
330
373
  onDeleteWithData: this._onDeleteWithData,
331
- onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
374
+ onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
332
375
  codec: this._codec,
333
- logger: this._logger,
376
+ logger: this._logger || undefined,
334
377
  });
335
378
  }
336
379
 
@@ -339,7 +382,7 @@ export default class WarpGraph {
339
382
  *
340
383
  * @param {string} writerId - The writer ID to load patches for
341
384
  * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
342
- * @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
343
386
  */
344
387
  async getWriterPatches(writerId, stopAtSha = null) {
345
388
  return await this._loadWriterPatches(writerId, stopAtSha);
@@ -390,7 +433,7 @@ export default class WarpGraph {
390
433
  *
391
434
  * @param {string} writerId - The writer ID to load patches for
392
435
  * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
393
- * @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
394
437
  * @private
395
438
  */
396
439
  async _loadWriterPatches(writerId, stopAtSha = null) {
@@ -421,7 +464,7 @@ export default class WarpGraph {
421
464
 
422
465
  // Read the patch blob
423
466
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
424
- const patch = this._codec.decode(patchBuffer);
467
+ const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
425
468
 
426
469
  patches.push({ patch, sha: currentSha });
427
470
 
@@ -465,8 +508,8 @@ export default class WarpGraph {
465
508
  incoming.get(to).push({ neighborId: from, label });
466
509
  }
467
510
 
468
- const sortNeighbors = (list) => {
469
- 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) => {
470
513
  if (a.neighborId !== b.neighborId) {
471
514
  return a.neighborId < b.neighborId ? -1 : 1;
472
515
  }
@@ -520,7 +563,7 @@ export default class WarpGraph {
520
563
  * provenance index, and frontier tracking.
521
564
  *
522
565
  * @param {string} writerId - The writer ID that committed the patch
523
- * @param {{patch?: Object, sha?: string}} [opts] - Commit details
566
+ * @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
524
567
  * @private
525
568
  */
526
569
  async _onPatchCommitted(writerId, { patch, sha } = {}) {
@@ -529,11 +572,11 @@ export default class WarpGraph {
529
572
  // Eager re-materialize: apply the just-committed patch to cached state
530
573
  // Only when the cache is clean — applying a patch to stale state would be incorrect
531
574
  if (this._cachedState && !this._stateDirty && patch && sha) {
532
- joinPatch(this._cachedState, patch, sha);
575
+ joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
533
576
  await this._setMaterializedState(this._cachedState);
534
577
  // Update provenance index with new patch
535
578
  if (this._provenanceIndex) {
536
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
579
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
537
580
  }
538
581
  // Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
539
582
  if (this._lastFrontier) {
@@ -552,9 +595,9 @@ export default class WarpGraph {
552
595
  async _materializeGraph() {
553
596
  const state = await this.materialize();
554
597
  if (!this._materializedGraph || this._materializedGraph.state !== state) {
555
- await this._setMaterializedState(state);
598
+ await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
556
599
  }
557
- return this._materializedGraph;
600
+ return /** @type {MaterializedGraph} */ (this._materializedGraph);
558
601
  }
559
602
 
560
603
  /**
@@ -570,11 +613,16 @@ export default class WarpGraph {
570
613
  * When false or omitted (default), returns just the state for backward
571
614
  * compatibility with zero receipt overhead.
572
615
  *
616
+ * When a Lamport ceiling is active (via `options.ceiling` or the
617
+ * instance-level `_seekCeiling`), delegates to a ceiling-aware path
618
+ * that replays only patches with `lamport <= ceiling`, bypassing
619
+ * checkpoints, auto-checkpoint, and GC.
620
+ *
573
621
  * Side effects: Updates internal cached state, version vector, last frontier,
574
622
  * and patches-since-checkpoint counter. May trigger auto-checkpoint and GC
575
623
  * based on configured policies. Notifies subscribers if state changed.
576
624
  *
577
- * @param {{receipts?: boolean}} [options] - Optional configuration
625
+ * @param {{receipts?: boolean, ceiling?: number|null}} [options] - Optional configuration
578
626
  * @returns {Promise<import('./services/JoinReducer.js').WarpStateV5|{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}>} The materialized graph state, or { state, receipts } when receipts enabled
579
627
  * @throws {Error} If checkpoint loading fails or patch decoding fails
580
628
  * @throws {Error} If writer ref access or patch blob reading fails
@@ -583,12 +631,21 @@ export default class WarpGraph {
583
631
  const t0 = this._clock.now();
584
632
  // ZERO-COST: only resolve receipts flag when options provided
585
633
  const collectReceipts = options && options.receipts;
634
+ // Resolve ceiling: explicit option > instance-level seek ceiling > null (latest)
635
+ const ceiling = this._resolveCeiling(options);
636
+
637
+ // When ceiling is active, delegate to ceiling-aware path (with its own cache)
638
+ if (ceiling !== null) {
639
+ return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
640
+ }
586
641
 
587
642
  try {
588
643
  // Check for checkpoint
589
644
  const checkpoint = await this._loadLatestCheckpoint();
590
645
 
646
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
591
647
  let state;
648
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
592
649
  let receipts;
593
650
  let patchCount = 0;
594
651
 
@@ -596,20 +653,21 @@ export default class WarpGraph {
596
653
  if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
597
654
  const patches = await this._loadPatchesSince(checkpoint);
598
655
  if (collectReceipts) {
599
- 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
600
657
  state = result.state;
601
658
  receipts = result.receipts;
602
659
  } else {
603
- 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
604
661
  }
605
662
  patchCount = patches.length;
606
663
 
607
664
  // Build provenance index: start from checkpoint index if present, then add new patches
608
- this._provenanceIndex = checkpoint.provenanceIndex
609
- ? checkpoint.provenanceIndex.clone()
665
+ const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
666
+ this._provenanceIndex = ckPI
667
+ ? ckPI.clone()
610
668
  : new ProvenanceIndex();
611
669
  for (const { patch, sha } of patches) {
612
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
670
+ /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
613
671
  }
614
672
  } else {
615
673
  // 1. Discover all writers
@@ -640,11 +698,11 @@ export default class WarpGraph {
640
698
  } else {
641
699
  // 5. Reduce all patches to state
642
700
  if (collectReceipts) {
643
- 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
644
702
  state = result.state;
645
703
  receipts = result.receipts;
646
704
  } else {
647
- state = reduceV5(allPatches);
705
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
648
706
  }
649
707
  patchCount = allPatches.length;
650
708
 
@@ -658,6 +716,9 @@ export default class WarpGraph {
658
716
  }
659
717
 
660
718
  await this._setMaterializedState(state);
719
+ this._provenanceDegraded = false;
720
+ this._cachedCeiling = null;
721
+ this._cachedFrontier = null;
661
722
  this._lastFrontier = await this.getFrontier();
662
723
  this._patchesSinceCheckpoint = patchCount;
663
724
 
@@ -689,15 +750,173 @@ export default class WarpGraph {
689
750
  this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
690
751
 
691
752
  if (collectReceipts) {
692
- return { state, receipts };
753
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
693
754
  }
694
755
  return state;
695
756
  } catch (err) {
696
- this._logTiming('materialize', t0, { error: err });
757
+ this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
697
758
  throw err;
698
759
  }
699
760
  }
700
761
 
762
+ /**
763
+ * Resolves the effective ceiling from options and instance state.
764
+ *
765
+ * Precedence: explicit `ceiling` in options overrides the instance-level
766
+ * `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
767
+ * `{ ceiling: null }` explicitly clears the seek ceiling for that call
768
+ * (returns `null`), while omitting the key falls through to `_seekCeiling`.
769
+ *
770
+ * @param {{ceiling?: number|null}} [options] - Options object; when the
771
+ * `ceiling` key is present (even if `null`), its value takes precedence
772
+ * @returns {number|null} Lamport ceiling to apply, or `null` for latest
773
+ * @private
774
+ */
775
+ _resolveCeiling(options) {
776
+ if (options && options.ceiling !== undefined) {
777
+ return options.ceiling;
778
+ }
779
+ return this._seekCeiling;
780
+ }
781
+
782
+ /**
783
+ * Materializes the graph with a Lamport ceiling (time-travel).
784
+ *
785
+ * Bypasses checkpoints entirely — replays all patches from all writers,
786
+ * filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
787
+ * and GC since this is an exploratory read.
788
+ *
789
+ * Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
790
+ * is bypassed when the writer frontier has advanced (new writers or
791
+ * updated tips) or when `collectReceipts` is `true` because the cached
792
+ * path does not retain receipt data.
793
+ *
794
+ * @param {number} ceiling - Maximum Lamport tick to include (patches with
795
+ * `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
796
+ * @param {boolean} collectReceipts - When `true`, return receipts alongside
797
+ * state and skip the ceiling cache
798
+ * @param {number} t0 - Start timestamp for performance logging
799
+ * @returns {Promise<import('./services/JoinReducer.js').WarpStateV5 |
800
+ * {state: import('./services/JoinReducer.js').WarpStateV5,
801
+ * receipts: import('./types/TickReceipt.js').TickReceipt[]}>}
802
+ * Plain state when `collectReceipts` is falsy; `{ state, receipts }`
803
+ * when truthy
804
+ * @private
805
+ */
806
+ async _materializeWithCeiling(ceiling, collectReceipts, t0) {
807
+ const frontier = await this.getFrontier();
808
+
809
+ // Cache hit: same ceiling, clean state, AND frontier unchanged.
810
+ // Bypass cache when collectReceipts is true — cached path has no receipts.
811
+ const cf = this._cachedFrontier;
812
+ if (
813
+ this._cachedState && !this._stateDirty &&
814
+ ceiling === this._cachedCeiling && !collectReceipts &&
815
+ cf !== null &&
816
+ cf.size === frontier.size &&
817
+ [...frontier].every(([w, sha]) => cf.get(w) === sha)
818
+ ) {
819
+ return this._cachedState;
820
+ }
821
+
822
+ const writerIds = [...frontier.keys()];
823
+
824
+ if (writerIds.length === 0 || ceiling <= 0) {
825
+ const state = createEmptyStateV5();
826
+ this._provenanceIndex = new ProvenanceIndex();
827
+ this._provenanceDegraded = false;
828
+ await this._setMaterializedState(state);
829
+ this._cachedCeiling = ceiling;
830
+ this._cachedFrontier = frontier;
831
+ this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
832
+ if (collectReceipts) {
833
+ return { state, receipts: [] };
834
+ }
835
+ return state;
836
+ }
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
+
864
+ const allPatches = [];
865
+ for (const writerId of writerIds) {
866
+ const writerPatches = await this._loadWriterPatches(writerId);
867
+ for (const entry of writerPatches) {
868
+ if (entry.patch.lamport <= ceiling) {
869
+ allPatches.push(entry);
870
+ }
871
+ }
872
+ }
873
+
874
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
875
+ let state;
876
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
877
+ let receipts;
878
+
879
+ if (allPatches.length === 0) {
880
+ state = createEmptyStateV5();
881
+ if (collectReceipts) {
882
+ receipts = [];
883
+ }
884
+ } else if (collectReceipts) {
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
886
+ state = result.state;
887
+ receipts = result.receipts;
888
+ } else {
889
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
890
+ }
891
+
892
+ this._provenanceIndex = new ProvenanceIndex();
893
+ for (const { patch, sha } of allPatches) {
894
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
895
+ }
896
+ this._provenanceDegraded = false;
897
+
898
+ await this._setMaterializedState(state);
899
+ this._cachedCeiling = ceiling;
900
+ this._cachedFrontier = frontier;
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
+
911
+ // Skip auto-checkpoint and GC — this is an exploratory read
912
+ this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
913
+
914
+ if (collectReceipts) {
915
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
916
+ }
917
+ return state;
918
+ }
919
+
701
920
  /**
702
921
  * Joins (merges) another state into the current cached state.
703
922
  *
@@ -742,16 +961,16 @@ export default class WarpGraph {
742
961
  }
743
962
 
744
963
  // Capture pre-merge counts for receipt
745
- const beforeNodes = this._cachedState.nodeAlive.elements.size;
746
- const beforeEdges = this._cachedState.edgeAlive.elements.size;
964
+ const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
965
+ const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
747
966
  const beforeFrontierSize = this._cachedState.observedFrontier.size;
748
967
 
749
968
  // Perform the join
750
969
  const mergedState = joinStates(this._cachedState, otherState);
751
970
 
752
971
  // Calculate receipt
753
- const afterNodes = mergedState.nodeAlive.elements.size;
754
- const afterEdges = mergedState.edgeAlive.elements.size;
972
+ const afterNodes = orsetElements(mergedState.nodeAlive).length;
973
+ const afterEdges = orsetElements(mergedState.edgeAlive).length;
755
974
  const afterFrontierSize = mergedState.observedFrontier.size;
756
975
 
757
976
  // Count property changes (keys that existed in both but have different values)
@@ -813,7 +1032,7 @@ export default class WarpGraph {
813
1032
  * @example
814
1033
  * // Time-travel to a previous checkpoint
815
1034
  * const oldState = await graph.materializeAt('abc123');
816
- * console.log('Nodes at checkpoint:', [...oldState.nodeAlive.elements.keys()]);
1035
+ * console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
817
1036
  */
818
1037
  async materializeAt(checkpointSha) {
819
1038
  // 1. Discover current writers to build target frontier
@@ -830,7 +1049,7 @@ export default class WarpGraph {
830
1049
  }
831
1050
 
832
1051
  // 3. Create a patch loader function for incremental materialization
833
- const patchLoader = async (writerId, fromSha, toSha) => {
1052
+ const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
834
1053
  // Load patches from fromSha (exclusive) to toSha (inclusive)
835
1054
  // Walk from toSha back to fromSha
836
1055
  const patches = [];
@@ -863,7 +1082,7 @@ export default class WarpGraph {
863
1082
 
864
1083
  // 4. Call materializeIncremental with the checkpoint and target frontier
865
1084
  const state = await materializeIncremental({
866
- persistence: this._persistence,
1085
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
867
1086
  graphName: this._graphName,
868
1087
  checkpointSha,
869
1088
  targetFrontier,
@@ -907,23 +1126,24 @@ export default class WarpGraph {
907
1126
  // 3. Materialize current state (reuse cached if fresh, guard against recursion)
908
1127
  const prevCheckpointing = this._checkpointing;
909
1128
  this._checkpointing = true;
1129
+ /** @type {import('./services/JoinReducer.js').WarpStateV5} */
910
1130
  let state;
911
1131
  try {
912
- state = (this._cachedState && !this._stateDirty)
1132
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
913
1133
  ? this._cachedState
914
- : await this.materialize();
1134
+ : await this.materialize());
915
1135
  } finally {
916
1136
  this._checkpointing = prevCheckpointing;
917
1137
  }
918
1138
 
919
1139
  // 4. Call CheckpointService.create() with provenance index if available
920
1140
  const checkpointSha = await createCheckpointCommit({
921
- persistence: this._persistence,
1141
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
922
1142
  graphName: this._graphName,
923
1143
  state,
924
1144
  frontier,
925
1145
  parents,
926
- provenanceIndex: this._provenanceIndex,
1146
+ provenanceIndex: this._provenanceIndex || undefined,
927
1147
  crypto: this._crypto,
928
1148
  codec: this._codec,
929
1149
  });
@@ -937,7 +1157,7 @@ export default class WarpGraph {
937
1157
  // 6. Return checkpoint SHA
938
1158
  return checkpointSha;
939
1159
  } catch (err) {
940
- this._logTiming('createCheckpoint', t0, { error: err });
1160
+ this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
941
1161
  throw err;
942
1162
  }
943
1163
  }
@@ -1010,6 +1230,81 @@ export default class WarpGraph {
1010
1230
  return writerIds.sort();
1011
1231
  }
1012
1232
 
1233
+ /**
1234
+ * Discovers all distinct Lamport ticks across all writers.
1235
+ *
1236
+ * Walks each writer's patch chain from tip to root, reading commit
1237
+ * messages (no CBOR blob deserialization) to extract Lamport timestamps.
1238
+ * Stops when a non-patch commit (e.g. checkpoint) is encountered.
1239
+ * Logs a warning for any non-monotonic lamport sequence within a single
1240
+ * writer's chain.
1241
+ *
1242
+ * @returns {Promise<{
1243
+ * ticks: number[],
1244
+ * maxTick: number,
1245
+ * perWriter: Map<string, {ticks: number[], tipSha: string|null}>
1246
+ * }>} `ticks` is the sorted (ascending) deduplicated union of all
1247
+ * Lamport values; `maxTick` is the largest value (0 if none);
1248
+ * `perWriter` maps each writer ID to its ticks in ascending order
1249
+ * and its current tip SHA (or `null` if the writer ref is missing)
1250
+ * @throws {Error} If reading refs or commit metadata fails
1251
+ */
1252
+ async discoverTicks() {
1253
+ const writerIds = await this.discoverWriters();
1254
+ /** @type {Set<number>} */
1255
+ const globalTickSet = new Set();
1256
+ const perWriter = new Map();
1257
+
1258
+ for (const writerId of writerIds) {
1259
+ const writerRef = buildWriterRef(this._graphName, writerId);
1260
+ const tipSha = await this._persistence.readRef(writerRef);
1261
+ const writerTicks = [];
1262
+ /** @type {Record<number, string>} */
1263
+ const tickShas = {};
1264
+
1265
+ if (tipSha) {
1266
+ let currentSha = tipSha;
1267
+ let lastLamport = Infinity;
1268
+
1269
+ while (currentSha) {
1270
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
1271
+ const kind = detectMessageKind(nodeInfo.message);
1272
+ if (kind !== 'patch') {
1273
+ break;
1274
+ }
1275
+
1276
+ const patchMeta = decodePatchMessage(nodeInfo.message);
1277
+ globalTickSet.add(patchMeta.lamport);
1278
+ writerTicks.push(patchMeta.lamport);
1279
+ tickShas[patchMeta.lamport] = currentSha;
1280
+
1281
+ // Check monotonic invariant (walking newest→oldest, lamport should decrease)
1282
+ if (patchMeta.lamport > lastLamport && this._logger) {
1283
+ this._logger.warn(`[warp] non-monotonic lamport for writer ${writerId}: ${patchMeta.lamport} > ${lastLamport}`);
1284
+ }
1285
+ lastLamport = patchMeta.lamport;
1286
+
1287
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
1288
+ currentSha = nodeInfo.parents[0];
1289
+ } else {
1290
+ break;
1291
+ }
1292
+ }
1293
+ }
1294
+
1295
+ perWriter.set(writerId, {
1296
+ ticks: writerTicks.reverse(),
1297
+ tipSha: tipSha || null,
1298
+ tickShas,
1299
+ });
1300
+ }
1301
+
1302
+ const ticks = [...globalTickSet].sort((a, b) => a - b);
1303
+ const maxTick = ticks.length > 0 ? ticks[ticks.length - 1] : 0;
1304
+
1305
+ return { ticks, maxTick, perWriter };
1306
+ }
1307
+
1013
1308
  // ============================================================================
1014
1309
  // Schema Migration Support
1015
1310
  // ============================================================================
@@ -1042,7 +1337,7 @@ export default class WarpGraph {
1042
1337
  /**
1043
1338
  * Loads the latest checkpoint for this graph.
1044
1339
  *
1045
- * @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
1046
1341
  * @private
1047
1342
  */
1048
1343
  async _loadLatestCheckpoint() {
@@ -1084,7 +1379,7 @@ export default class WarpGraph {
1084
1379
  if (kind === 'patch') {
1085
1380
  const patchMeta = decodePatchMessage(nodeInfo.message);
1086
1381
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
1087
- const patch = this._codec.decode(patchBuffer);
1382
+ const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
1088
1383
 
1089
1384
  // If any patch has schema:1, we have v1 history
1090
1385
  if (patch.schema === 1 || patch.schema === undefined) {
@@ -1099,8 +1394,8 @@ export default class WarpGraph {
1099
1394
  /**
1100
1395
  * Loads patches since a checkpoint for incremental materialization.
1101
1396
  *
1102
- * @param {{state: Object, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
1103
- * @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
1104
1399
  * @private
1105
1400
  */
1106
1401
  async _loadPatchesSince(checkpoint) {
@@ -1182,7 +1477,7 @@ export default class WarpGraph {
1182
1477
  *
1183
1478
  * @param {string} writerId - The writer ID for this patch
1184
1479
  * @param {string} incomingSha - The incoming patch commit SHA
1185
- * @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
1186
1481
  * @returns {Promise<void>}
1187
1482
  * @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
1188
1483
  * @throws {Error} If patch does not extend checkpoint head (writer fork detected)
@@ -1230,18 +1525,19 @@ export default class WarpGraph {
1230
1525
  _maybeRunGC(state) {
1231
1526
  try {
1232
1527
  const metrics = collectGCMetrics(state);
1528
+ /** @type {import('./services/GCPolicy.js').GCInputMetrics} */
1233
1529
  const inputMetrics = {
1234
1530
  ...metrics,
1235
1531
  patchesSinceCompaction: this._patchesSinceGC,
1236
1532
  timeSinceCompaction: Date.now() - this._lastGCTime,
1237
1533
  };
1238
- const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
1534
+ const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1239
1535
 
1240
1536
  if (!shouldRun) {
1241
1537
  return;
1242
1538
  }
1243
1539
 
1244
- if (this._gcPolicy.enabled) {
1540
+ if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
1245
1541
  const appliedVV = computeAppliedVV(state);
1246
1542
  const result = executeGC(state, appliedVV);
1247
1543
  this._lastGCTime = Date.now();
@@ -1280,11 +1576,15 @@ export default class WarpGraph {
1280
1576
  return { ran: false, result: null, reasons: [] };
1281
1577
  }
1282
1578
 
1283
- const metrics = collectGCMetrics(this._cachedState);
1284
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1285
- 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
+ };
1286
1586
 
1287
- const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
1587
+ const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1288
1588
 
1289
1589
  if (!shouldRun) {
1290
1590
  return { ran: false, result: null, reasons: [] };
@@ -1331,7 +1631,7 @@ export default class WarpGraph {
1331
1631
 
1332
1632
  return result;
1333
1633
  } catch (err) {
1334
- this._logTiming('runGC', t0, { error: err });
1634
+ this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
1335
1635
  throw err;
1336
1636
  }
1337
1637
  }
@@ -1353,10 +1653,15 @@ export default class WarpGraph {
1353
1653
  return null;
1354
1654
  }
1355
1655
 
1356
- const metrics = collectGCMetrics(this._cachedState);
1357
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1358
- metrics.lastCompactionTime = this._lastGCTime;
1359
- 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
+ };
1360
1665
  }
1361
1666
 
1362
1667
  /**
@@ -1439,6 +1744,7 @@ export default class WarpGraph {
1439
1744
  */
1440
1745
  async status() {
1441
1746
  // Determine cachedState
1747
+ /** @type {'fresh' | 'stale' | 'none'} */
1442
1748
  let cachedState;
1443
1749
  if (this._cachedState === null) {
1444
1750
  cachedState = 'none';
@@ -1488,7 +1794,7 @@ export default class WarpGraph {
1488
1794
  * One handler's error does not prevent other handlers from being called.
1489
1795
  *
1490
1796
  * @param {Object} options - Subscription options
1491
- * @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
1492
1798
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1493
1799
  * @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
1494
1800
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1531,7 +1837,7 @@ export default class WarpGraph {
1531
1837
  } catch (err) {
1532
1838
  if (onError) {
1533
1839
  try {
1534
- onError(err);
1840
+ onError(/** @type {Error} */ (err));
1535
1841
  } catch {
1536
1842
  // onError itself threw — swallow to prevent cascade
1537
1843
  }
@@ -1568,7 +1874,7 @@ export default class WarpGraph {
1568
1874
  *
1569
1875
  * @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
1570
1876
  * @param {Object} options - Watch options
1571
- * @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
1572
1878
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1573
1879
  * @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
1574
1880
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1609,31 +1915,32 @@ export default class WarpGraph {
1609
1915
 
1610
1916
  // Pattern matching: same logic as QueryBuilder.match()
1611
1917
  // Pre-compile pattern matcher once for performance
1918
+ /** @type {(nodeId: string) => boolean} */
1612
1919
  let matchesPattern;
1613
1920
  if (pattern === '*') {
1614
1921
  matchesPattern = () => true;
1615
1922
  } else if (pattern.includes('*')) {
1616
1923
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
1617
1924
  const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
1618
- matchesPattern = (nodeId) => regex.test(nodeId);
1925
+ matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
1619
1926
  } else {
1620
- matchesPattern = (nodeId) => nodeId === pattern;
1927
+ matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
1621
1928
  }
1622
1929
 
1623
1930
  // Filtered onChange that only passes matching changes
1624
- const filteredOnChange = (diff) => {
1931
+ const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
1625
1932
  const filteredDiff = {
1626
1933
  nodes: {
1627
1934
  added: diff.nodes.added.filter(matchesPattern),
1628
1935
  removed: diff.nodes.removed.filter(matchesPattern),
1629
1936
  },
1630
1937
  edges: {
1631
- added: diff.edges.added.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
1632
- 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)),
1633
1940
  },
1634
1941
  props: {
1635
- set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
1636
- 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)),
1637
1944
  },
1638
1945
  };
1639
1946
 
@@ -1655,6 +1962,7 @@ export default class WarpGraph {
1655
1962
  const subscription = this.subscribe({ onChange: filteredOnChange, onError });
1656
1963
 
1657
1964
  // Polling: periodically check frontier and auto-materialize if changed
1965
+ /** @type {ReturnType<typeof setInterval>|null} */
1658
1966
  let pollIntervalId = null;
1659
1967
  let pollInFlight = false;
1660
1968
  if (poll) {
@@ -1736,7 +2044,7 @@ export default class WarpGraph {
1736
2044
  * Creates a sync request to send to a remote peer.
1737
2045
  * The request contains the local frontier for comparison.
1738
2046
  *
1739
- * @returns {Promise<{type: 'sync-request', frontier: Map<string, string>}>} The sync request
2047
+ * @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
1740
2048
  * @throws {Error} If listing refs fails
1741
2049
  *
1742
2050
  * @example
@@ -1751,8 +2059,8 @@ export default class WarpGraph {
1751
2059
  /**
1752
2060
  * Processes an incoming sync request and returns patches the requester needs.
1753
2061
  *
1754
- * @param {{type: 'sync-request', frontier: Map<string, string>}} request - The incoming sync request
1755
- * @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
1756
2064
  * @throws {Error} If listing refs or reading patches fails
1757
2065
  *
1758
2066
  * @example
@@ -1765,7 +2073,7 @@ export default class WarpGraph {
1765
2073
  return await processSyncRequest(
1766
2074
  request,
1767
2075
  localFrontier,
1768
- this._persistence,
2076
+ /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1769
2077
  this._graphName,
1770
2078
  { codec: this._codec }
1771
2079
  );
@@ -1777,8 +2085,8 @@ export default class WarpGraph {
1777
2085
  *
1778
2086
  * **Requires a cached state.**
1779
2087
  *
1780
- * @param {{type: 'sync-response', frontier: Map, patches: Map}} response - The sync response
1781
- * @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
1782
2090
  * @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
1783
2091
  *
1784
2092
  * @example
@@ -1793,8 +2101,8 @@ export default class WarpGraph {
1793
2101
  });
1794
2102
  }
1795
2103
 
1796
- const currentFrontier = this._cachedState.observedFrontier;
1797
- 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));
1798
2106
 
1799
2107
  // Update cached state
1800
2108
  this._cachedState = result.state;
@@ -1865,7 +2173,7 @@ export default class WarpGraph {
1865
2173
  let targetUrl = null;
1866
2174
  if (!isDirectPeer) {
1867
2175
  try {
1868
- 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));
1869
2177
  } catch {
1870
2178
  throw new SyncError('Invalid remote URL', {
1871
2179
  code: 'E_SYNC_REMOTE_URL',
@@ -1890,13 +2198,13 @@ export default class WarpGraph {
1890
2198
  }
1891
2199
 
1892
2200
  let attempt = 0;
1893
- const emit = (type, payload = {}) => {
2201
+ const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
1894
2202
  if (typeof onStatus === 'function') {
1895
- onStatus({ type, attempt, ...payload });
2203
+ onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
1896
2204
  }
1897
2205
  };
1898
2206
 
1899
- const shouldRetry = (err) => {
2207
+ const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
1900
2208
  if (isDirectPeer) { return false; }
1901
2209
  if (err instanceof SyncError) {
1902
2210
  return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
@@ -1926,7 +2234,7 @@ export default class WarpGraph {
1926
2234
  const combinedSignal = signal
1927
2235
  ? AbortSignal.any([timeoutSignal, signal])
1928
2236
  : timeoutSignal;
1929
- return fetch(targetUrl.toString(), {
2237
+ return fetch(/** @type {URL} */ (targetUrl).toString(), {
1930
2238
  method: 'POST',
1931
2239
  headers: {
1932
2240
  'content-type': 'application/json',
@@ -1937,7 +2245,7 @@ export default class WarpGraph {
1937
2245
  });
1938
2246
  });
1939
2247
  } catch (err) {
1940
- if (err?.name === 'AbortError') {
2248
+ if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
1941
2249
  throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
1942
2250
  }
1943
2251
  if (err instanceof TimeoutError) {
@@ -1948,7 +2256,7 @@ export default class WarpGraph {
1948
2256
  }
1949
2257
  throw new SyncError('Network error', {
1950
2258
  code: 'E_SYNC_NETWORK',
1951
- context: { message: err?.message },
2259
+ context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
1952
2260
  });
1953
2261
  }
1954
2262
 
@@ -2009,9 +2317,9 @@ export default class WarpGraph {
2009
2317
  jitter: 'decorrelated',
2010
2318
  signal,
2011
2319
  shouldRetry,
2012
- onRetry: (error, attemptNumber, delayMs) => {
2320
+ onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
2013
2321
  if (typeof onStatus === 'function') {
2014
- onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
2322
+ onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
2015
2323
  }
2016
2324
  },
2017
2325
  });
@@ -2020,12 +2328,12 @@ export default class WarpGraph {
2020
2328
 
2021
2329
  if (materializeAfterSync) {
2022
2330
  if (!this._cachedState) { await this.materialize(); }
2023
- return { ...syncResult, state: this._cachedState };
2331
+ return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
2024
2332
  }
2025
2333
  return syncResult;
2026
2334
  } catch (err) {
2027
- this._logTiming('syncWith', t0, { error: err });
2028
- 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
2029
2337
  const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
2030
2338
  if (typeof onStatus === 'function') {
2031
2339
  onStatus({ type: 'failed', attempt, error: abortedError });
@@ -2033,14 +2341,14 @@ export default class WarpGraph {
2033
2341
  throw abortedError;
2034
2342
  }
2035
2343
  if (err instanceof RetryExhaustedError) {
2036
- const cause = err.cause || err;
2344
+ const cause = /** @type {Error} */ (err.cause || err);
2037
2345
  if (typeof onStatus === 'function') {
2038
2346
  onStatus({ type: 'failed', attempt: err.attempts, error: cause });
2039
2347
  }
2040
2348
  throw cause;
2041
2349
  }
2042
2350
  if (typeof onStatus === 'function') {
2043
- onStatus({ type: 'failed', attempt, error: err });
2351
+ onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
2044
2352
  }
2045
2353
  throw err;
2046
2354
  }
@@ -2059,7 +2367,7 @@ export default class WarpGraph {
2059
2367
  * @throws {Error} If port is not a number
2060
2368
  * @throws {Error} If httpPort adapter is not provided
2061
2369
  */
2062
- 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
2063
2371
  if (typeof port !== 'number') {
2064
2372
  throw new Error('serve() requires a numeric port');
2065
2373
  }
@@ -2104,8 +2412,8 @@ export default class WarpGraph {
2104
2412
  */
2105
2413
  async writer(writerId) {
2106
2414
  // Build config adapters for resolveWriterId
2107
- const configGet = async (key) => await this._persistence.configGet(key);
2108
- 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);
2109
2417
 
2110
2418
  // Resolve the writer ID
2111
2419
  const resolvedWriterId = await resolveWriterId({
@@ -2116,13 +2424,13 @@ export default class WarpGraph {
2116
2424
  });
2117
2425
 
2118
2426
  return new Writer({
2119
- persistence: this._persistence,
2427
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2120
2428
  graphName: this._graphName,
2121
2429
  writerId: resolvedWriterId,
2122
2430
  versionVector: this._versionVector,
2123
- getCurrentState: () => this._cachedState,
2431
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2124
2432
  onDeleteWithData: this._onDeleteWithData,
2125
- onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
2433
+ onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
2126
2434
  codec: this._codec,
2127
2435
  });
2128
2436
  }
@@ -2170,13 +2478,13 @@ export default class WarpGraph {
2170
2478
  }
2171
2479
 
2172
2480
  return new Writer({
2173
- persistence: this._persistence,
2481
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2174
2482
  graphName: this._graphName,
2175
2483
  writerId: freshWriterId,
2176
2484
  versionVector: this._versionVector,
2177
- getCurrentState: () => this._cachedState,
2485
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2178
2486
  onDeleteWithData: this._onDeleteWithData,
2179
- onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
2487
+ onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
2180
2488
  codec: this._codec,
2181
2489
  });
2182
2490
  }
@@ -2331,7 +2639,8 @@ export default class WarpGraph {
2331
2639
  */
2332
2640
  async hasNode(nodeId) {
2333
2641
  await this._ensureFreshState();
2334
- return orsetContains(this._cachedState.nodeAlive, nodeId);
2642
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2643
+ return orsetContains(s.nodeAlive, nodeId);
2335
2644
  }
2336
2645
 
2337
2646
  /**
@@ -2356,15 +2665,16 @@ export default class WarpGraph {
2356
2665
  */
2357
2666
  async getNodeProps(nodeId) {
2358
2667
  await this._ensureFreshState();
2668
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2359
2669
 
2360
2670
  // Check if node exists
2361
- if (!orsetContains(this._cachedState.nodeAlive, nodeId)) {
2671
+ if (!orsetContains(s.nodeAlive, nodeId)) {
2362
2672
  return null;
2363
2673
  }
2364
2674
 
2365
2675
  // Collect all properties for this node
2366
2676
  const props = new Map();
2367
- for (const [propKey, register] of this._cachedState.prop) {
2677
+ for (const [propKey, register] of s.prop) {
2368
2678
  const decoded = decodePropKey(propKey);
2369
2679
  if (decoded.nodeId === nodeId) {
2370
2680
  props.set(decoded.propKey, register.value);
@@ -2398,26 +2708,28 @@ export default class WarpGraph {
2398
2708
  */
2399
2709
  async getEdgeProps(from, to, label) {
2400
2710
  await this._ensureFreshState();
2711
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2401
2712
 
2402
2713
  // Check if edge exists
2403
2714
  const edgeKey = encodeEdgeKey(from, to, label);
2404
- if (!orsetContains(this._cachedState.edgeAlive, edgeKey)) {
2715
+ if (!orsetContains(s.edgeAlive, edgeKey)) {
2405
2716
  return null;
2406
2717
  }
2407
2718
 
2408
2719
  // Check node liveness for both endpoints
2409
- if (!orsetContains(this._cachedState.nodeAlive, from) ||
2410
- !orsetContains(this._cachedState.nodeAlive, to)) {
2720
+ if (!orsetContains(s.nodeAlive, from) ||
2721
+ !orsetContains(s.nodeAlive, to)) {
2411
2722
  return null;
2412
2723
  }
2413
2724
 
2414
2725
  // Determine the birth EventId for clean-slate filtering
2415
- const birthEvent = this._cachedState.edgeBirthEvent?.get(edgeKey);
2726
+ const birthEvent = s.edgeBirthEvent?.get(edgeKey);
2416
2727
 
2417
2728
  // Collect all properties for this edge, filtering out stale props
2418
2729
  // (props set before the edge's most recent re-add)
2730
+ /** @type {Record<string, any>} */
2419
2731
  const props = {};
2420
- for (const [propKey, register] of this._cachedState.prop) {
2732
+ for (const [propKey, register] of s.prop) {
2421
2733
  if (!isEdgePropKey(propKey)) {
2422
2734
  continue;
2423
2735
  }
@@ -2459,11 +2771,13 @@ export default class WarpGraph {
2459
2771
  */
2460
2772
  async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
2461
2773
  await this._ensureFreshState();
2774
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2462
2775
 
2776
+ /** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
2463
2777
  const neighbors = [];
2464
2778
 
2465
2779
  // Iterate over all visible edges
2466
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2780
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2467
2781
  const { from, to, label } = decodeEdgeKey(edgeKey);
2468
2782
 
2469
2783
  // Filter by label if specified
@@ -2474,15 +2788,15 @@ export default class WarpGraph {
2474
2788
  // Check edge direction and collect neighbors
2475
2789
  if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
2476
2790
  // Ensure target node is visible
2477
- if (orsetContains(this._cachedState.nodeAlive, to)) {
2478
- neighbors.push({ nodeId: to, label, direction: 'outgoing' });
2791
+ if (orsetContains(s.nodeAlive, to)) {
2792
+ neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
2479
2793
  }
2480
2794
  }
2481
2795
 
2482
2796
  if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
2483
2797
  // Ensure source node is visible
2484
- if (orsetContains(this._cachedState.nodeAlive, from)) {
2485
- neighbors.push({ nodeId: from, label, direction: 'incoming' });
2798
+ if (orsetContains(s.nodeAlive, from)) {
2799
+ neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
2486
2800
  }
2487
2801
  }
2488
2802
  }
@@ -2507,7 +2821,8 @@ export default class WarpGraph {
2507
2821
  */
2508
2822
  async getNodes() {
2509
2823
  await this._ensureFreshState();
2510
- return [...orsetElements(this._cachedState.nodeAlive)];
2824
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2825
+ return [...orsetElements(s.nodeAlive)];
2511
2826
  }
2512
2827
 
2513
2828
  /**
@@ -2530,12 +2845,13 @@ export default class WarpGraph {
2530
2845
  */
2531
2846
  async getEdges() {
2532
2847
  await this._ensureFreshState();
2848
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2533
2849
 
2534
2850
  // Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
2535
2851
  // Filters out stale props using full EventId ordering via compareEventIds
2536
2852
  // against the edge's birth EventId (clean-slate semantics on re-add)
2537
2853
  const edgePropsByKey = new Map();
2538
- for (const [propKey, register] of this._cachedState.prop) {
2854
+ for (const [propKey, register] of s.prop) {
2539
2855
  if (!isEdgePropKey(propKey)) {
2540
2856
  continue;
2541
2857
  }
@@ -2543,7 +2859,7 @@ export default class WarpGraph {
2543
2859
  const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
2544
2860
 
2545
2861
  // Clean-slate filter: skip props from before the edge's current incarnation
2546
- const birthEvent = this._cachedState.edgeBirthEvent?.get(ek);
2862
+ const birthEvent = s.edgeBirthEvent?.get(ek);
2547
2863
  if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
2548
2864
  continue;
2549
2865
  }
@@ -2557,11 +2873,11 @@ export default class WarpGraph {
2557
2873
  }
2558
2874
 
2559
2875
  const edges = [];
2560
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2876
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2561
2877
  const { from, to, label } = decodeEdgeKey(edgeKey);
2562
2878
  // Only include edges where both endpoints are visible
2563
- if (orsetContains(this._cachedState.nodeAlive, from) &&
2564
- orsetContains(this._cachedState.nodeAlive, to)) {
2879
+ if (orsetContains(s.nodeAlive, from) &&
2880
+ orsetContains(s.nodeAlive, to)) {
2565
2881
  const props = edgePropsByKey.get(edgeKey) || {};
2566
2882
  edges.push({ from, to, label, props });
2567
2883
  }
@@ -2580,7 +2896,8 @@ export default class WarpGraph {
2580
2896
  */
2581
2897
  async getPropertyCount() {
2582
2898
  await this._ensureFreshState();
2583
- return this._cachedState.prop.size;
2899
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2900
+ return s.prop.size;
2584
2901
  }
2585
2902
 
2586
2903
  // ============================================================================
@@ -2699,9 +3016,9 @@ export default class WarpGraph {
2699
3016
  try {
2700
3017
  validateGraphName(resolvedForkName);
2701
3018
  } catch (err) {
2702
- throw new ForkError(`Invalid fork name: ${err.message}`, {
3019
+ throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
2703
3020
  code: 'E_FORK_NAME_INVALID',
2704
- context: { forkName: resolvedForkName, originalError: err.message },
3021
+ context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
2705
3022
  });
2706
3023
  }
2707
3024
 
@@ -2720,9 +3037,9 @@ export default class WarpGraph {
2720
3037
  try {
2721
3038
  validateWriterId(resolvedForkWriterId);
2722
3039
  } catch (err) {
2723
- throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
3040
+ throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
2724
3041
  code: 'E_FORK_WRITER_ID_INVALID',
2725
- context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
3042
+ context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
2726
3043
  });
2727
3044
  }
2728
3045
 
@@ -2737,10 +3054,10 @@ export default class WarpGraph {
2737
3054
  writerId: resolvedForkWriterId,
2738
3055
  gcPolicy: this._gcPolicy,
2739
3056
  adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
2740
- checkpointPolicy: this._checkpointPolicy,
3057
+ checkpointPolicy: this._checkpointPolicy || undefined,
2741
3058
  autoMaterialize: this._autoMaterialize,
2742
3059
  onDeleteWithData: this._onDeleteWithData,
2743
- logger: this._logger,
3060
+ logger: this._logger || undefined,
2744
3061
  clock: this._clock,
2745
3062
  crypto: this._crypto,
2746
3063
  codec: this._codec,
@@ -2752,7 +3069,7 @@ export default class WarpGraph {
2752
3069
 
2753
3070
  return forkGraph;
2754
3071
  } catch (err) {
2755
- this._logTiming('fork', t0, { error: err });
3072
+ this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
2756
3073
  throw err;
2757
3074
  }
2758
3075
  }
@@ -2806,13 +3123,13 @@ export default class WarpGraph {
2806
3123
  const t0 = this._clock.now();
2807
3124
 
2808
3125
  try {
2809
- const wormhole = await createWormholeImpl({
3126
+ const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
2810
3127
  persistence: this._persistence,
2811
3128
  graphName: this._graphName,
2812
3129
  fromSha,
2813
3130
  toSha,
2814
3131
  codec: this._codec,
2815
- });
3132
+ }));
2816
3133
 
2817
3134
  this._logTiming('createWormhole', t0, {
2818
3135
  metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
@@ -2820,7 +3137,7 @@ export default class WarpGraph {
2820
3137
 
2821
3138
  return wormhole;
2822
3139
  } catch (err) {
2823
- this._logTiming('createWormhole', t0, { error: err });
3140
+ this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
2824
3141
  throw err;
2825
3142
  }
2826
3143
  }
@@ -2854,6 +3171,12 @@ export default class WarpGraph {
2854
3171
  async patchesFor(entityId) {
2855
3172
  await this._ensureFreshState();
2856
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
+
2857
3180
  if (!this._provenanceIndex) {
2858
3181
  throw new QueryError('No provenance index. Call materialize() first.', {
2859
3182
  code: 'E_NO_STATE',
@@ -2915,6 +3238,12 @@ export default class WarpGraph {
2915
3238
  // Ensure fresh state before accessing provenance index
2916
3239
  await this._ensureFreshState();
2917
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
+
2918
3247
  if (!this._provenanceIndex) {
2919
3248
  throw new QueryError('No provenance index. Call materialize() first.', {
2920
3249
  code: 'E_NO_STATE',
@@ -2949,7 +3278,7 @@ export default class WarpGraph {
2949
3278
  this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
2950
3279
 
2951
3280
  if (collectReceipts) {
2952
- 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 }));
2953
3282
  return {
2954
3283
  state: result.state,
2955
3284
  patchCount: sortedPatches.length,
@@ -2963,7 +3292,7 @@ export default class WarpGraph {
2963
3292
  patchCount: sortedPatches.length,
2964
3293
  };
2965
3294
  } catch (err) {
2966
- this._logTiming('materializeSlice', t0, { error: err });
3295
+ this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
2967
3296
  throw err;
2968
3297
  }
2969
3298
  }
@@ -3000,7 +3329,7 @@ export default class WarpGraph {
3000
3329
  visited.add(entityId);
3001
3330
 
3002
3331
  // Get all patches that affected this entity
3003
- const patchShas = this._provenanceIndex.patchesFor(entityId);
3332
+ const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
3004
3333
 
3005
3334
  for (const sha of patchShas) {
3006
3335
  if (cone.has(sha)) {
@@ -3012,8 +3341,9 @@ export default class WarpGraph {
3012
3341
  cone.set(sha, patch);
3013
3342
 
3014
3343
  // Add read dependencies to the queue
3015
- if (patch && patch.reads) {
3016
- 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) {
3017
3347
  if (!visited.has(readEntity)) {
3018
3348
  queue.push(readEntity);
3019
3349
  }
@@ -3025,6 +3355,23 @@ export default class WarpGraph {
3025
3355
  return cone;
3026
3356
  }
3027
3357
 
3358
+ /**
3359
+ * Loads a single patch by its SHA.
3360
+ *
3361
+ * @param {string} sha - The patch commit SHA
3362
+ * @returns {Promise<Object>} The decoded patch object
3363
+ * @throws {Error} If the commit is not a patch or loading fails
3364
+ *
3365
+ * @public
3366
+ * @remarks
3367
+ * Thin wrapper around the internal `_loadPatchBySha` helper. Exposed for
3368
+ * CLI/debug tooling (e.g. seek tick receipts) that needs to inspect patch
3369
+ * operations without re-materializing intermediate states.
3370
+ */
3371
+ async loadPatchBySha(sha) {
3372
+ return await this._loadPatchBySha(sha);
3373
+ }
3374
+
3028
3375
  /**
3029
3376
  * Loads a single patch by its SHA.
3030
3377
  *
@@ -3043,7 +3390,7 @@ export default class WarpGraph {
3043
3390
 
3044
3391
  const patchMeta = decodePatchMessage(nodeInfo.message);
3045
3392
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
3046
- return this._codec.decode(patchBuffer);
3393
+ return /** @type {Object} */ (this._codec.decode(patchBuffer));
3047
3394
  }
3048
3395
 
3049
3396
  /**
@@ -3071,8 +3418,8 @@ export default class WarpGraph {
3071
3418
  * Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
3072
3419
  * This ensures deterministic ordering regardless of discovery order.
3073
3420
  *
3074
- * @param {Array<{patch: Object, sha: string}>} patches - Unsorted patch entries
3075
- * @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
3076
3423
  * @private
3077
3424
  */
3078
3425
  _sortPatchesCausally(patches) {