@git-stunts/git-warp 10.3.2 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +6 -3
  2. package/SECURITY.md +89 -1
  3. package/bin/warp-graph.js +574 -208
  4. package/index.d.ts +55 -0
  5. package/index.js +4 -0
  6. package/package.json +8 -4
  7. package/src/domain/WarpGraph.js +334 -161
  8. package/src/domain/crdt/LWW.js +1 -1
  9. package/src/domain/crdt/ORSet.js +10 -6
  10. package/src/domain/crdt/VersionVector.js +5 -1
  11. package/src/domain/errors/EmptyMessageError.js +2 -4
  12. package/src/domain/errors/ForkError.js +4 -0
  13. package/src/domain/errors/IndexError.js +4 -0
  14. package/src/domain/errors/OperationAbortedError.js +4 -0
  15. package/src/domain/errors/QueryError.js +4 -0
  16. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  17. package/src/domain/errors/ShardCorruptionError.js +2 -6
  18. package/src/domain/errors/ShardLoadError.js +2 -6
  19. package/src/domain/errors/ShardValidationError.js +2 -7
  20. package/src/domain/errors/StorageError.js +2 -6
  21. package/src/domain/errors/SyncError.js +4 -0
  22. package/src/domain/errors/TraversalError.js +4 -0
  23. package/src/domain/errors/WarpError.js +2 -4
  24. package/src/domain/errors/WormholeError.js +4 -0
  25. package/src/domain/services/AnchorMessageCodec.js +1 -4
  26. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  27. package/src/domain/services/BitmapIndexReader.js +27 -21
  28. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  29. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  30. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  31. package/src/domain/services/CheckpointService.js +18 -18
  32. package/src/domain/services/CommitDagTraversalService.js +13 -1
  33. package/src/domain/services/DagPathFinding.js +40 -18
  34. package/src/domain/services/DagTopology.js +7 -6
  35. package/src/domain/services/DagTraversal.js +5 -3
  36. package/src/domain/services/Frontier.js +7 -6
  37. package/src/domain/services/HealthCheckService.js +15 -14
  38. package/src/domain/services/HookInstaller.js +64 -13
  39. package/src/domain/services/HttpSyncServer.js +88 -19
  40. package/src/domain/services/IndexRebuildService.js +12 -12
  41. package/src/domain/services/IndexStalenessChecker.js +13 -6
  42. package/src/domain/services/JoinReducer.js +28 -27
  43. package/src/domain/services/LogicalTraversal.js +7 -6
  44. package/src/domain/services/MessageCodecInternal.js +2 -0
  45. package/src/domain/services/ObserverView.js +6 -6
  46. package/src/domain/services/PatchBuilderV2.js +9 -9
  47. package/src/domain/services/PatchMessageCodec.js +1 -7
  48. package/src/domain/services/ProvenanceIndex.js +6 -8
  49. package/src/domain/services/ProvenancePayload.js +1 -2
  50. package/src/domain/services/QueryBuilder.js +29 -23
  51. package/src/domain/services/StateDiff.js +7 -7
  52. package/src/domain/services/StateSerializerV5.js +8 -6
  53. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  54. package/src/domain/services/SyncAuthService.js +396 -0
  55. package/src/domain/services/SyncProtocol.js +23 -26
  56. package/src/domain/services/TemporalQuery.js +4 -3
  57. package/src/domain/services/TranslationCost.js +4 -4
  58. package/src/domain/services/WormholeService.js +19 -15
  59. package/src/domain/types/TickReceipt.js +10 -6
  60. package/src/domain/types/WarpTypesV2.js +2 -3
  61. package/src/domain/utils/CachedValue.js +1 -1
  62. package/src/domain/utils/LRUCache.js +3 -3
  63. package/src/domain/utils/MinHeap.js +2 -2
  64. package/src/domain/utils/RefLayout.js +19 -0
  65. package/src/domain/utils/WriterId.js +2 -2
  66. package/src/domain/utils/defaultCodec.js +9 -2
  67. package/src/domain/utils/defaultCrypto.js +36 -0
  68. package/src/domain/utils/roaring.js +5 -5
  69. package/src/domain/utils/seekCacheKey.js +32 -0
  70. package/src/domain/warp/PatchSession.js +3 -3
  71. package/src/domain/warp/Writer.js +2 -2
  72. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  73. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  74. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  75. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  76. package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
  77. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
  78. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  79. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  80. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  81. package/src/infrastructure/adapters/adapterValidation.js +90 -0
  82. package/src/infrastructure/codecs/CborCodec.js +16 -8
  83. package/src/ports/BlobPort.js +2 -2
  84. package/src/ports/CodecPort.js +2 -2
  85. package/src/ports/CommitPort.js +8 -21
  86. package/src/ports/ConfigPort.js +3 -3
  87. package/src/ports/CryptoPort.js +7 -7
  88. package/src/ports/GraphPersistencePort.js +12 -14
  89. package/src/ports/HttpServerPort.js +1 -5
  90. package/src/ports/IndexStoragePort.js +1 -0
  91. package/src/ports/LoggerPort.js +9 -9
  92. package/src/ports/RefPort.js +5 -5
  93. package/src/ports/SeekCachePort.js +73 -0
  94. package/src/ports/TreePort.js +3 -3
  95. package/src/visualization/layouts/converters.js +14 -7
  96. package/src/visualization/layouts/elkAdapter.js +17 -4
  97. package/src/visualization/layouts/elkLayout.js +23 -7
  98. package/src/visualization/layouts/index.js +3 -3
  99. package/src/visualization/renderers/ascii/check.js +30 -17
  100. package/src/visualization/renderers/ascii/graph.js +92 -1
  101. package/src/visualization/renderers/ascii/history.js +28 -26
  102. package/src/visualization/renderers/ascii/info.js +9 -7
  103. package/src/visualization/renderers/ascii/materialize.js +20 -16
  104. package/src/visualization/renderers/ascii/opSummary.js +15 -7
  105. package/src/visualization/renderers/ascii/path.js +1 -1
  106. package/src/visualization/renderers/ascii/seek.js +187 -23
  107. package/src/visualization/renderers/ascii/table.js +1 -1
  108. 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,14 @@ 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 { signSyncRequest, canonicalizePath } from './services/SyncAuthService.js';
53
+ import { buildSeekCacheKey } from './utils/seekCacheKey.js';
51
54
  import defaultClock from './utils/defaultClock.js';
52
55
 
56
+ /**
57
+ * @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
58
+ */
59
+
53
60
  const DEFAULT_SYNC_SERVER_MAX_BYTES = 4 * 1024 * 1024;
54
61
  const DEFAULT_SYNC_WITH_RETRIES = 3;
55
62
  const DEFAULT_SYNC_WITH_BASE_DELAY_MS = 250;
@@ -71,6 +78,35 @@ function normalizeSyncPath(path) {
71
78
  return path.startsWith('/') ? path : `/${path}`;
72
79
  }
73
80
 
81
+ /**
82
+ * Builds auth headers for an outgoing sync request if auth is configured.
83
+ *
84
+ * @param {Object} params
85
+ * @param {{ secret: string, keyId?: string }|undefined} params.auth
86
+ * @param {string} params.bodyStr - Serialized request body
87
+ * @param {URL} params.targetUrl
88
+ * @param {import('../ports/CryptoPort.js').default} params.crypto
89
+ * @returns {Promise<Record<string, string>>}
90
+ * @private
91
+ */
92
+ async function buildSyncAuthHeaders({ auth, bodyStr, targetUrl, crypto }) {
93
+ if (!auth || !auth.secret) {
94
+ return {};
95
+ }
96
+ const bodyBuf = new TextEncoder().encode(bodyStr);
97
+ return await signSyncRequest(
98
+ {
99
+ method: 'POST',
100
+ path: canonicalizePath(targetUrl.pathname + (targetUrl.search || '')),
101
+ contentType: 'application/json',
102
+ body: bodyBuf,
103
+ secret: auth.secret,
104
+ keyId: auth.keyId || 'default',
105
+ },
106
+ { crypto },
107
+ );
108
+ }
109
+
74
110
  const DEFAULT_ADJACENCY_CACHE_SIZE = 3;
75
111
 
76
112
  /**
@@ -99,10 +135,11 @@ export default class WarpGraph {
99
135
  * @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
100
136
  * @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
101
137
  * @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
138
+ * @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
102
139
  */
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;
140
+ constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec, seekCache }) {
141
+ /** @type {FullPersistence} */
142
+ this._persistence = /** @type {FullPersistence} */ (persistence);
106
143
 
107
144
  /** @type {string} */
108
145
  this._graphName = graphName;
@@ -146,7 +183,7 @@ export default class WarpGraph {
146
183
  /** @type {MaterializedGraph|null} */
147
184
  this._materializedGraph = null;
148
185
 
149
- /** @type {import('./utils/LRUCache.js').default|null} */
186
+ /** @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
187
  this._adjacencyCache = adjacencyCacheSize > 0 ? new LRUCache(adjacencyCacheSize) : null;
151
188
 
152
189
  /** @type {Map<string, string>|null} */
@@ -158,8 +195,8 @@ export default class WarpGraph {
158
195
  /** @type {import('../ports/ClockPort.js').default} */
159
196
  this._clock = clock || defaultClock;
160
197
 
161
- /** @type {import('../ports/CryptoPort.js').default|undefined} */
162
- this._crypto = crypto;
198
+ /** @type {import('../ports/CryptoPort.js').default} */
199
+ this._crypto = crypto || defaultCrypto;
163
200
 
164
201
  /** @type {import('../ports/CodecPort.js').default} */
165
202
  this._codec = codec || defaultCodec;
@@ -167,7 +204,7 @@ export default class WarpGraph {
167
204
  /** @type {'reject'|'cascade'|'warn'} */
168
205
  this._onDeleteWithData = onDeleteWithData;
169
206
 
170
- /** @type {Array<{onChange: Function, onError?: Function}>} */
207
+ /** @type {Array<{onChange: Function, onError?: Function, pendingReplay?: boolean}>} */
171
208
  this._subscribers = [];
172
209
 
173
210
  /** @type {import('./services/JoinReducer.js').WarpStateV5|null} */
@@ -187,6 +224,32 @@ export default class WarpGraph {
187
224
 
188
225
  /** @type {Map<string, string>|null} */
189
226
  this._cachedFrontier = null;
227
+
228
+ /** @type {import('../ports/SeekCachePort.js').default|null} */
229
+ this._seekCache = seekCache || null;
230
+
231
+ /** @type {boolean} */
232
+ this._provenanceDegraded = false;
233
+ }
234
+
235
+ /**
236
+ * Returns the attached seek cache, or null if none is set.
237
+ * @returns {import('../ports/SeekCachePort.js').default|null}
238
+ */
239
+ get seekCache() {
240
+ return this._seekCache;
241
+ }
242
+
243
+ /**
244
+ * Attaches a persistent seek cache after construction.
245
+ *
246
+ * Useful when the cache adapter cannot be created until after the
247
+ * graph is opened (e.g. the CLI wires it based on flags).
248
+ *
249
+ * @param {import('../ports/SeekCachePort.js').default} cache - SeekCachePort implementation
250
+ */
251
+ setSeekCache(cache) {
252
+ this._seekCache = cache;
190
253
  }
191
254
 
192
255
  /**
@@ -227,6 +290,7 @@ export default class WarpGraph {
227
290
  * @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
228
291
  * @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
229
292
  * @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
293
+ * @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
230
294
  * @returns {Promise<WarpGraph>} The opened graph instance
231
295
  * @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
232
296
  *
@@ -237,7 +301,7 @@ export default class WarpGraph {
237
301
  * writerId: 'node-1'
238
302
  * });
239
303
  */
240
- static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec }) {
304
+ static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache }) {
241
305
  // Validate inputs
242
306
  validateGraphName(graphName);
243
307
  validateWriterId(writerId);
@@ -269,7 +333,7 @@ export default class WarpGraph {
269
333
  }
270
334
  }
271
335
 
272
- const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec });
336
+ const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache });
273
337
 
274
338
  // Validate migration boundary
275
339
  await graph._validateMigrationBoundary();
@@ -295,7 +359,7 @@ export default class WarpGraph {
295
359
 
296
360
  /**
297
361
  * Gets the persistence adapter.
298
- * @returns {import('../ports/GraphPersistencePort.js').default} The persistence adapter
362
+ * @returns {FullPersistence} The persistence adapter
299
363
  */
300
364
  get persistence() {
301
365
  return this._persistence;
@@ -337,9 +401,9 @@ export default class WarpGraph {
337
401
  getCurrentState: () => this._cachedState,
338
402
  expectedParentSha: parentSha,
339
403
  onDeleteWithData: this._onDeleteWithData,
340
- onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
404
+ onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
341
405
  codec: this._codec,
342
- logger: this._logger,
406
+ logger: this._logger || undefined,
343
407
  });
344
408
  }
345
409
 
@@ -348,7 +412,7 @@ export default class WarpGraph {
348
412
  *
349
413
  * @param {string} writerId - The writer ID to load patches for
350
414
  * @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
415
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
352
416
  */
353
417
  async getWriterPatches(writerId, stopAtSha = null) {
354
418
  return await this._loadWriterPatches(writerId, stopAtSha);
@@ -399,7 +463,7 @@ export default class WarpGraph {
399
463
  *
400
464
  * @param {string} writerId - The writer ID to load patches for
401
465
  * @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
466
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
403
467
  * @private
404
468
  */
405
469
  async _loadWriterPatches(writerId, stopAtSha = null) {
@@ -430,7 +494,7 @@ export default class WarpGraph {
430
494
 
431
495
  // Read the patch blob
432
496
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
433
- const patch = this._codec.decode(patchBuffer);
497
+ const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
434
498
 
435
499
  patches.push({ patch, sha: currentSha });
436
500
 
@@ -474,8 +538,8 @@ export default class WarpGraph {
474
538
  incoming.get(to).push({ neighborId: from, label });
475
539
  }
476
540
 
477
- const sortNeighbors = (list) => {
478
- list.sort((a, b) => {
541
+ const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
542
+ list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
479
543
  if (a.neighborId !== b.neighborId) {
480
544
  return a.neighborId < b.neighborId ? -1 : 1;
481
545
  }
@@ -529,7 +593,7 @@ export default class WarpGraph {
529
593
  * provenance index, and frontier tracking.
530
594
  *
531
595
  * @param {string} writerId - The writer ID that committed the patch
532
- * @param {{patch?: Object, sha?: string}} [opts] - Commit details
596
+ * @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
533
597
  * @private
534
598
  */
535
599
  async _onPatchCommitted(writerId, { patch, sha } = {}) {
@@ -538,11 +602,11 @@ export default class WarpGraph {
538
602
  // Eager re-materialize: apply the just-committed patch to cached state
539
603
  // Only when the cache is clean — applying a patch to stale state would be incorrect
540
604
  if (this._cachedState && !this._stateDirty && patch && sha) {
541
- joinPatch(this._cachedState, patch, sha);
605
+ joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
542
606
  await this._setMaterializedState(this._cachedState);
543
607
  // Update provenance index with new patch
544
608
  if (this._provenanceIndex) {
545
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
609
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
546
610
  }
547
611
  // Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
548
612
  if (this._lastFrontier) {
@@ -561,9 +625,9 @@ export default class WarpGraph {
561
625
  async _materializeGraph() {
562
626
  const state = await this.materialize();
563
627
  if (!this._materializedGraph || this._materializedGraph.state !== state) {
564
- await this._setMaterializedState(state);
628
+ await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
565
629
  }
566
- return this._materializedGraph;
630
+ return /** @type {MaterializedGraph} */ (this._materializedGraph);
567
631
  }
568
632
 
569
633
  /**
@@ -602,14 +666,16 @@ export default class WarpGraph {
602
666
 
603
667
  // When ceiling is active, delegate to ceiling-aware path (with its own cache)
604
668
  if (ceiling !== null) {
605
- return await this._materializeWithCeiling(ceiling, collectReceipts, t0);
669
+ return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
606
670
  }
607
671
 
608
672
  try {
609
673
  // Check for checkpoint
610
674
  const checkpoint = await this._loadLatestCheckpoint();
611
675
 
676
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
612
677
  let state;
678
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
613
679
  let receipts;
614
680
  let patchCount = 0;
615
681
 
@@ -617,20 +683,21 @@ export default class WarpGraph {
617
683
  if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
618
684
  const patches = await this._loadPatchesSince(checkpoint);
619
685
  if (collectReceipts) {
620
- const result = reduceV5(patches, checkpoint.state, { receipts: true });
686
+ 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
687
  state = result.state;
622
688
  receipts = result.receipts;
623
689
  } else {
624
- state = reduceV5(patches, checkpoint.state);
690
+ 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
691
  }
626
692
  patchCount = patches.length;
627
693
 
628
694
  // Build provenance index: start from checkpoint index if present, then add new patches
629
- this._provenanceIndex = checkpoint.provenanceIndex
630
- ? checkpoint.provenanceIndex.clone()
695
+ const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
696
+ this._provenanceIndex = ckPI
697
+ ? ckPI.clone()
631
698
  : new ProvenanceIndex();
632
699
  for (const { patch, sha } of patches) {
633
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
700
+ /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
634
701
  }
635
702
  } else {
636
703
  // 1. Discover all writers
@@ -661,11 +728,11 @@ export default class WarpGraph {
661
728
  } else {
662
729
  // 5. Reduce all patches to state
663
730
  if (collectReceipts) {
664
- const result = reduceV5(allPatches, undefined, { receipts: true });
731
+ 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
732
  state = result.state;
666
733
  receipts = result.receipts;
667
734
  } else {
668
- state = reduceV5(allPatches);
735
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
669
736
  }
670
737
  patchCount = allPatches.length;
671
738
 
@@ -679,6 +746,7 @@ export default class WarpGraph {
679
746
  }
680
747
 
681
748
  await this._setMaterializedState(state);
749
+ this._provenanceDegraded = false;
682
750
  this._cachedCeiling = null;
683
751
  this._cachedFrontier = null;
684
752
  this._lastFrontier = await this.getFrontier();
@@ -712,11 +780,11 @@ export default class WarpGraph {
712
780
  this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
713
781
 
714
782
  if (collectReceipts) {
715
- return { state, receipts };
783
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
716
784
  }
717
785
  return state;
718
786
  } catch (err) {
719
- this._logTiming('materialize', t0, { error: err });
787
+ this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
720
788
  throw err;
721
789
  }
722
790
  }
@@ -770,12 +838,13 @@ export default class WarpGraph {
770
838
 
771
839
  // Cache hit: same ceiling, clean state, AND frontier unchanged.
772
840
  // Bypass cache when collectReceipts is true — cached path has no receipts.
841
+ const cf = this._cachedFrontier;
773
842
  if (
774
843
  this._cachedState && !this._stateDirty &&
775
844
  ceiling === this._cachedCeiling && !collectReceipts &&
776
- this._cachedFrontier !== null &&
777
- this._cachedFrontier.size === frontier.size &&
778
- [...frontier].every(([w, sha]) => this._cachedFrontier.get(w) === sha)
845
+ cf !== null &&
846
+ cf.size === frontier.size &&
847
+ [...frontier].every(([w, sha]) => cf.get(w) === sha)
779
848
  ) {
780
849
  return this._cachedState;
781
850
  }
@@ -785,6 +854,7 @@ export default class WarpGraph {
785
854
  if (writerIds.length === 0 || ceiling <= 0) {
786
855
  const state = createEmptyStateV5();
787
856
  this._provenanceIndex = new ProvenanceIndex();
857
+ this._provenanceDegraded = false;
788
858
  await this._setMaterializedState(state);
789
859
  this._cachedCeiling = ceiling;
790
860
  this._cachedFrontier = frontier;
@@ -795,6 +865,32 @@ export default class WarpGraph {
795
865
  return state;
796
866
  }
797
867
 
868
+ // Persistent cache check — skip when collectReceipts is requested
869
+ let cacheKey;
870
+ if (this._seekCache && !collectReceipts) {
871
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
872
+ try {
873
+ const cached = await this._seekCache.get(cacheKey);
874
+ if (cached) {
875
+ try {
876
+ const state = deserializeFullStateV5(cached, { codec: this._codec });
877
+ this._provenanceIndex = new ProvenanceIndex();
878
+ this._provenanceDegraded = true;
879
+ await this._setMaterializedState(state);
880
+ this._cachedCeiling = ceiling;
881
+ this._cachedFrontier = frontier;
882
+ this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
883
+ return state;
884
+ } catch {
885
+ // Corrupted payload — self-heal by removing the bad entry
886
+ try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
887
+ }
888
+ }
889
+ } catch {
890
+ // Cache read failed — fall through to full materialization
891
+ }
892
+ }
893
+
798
894
  const allPatches = [];
799
895
  for (const writerId of writerIds) {
800
896
  const writerPatches = await this._loadWriterPatches(writerId);
@@ -805,7 +901,9 @@ export default class WarpGraph {
805
901
  }
806
902
  }
807
903
 
904
+ /** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
808
905
  let state;
906
+ /** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
809
907
  let receipts;
810
908
 
811
909
  if (allPatches.length === 0) {
@@ -814,27 +912,37 @@ export default class WarpGraph {
814
912
  receipts = [];
815
913
  }
816
914
  } else if (collectReceipts) {
817
- const result = reduceV5(allPatches, undefined, { receipts: true });
915
+ 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
916
  state = result.state;
819
917
  receipts = result.receipts;
820
918
  } else {
821
- state = reduceV5(allPatches);
919
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
822
920
  }
823
921
 
824
922
  this._provenanceIndex = new ProvenanceIndex();
825
923
  for (const { patch, sha } of allPatches) {
826
- this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
924
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
827
925
  }
926
+ this._provenanceDegraded = false;
828
927
 
829
928
  await this._setMaterializedState(state);
830
929
  this._cachedCeiling = ceiling;
831
930
  this._cachedFrontier = frontier;
832
931
 
932
+ // Store to persistent cache (fire-and-forget — failure is non-fatal)
933
+ if (this._seekCache && !collectReceipts && allPatches.length > 0) {
934
+ if (!cacheKey) {
935
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
936
+ }
937
+ const buf = serializeFullStateV5(state, { codec: this._codec });
938
+ this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
939
+ }
940
+
833
941
  // Skip auto-checkpoint and GC — this is an exploratory read
834
942
  this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
835
943
 
836
944
  if (collectReceipts) {
837
- return { state, receipts };
945
+ return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
838
946
  }
839
947
  return state;
840
948
  }
@@ -883,16 +991,16 @@ export default class WarpGraph {
883
991
  }
884
992
 
885
993
  // Capture pre-merge counts for receipt
886
- const beforeNodes = this._cachedState.nodeAlive.elements.size;
887
- const beforeEdges = this._cachedState.edgeAlive.elements.size;
994
+ const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
995
+ const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
888
996
  const beforeFrontierSize = this._cachedState.observedFrontier.size;
889
997
 
890
998
  // Perform the join
891
999
  const mergedState = joinStates(this._cachedState, otherState);
892
1000
 
893
1001
  // Calculate receipt
894
- const afterNodes = mergedState.nodeAlive.elements.size;
895
- const afterEdges = mergedState.edgeAlive.elements.size;
1002
+ const afterNodes = orsetElements(mergedState.nodeAlive).length;
1003
+ const afterEdges = orsetElements(mergedState.edgeAlive).length;
896
1004
  const afterFrontierSize = mergedState.observedFrontier.size;
897
1005
 
898
1006
  // Count property changes (keys that existed in both but have different values)
@@ -954,7 +1062,7 @@ export default class WarpGraph {
954
1062
  * @example
955
1063
  * // Time-travel to a previous checkpoint
956
1064
  * const oldState = await graph.materializeAt('abc123');
957
- * console.log('Nodes at checkpoint:', [...oldState.nodeAlive.elements.keys()]);
1065
+ * console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
958
1066
  */
959
1067
  async materializeAt(checkpointSha) {
960
1068
  // 1. Discover current writers to build target frontier
@@ -971,7 +1079,7 @@ export default class WarpGraph {
971
1079
  }
972
1080
 
973
1081
  // 3. Create a patch loader function for incremental materialization
974
- const patchLoader = async (writerId, fromSha, toSha) => {
1082
+ const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
975
1083
  // Load patches from fromSha (exclusive) to toSha (inclusive)
976
1084
  // Walk from toSha back to fromSha
977
1085
  const patches = [];
@@ -1004,7 +1112,7 @@ export default class WarpGraph {
1004
1112
 
1005
1113
  // 4. Call materializeIncremental with the checkpoint and target frontier
1006
1114
  const state = await materializeIncremental({
1007
- persistence: this._persistence,
1115
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1008
1116
  graphName: this._graphName,
1009
1117
  checkpointSha,
1010
1118
  targetFrontier,
@@ -1048,23 +1156,24 @@ export default class WarpGraph {
1048
1156
  // 3. Materialize current state (reuse cached if fresh, guard against recursion)
1049
1157
  const prevCheckpointing = this._checkpointing;
1050
1158
  this._checkpointing = true;
1159
+ /** @type {import('./services/JoinReducer.js').WarpStateV5} */
1051
1160
  let state;
1052
1161
  try {
1053
- state = (this._cachedState && !this._stateDirty)
1162
+ state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
1054
1163
  ? this._cachedState
1055
- : await this.materialize();
1164
+ : await this.materialize());
1056
1165
  } finally {
1057
1166
  this._checkpointing = prevCheckpointing;
1058
1167
  }
1059
1168
 
1060
1169
  // 4. Call CheckpointService.create() with provenance index if available
1061
1170
  const checkpointSha = await createCheckpointCommit({
1062
- persistence: this._persistence,
1171
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1063
1172
  graphName: this._graphName,
1064
1173
  state,
1065
1174
  frontier,
1066
1175
  parents,
1067
- provenanceIndex: this._provenanceIndex,
1176
+ provenanceIndex: this._provenanceIndex || undefined,
1068
1177
  crypto: this._crypto,
1069
1178
  codec: this._codec,
1070
1179
  });
@@ -1078,7 +1187,7 @@ export default class WarpGraph {
1078
1187
  // 6. Return checkpoint SHA
1079
1188
  return checkpointSha;
1080
1189
  } catch (err) {
1081
- this._logTiming('createCheckpoint', t0, { error: err });
1190
+ this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
1082
1191
  throw err;
1083
1192
  }
1084
1193
  }
@@ -1172,6 +1281,7 @@ export default class WarpGraph {
1172
1281
  */
1173
1282
  async discoverTicks() {
1174
1283
  const writerIds = await this.discoverWriters();
1284
+ /** @type {Set<number>} */
1175
1285
  const globalTickSet = new Set();
1176
1286
  const perWriter = new Map();
1177
1287
 
@@ -1179,6 +1289,7 @@ export default class WarpGraph {
1179
1289
  const writerRef = buildWriterRef(this._graphName, writerId);
1180
1290
  const tipSha = await this._persistence.readRef(writerRef);
1181
1291
  const writerTicks = [];
1292
+ /** @type {Record<number, string>} */
1182
1293
  const tickShas = {};
1183
1294
 
1184
1295
  if (tipSha) {
@@ -1256,7 +1367,7 @@ export default class WarpGraph {
1256
1367
  /**
1257
1368
  * Loads the latest checkpoint for this graph.
1258
1369
  *
1259
- * @returns {Promise<{state: Object, frontier: Map, stateHash: string, schema: number}|null>} The checkpoint or null
1370
+ * @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
1371
  * @private
1261
1372
  */
1262
1373
  async _loadLatestCheckpoint() {
@@ -1298,7 +1409,7 @@ export default class WarpGraph {
1298
1409
  if (kind === 'patch') {
1299
1410
  const patchMeta = decodePatchMessage(nodeInfo.message);
1300
1411
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
1301
- const patch = this._codec.decode(patchBuffer);
1412
+ const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
1302
1413
 
1303
1414
  // If any patch has schema:1, we have v1 history
1304
1415
  if (patch.schema === 1 || patch.schema === undefined) {
@@ -1313,8 +1424,8 @@ export default class WarpGraph {
1313
1424
  /**
1314
1425
  * Loads patches since a checkpoint for incremental materialization.
1315
1426
  *
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
1427
+ * @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
1428
+ * @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Patches since checkpoint
1318
1429
  * @private
1319
1430
  */
1320
1431
  async _loadPatchesSince(checkpoint) {
@@ -1396,7 +1507,7 @@ export default class WarpGraph {
1396
1507
  *
1397
1508
  * @param {string} writerId - The writer ID for this patch
1398
1509
  * @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
1510
+ * @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
1400
1511
  * @returns {Promise<void>}
1401
1512
  * @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
1402
1513
  * @throws {Error} If patch does not extend checkpoint head (writer fork detected)
@@ -1444,18 +1555,19 @@ export default class WarpGraph {
1444
1555
  _maybeRunGC(state) {
1445
1556
  try {
1446
1557
  const metrics = collectGCMetrics(state);
1558
+ /** @type {import('./services/GCPolicy.js').GCInputMetrics} */
1447
1559
  const inputMetrics = {
1448
1560
  ...metrics,
1449
1561
  patchesSinceCompaction: this._patchesSinceGC,
1450
1562
  timeSinceCompaction: Date.now() - this._lastGCTime,
1451
1563
  };
1452
- const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
1564
+ const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1453
1565
 
1454
1566
  if (!shouldRun) {
1455
1567
  return;
1456
1568
  }
1457
1569
 
1458
- if (this._gcPolicy.enabled) {
1570
+ if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
1459
1571
  const appliedVV = computeAppliedVV(state);
1460
1572
  const result = executeGC(state, appliedVV);
1461
1573
  this._lastGCTime = Date.now();
@@ -1494,11 +1606,15 @@ export default class WarpGraph {
1494
1606
  return { ran: false, result: null, reasons: [] };
1495
1607
  }
1496
1608
 
1497
- const metrics = collectGCMetrics(this._cachedState);
1498
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1499
- metrics.lastCompactionTime = this._lastGCTime;
1609
+ const rawMetrics = collectGCMetrics(this._cachedState);
1610
+ /** @type {import('./services/GCPolicy.js').GCInputMetrics} */
1611
+ const metrics = {
1612
+ ...rawMetrics,
1613
+ patchesSinceCompaction: this._patchesSinceGC,
1614
+ timeSinceCompaction: this._lastGCTime > 0 ? Date.now() - this._lastGCTime : 0,
1615
+ };
1500
1616
 
1501
- const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
1617
+ const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
1502
1618
 
1503
1619
  if (!shouldRun) {
1504
1620
  return { ran: false, result: null, reasons: [] };
@@ -1545,7 +1661,7 @@ export default class WarpGraph {
1545
1661
 
1546
1662
  return result;
1547
1663
  } catch (err) {
1548
- this._logTiming('runGC', t0, { error: err });
1664
+ this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
1549
1665
  throw err;
1550
1666
  }
1551
1667
  }
@@ -1567,10 +1683,15 @@ export default class WarpGraph {
1567
1683
  return null;
1568
1684
  }
1569
1685
 
1570
- const metrics = collectGCMetrics(this._cachedState);
1571
- metrics.patchesSinceCompaction = this._patchesSinceGC;
1572
- metrics.lastCompactionTime = this._lastGCTime;
1573
- return metrics;
1686
+ const rawMetrics = collectGCMetrics(this._cachedState);
1687
+ return {
1688
+ ...rawMetrics,
1689
+ nodeCount: rawMetrics.nodeLiveDots,
1690
+ edgeCount: rawMetrics.edgeLiveDots,
1691
+ tombstoneCount: rawMetrics.totalTombstones,
1692
+ patchesSinceCompaction: this._patchesSinceGC,
1693
+ lastCompactionTime: this._lastGCTime,
1694
+ };
1574
1695
  }
1575
1696
 
1576
1697
  /**
@@ -1653,6 +1774,7 @@ export default class WarpGraph {
1653
1774
  */
1654
1775
  async status() {
1655
1776
  // Determine cachedState
1777
+ /** @type {'fresh' | 'stale' | 'none'} */
1656
1778
  let cachedState;
1657
1779
  if (this._cachedState === null) {
1658
1780
  cachedState = 'none';
@@ -1702,7 +1824,7 @@ export default class WarpGraph {
1702
1824
  * One handler's error does not prevent other handlers from being called.
1703
1825
  *
1704
1826
  * @param {Object} options - Subscription options
1705
- * @param {(diff: import('./services/StateDiff.js').StateDiff) => void} options.onChange - Called with diff when graph changes
1827
+ * @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with diff when graph changes
1706
1828
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1707
1829
  * @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
1708
1830
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1745,7 +1867,7 @@ export default class WarpGraph {
1745
1867
  } catch (err) {
1746
1868
  if (onError) {
1747
1869
  try {
1748
- onError(err);
1870
+ onError(/** @type {Error} */ (err));
1749
1871
  } catch {
1750
1872
  // onError itself threw — swallow to prevent cascade
1751
1873
  }
@@ -1782,7 +1904,7 @@ export default class WarpGraph {
1782
1904
  *
1783
1905
  * @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
1784
1906
  * @param {Object} options - Watch options
1785
- * @param {(diff: import('./services/StateDiff.js').StateDiff) => void} options.onChange - Called with filtered diff when matching changes occur
1907
+ * @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with filtered diff when matching changes occur
1786
1908
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
1787
1909
  * @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
1788
1910
  * @returns {{unsubscribe: () => void}} Subscription handle
@@ -1823,31 +1945,32 @@ export default class WarpGraph {
1823
1945
 
1824
1946
  // Pattern matching: same logic as QueryBuilder.match()
1825
1947
  // Pre-compile pattern matcher once for performance
1948
+ /** @type {(nodeId: string) => boolean} */
1826
1949
  let matchesPattern;
1827
1950
  if (pattern === '*') {
1828
1951
  matchesPattern = () => true;
1829
1952
  } else if (pattern.includes('*')) {
1830
1953
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
1831
1954
  const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
1832
- matchesPattern = (nodeId) => regex.test(nodeId);
1955
+ matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
1833
1956
  } else {
1834
- matchesPattern = (nodeId) => nodeId === pattern;
1957
+ matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
1835
1958
  }
1836
1959
 
1837
1960
  // Filtered onChange that only passes matching changes
1838
- const filteredOnChange = (diff) => {
1961
+ const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
1839
1962
  const filteredDiff = {
1840
1963
  nodes: {
1841
1964
  added: diff.nodes.added.filter(matchesPattern),
1842
1965
  removed: diff.nodes.removed.filter(matchesPattern),
1843
1966
  },
1844
1967
  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)),
1968
+ added: diff.edges.added.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
1969
+ removed: diff.edges.removed.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
1847
1970
  },
1848
1971
  props: {
1849
- set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
1850
- removed: diff.props.removed.filter(p => matchesPattern(p.nodeId)),
1972
+ set: diff.props.set.filter((/** @type {import('./services/StateDiff.js').PropSet} */ p) => matchesPattern(p.nodeId)),
1973
+ removed: diff.props.removed.filter((/** @type {import('./services/StateDiff.js').PropRemoved} */ p) => matchesPattern(p.nodeId)),
1851
1974
  },
1852
1975
  };
1853
1976
 
@@ -1869,6 +1992,7 @@ export default class WarpGraph {
1869
1992
  const subscription = this.subscribe({ onChange: filteredOnChange, onError });
1870
1993
 
1871
1994
  // Polling: periodically check frontier and auto-materialize if changed
1995
+ /** @type {ReturnType<typeof setInterval>|null} */
1872
1996
  let pollIntervalId = null;
1873
1997
  let pollInFlight = false;
1874
1998
  if (poll) {
@@ -1950,7 +2074,7 @@ export default class WarpGraph {
1950
2074
  * Creates a sync request to send to a remote peer.
1951
2075
  * The request contains the local frontier for comparison.
1952
2076
  *
1953
- * @returns {Promise<{type: 'sync-request', frontier: Map<string, string>}>} The sync request
2077
+ * @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
1954
2078
  * @throws {Error} If listing refs fails
1955
2079
  *
1956
2080
  * @example
@@ -1965,8 +2089,8 @@ export default class WarpGraph {
1965
2089
  /**
1966
2090
  * Processes an incoming sync request and returns patches the requester needs.
1967
2091
  *
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
2092
+ * @param {import('./services/SyncProtocol.js').SyncRequest} request - The incoming sync request
2093
+ * @returns {Promise<import('./services/SyncProtocol.js').SyncResponse>} The sync response
1970
2094
  * @throws {Error} If listing refs or reading patches fails
1971
2095
  *
1972
2096
  * @example
@@ -1979,7 +2103,7 @@ export default class WarpGraph {
1979
2103
  return await processSyncRequest(
1980
2104
  request,
1981
2105
  localFrontier,
1982
- this._persistence,
2106
+ /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
1983
2107
  this._graphName,
1984
2108
  { codec: this._codec }
1985
2109
  );
@@ -1991,8 +2115,8 @@ export default class WarpGraph {
1991
2115
  *
1992
2116
  * **Requires a cached state.**
1993
2117
  *
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
2118
+ * @param {import('./services/SyncProtocol.js').SyncResponse} response - The sync response
2119
+ * @returns {{state: import('./services/JoinReducer.js').WarpStateV5, applied: number}} Result with updated state
1996
2120
  * @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
1997
2121
  *
1998
2122
  * @example
@@ -2007,8 +2131,8 @@ export default class WarpGraph {
2007
2131
  });
2008
2132
  }
2009
2133
 
2010
- const currentFrontier = this._cachedState.observedFrontier;
2011
- const result = applySyncResponse(response, this._cachedState, currentFrontier);
2134
+ const currentFrontier = /** @type {any} */ (this._cachedState.observedFrontier); // TODO(ts-cleanup): narrow port type
2135
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} */ (applySyncResponse(response, this._cachedState, currentFrontier));
2012
2136
 
2013
2137
  // Update cached state
2014
2138
  this._cachedState = result.state;
@@ -2047,18 +2171,15 @@ export default class WarpGraph {
2047
2171
  * @param {string|WarpGraph} remote - URL or peer graph instance
2048
2172
  * @param {Object} [options]
2049
2173
  * @param {string} [options.path='/sync'] - Sync path (HTTP mode)
2050
- * @param {number} [options.retries=3] - Retry count for retryable failures
2174
+ * @param {number} [options.retries=3] - Retry count
2051
2175
  * @param {number} [options.baseDelayMs=250] - Base backoff delay
2052
2176
  * @param {number} [options.maxDelayMs=2000] - Max backoff delay
2053
- * @param {number} [options.timeoutMs=10000] - Request timeout (HTTP mode)
2054
- * @param {AbortSignal} [options.signal] - Optional abort signal to cancel sync
2177
+ * @param {number} [options.timeoutMs=10000] - Request timeout
2178
+ * @param {AbortSignal} [options.signal] - Abort signal
2055
2179
  * @param {(event: {type: string, attempt: number, durationMs?: number, status?: number, error?: Error}) => void} [options.onStatus]
2056
- * @param {boolean} [options.materialize=false] - If true, auto-materialize after sync and include state in result
2180
+ * @param {boolean} [options.materialize=false] - Auto-materialize after sync
2181
+ * @param {{ secret: string, keyId?: string }} [options.auth] - Client auth credentials
2057
2182
  * @returns {Promise<{applied: number, attempts: number, state?: import('./services/JoinReducer.js').WarpStateV5}>}
2058
- * @throws {SyncError} If remote URL is invalid (code: `E_SYNC_REMOTE_URL`)
2059
- * @throws {SyncError} If remote returns error or invalid response (code: `E_SYNC_REMOTE`, `E_SYNC_PROTOCOL`)
2060
- * @throws {SyncError} If request times out (code: `E_SYNC_TIMEOUT`)
2061
- * @throws {OperationAbortedError} If abort signal fires
2062
2183
  */
2063
2184
  async syncWith(remote, options = {}) {
2064
2185
  const t0 = this._clock.now();
@@ -2071,6 +2192,7 @@ export default class WarpGraph {
2071
2192
  signal,
2072
2193
  onStatus,
2073
2194
  materialize: materializeAfterSync = false,
2195
+ auth,
2074
2196
  } = options;
2075
2197
 
2076
2198
  const hasPathOverride = Object.prototype.hasOwnProperty.call(options, 'path');
@@ -2079,7 +2201,7 @@ export default class WarpGraph {
2079
2201
  let targetUrl = null;
2080
2202
  if (!isDirectPeer) {
2081
2203
  try {
2082
- targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(remote);
2204
+ targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(/** @type {string} */ (remote));
2083
2205
  } catch {
2084
2206
  throw new SyncError('Invalid remote URL', {
2085
2207
  code: 'E_SYNC_REMOTE_URL',
@@ -2102,31 +2224,26 @@ export default class WarpGraph {
2102
2224
  }
2103
2225
  targetUrl.hash = '';
2104
2226
  }
2105
-
2106
2227
  let attempt = 0;
2107
- const emit = (type, payload = {}) => {
2228
+ const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
2108
2229
  if (typeof onStatus === 'function') {
2109
- onStatus({ type, attempt, ...payload });
2230
+ onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
2110
2231
  }
2111
2232
  };
2112
-
2113
- const shouldRetry = (err) => {
2233
+ const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
2114
2234
  if (isDirectPeer) { return false; }
2115
2235
  if (err instanceof SyncError) {
2116
2236
  return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
2117
2237
  }
2118
2238
  return err instanceof TimeoutError;
2119
2239
  };
2120
-
2121
2240
  const executeAttempt = async () => {
2122
2241
  checkAborted(signal, 'syncWith');
2123
2242
  attempt += 1;
2124
2243
  const attemptStart = Date.now();
2125
2244
  emit('connecting');
2126
-
2127
2245
  const request = await this.createSyncRequest();
2128
2246
  emit('requestBuilt');
2129
-
2130
2247
  let response;
2131
2248
  if (isDirectPeer) {
2132
2249
  emit('requestSent');
@@ -2134,24 +2251,29 @@ export default class WarpGraph {
2134
2251
  emit('responseReceived');
2135
2252
  } else {
2136
2253
  emit('requestSent');
2254
+ const bodyStr = JSON.stringify(request);
2255
+ const authHeaders = await buildSyncAuthHeaders({
2256
+ auth, bodyStr, targetUrl: /** @type {URL} */ (targetUrl), crypto: this._crypto,
2257
+ });
2137
2258
  let res;
2138
2259
  try {
2139
2260
  res = await timeout(timeoutMs, (timeoutSignal) => {
2140
2261
  const combinedSignal = signal
2141
2262
  ? AbortSignal.any([timeoutSignal, signal])
2142
2263
  : timeoutSignal;
2143
- return fetch(targetUrl.toString(), {
2264
+ return fetch(/** @type {URL} */ (targetUrl).toString(), {
2144
2265
  method: 'POST',
2145
2266
  headers: {
2146
2267
  'content-type': 'application/json',
2147
2268
  'accept': 'application/json',
2269
+ ...authHeaders,
2148
2270
  },
2149
- body: JSON.stringify(request),
2271
+ body: bodyStr,
2150
2272
  signal: combinedSignal,
2151
2273
  });
2152
2274
  });
2153
2275
  } catch (err) {
2154
- if (err?.name === 'AbortError') {
2276
+ if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
2155
2277
  throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
2156
2278
  }
2157
2279
  if (err instanceof TimeoutError) {
@@ -2162,7 +2284,7 @@ export default class WarpGraph {
2162
2284
  }
2163
2285
  throw new SyncError('Network error', {
2164
2286
  code: 'E_SYNC_NETWORK',
2165
- context: { message: err?.message },
2287
+ context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
2166
2288
  });
2167
2289
  }
2168
2290
 
@@ -2223,9 +2345,9 @@ export default class WarpGraph {
2223
2345
  jitter: 'decorrelated',
2224
2346
  signal,
2225
2347
  shouldRetry,
2226
- onRetry: (error, attemptNumber, delayMs) => {
2348
+ onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
2227
2349
  if (typeof onStatus === 'function') {
2228
- onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
2350
+ onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
2229
2351
  }
2230
2352
  },
2231
2353
  });
@@ -2234,12 +2356,12 @@ export default class WarpGraph {
2234
2356
 
2235
2357
  if (materializeAfterSync) {
2236
2358
  if (!this._cachedState) { await this.materialize(); }
2237
- return { ...syncResult, state: this._cachedState };
2359
+ return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
2238
2360
  }
2239
2361
  return syncResult;
2240
2362
  } catch (err) {
2241
- this._logTiming('syncWith', t0, { error: err });
2242
- if (err?.name === 'AbortError') {
2363
+ this._logTiming('syncWith', t0, { error: /** @type {Error} */ (err) });
2364
+ if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
2243
2365
  const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
2244
2366
  if (typeof onStatus === 'function') {
2245
2367
  onStatus({ type: 'failed', attempt, error: abortedError });
@@ -2247,14 +2369,14 @@ export default class WarpGraph {
2247
2369
  throw abortedError;
2248
2370
  }
2249
2371
  if (err instanceof RetryExhaustedError) {
2250
- const cause = err.cause || err;
2372
+ const cause = /** @type {Error} */ (err.cause || err);
2251
2373
  if (typeof onStatus === 'function') {
2252
2374
  onStatus({ type: 'failed', attempt: err.attempts, error: cause });
2253
2375
  }
2254
2376
  throw cause;
2255
2377
  }
2256
2378
  if (typeof onStatus === 'function') {
2257
- onStatus({ type: 'failed', attempt, error: err });
2379
+ onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
2258
2380
  }
2259
2381
  throw err;
2260
2382
  }
@@ -2269,11 +2391,12 @@ export default class WarpGraph {
2269
2391
  * @param {string} [options.path='/sync'] - Path to handle sync requests
2270
2392
  * @param {number} [options.maxRequestBytes=4194304] - Max request size in bytes
2271
2393
  * @param {import('../ports/HttpServerPort.js').default} options.httpPort - HTTP server adapter (required)
2394
+ * @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only' }} [options.auth] - Auth configuration
2272
2395
  * @returns {Promise<{close: () => Promise<void>, url: string}>} Server handle
2273
2396
  * @throws {Error} If port is not a number
2274
2397
  * @throws {Error} If httpPort adapter is not provided
2275
2398
  */
2276
- async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = {}) {
2399
+ async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort, auth } = /** @type {any} */ ({})) { // TODO(ts-cleanup): needs options type
2277
2400
  if (typeof port !== 'number') {
2278
2401
  throw new Error('serve() requires a numeric port');
2279
2402
  }
@@ -2281,12 +2404,17 @@ export default class WarpGraph {
2281
2404
  throw new Error('serve() requires an httpPort adapter');
2282
2405
  }
2283
2406
 
2407
+ const authConfig = auth
2408
+ ? { ...auth, crypto: this._crypto, logger: this._logger || undefined }
2409
+ : undefined;
2410
+
2284
2411
  const httpServer = new HttpSyncServer({
2285
2412
  httpPort,
2286
2413
  graph: this,
2287
2414
  path,
2288
2415
  host,
2289
2416
  maxRequestBytes,
2417
+ auth: authConfig,
2290
2418
  });
2291
2419
 
2292
2420
  return await httpServer.listen(port);
@@ -2318,8 +2446,8 @@ export default class WarpGraph {
2318
2446
  */
2319
2447
  async writer(writerId) {
2320
2448
  // 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);
2449
+ const configGet = async (/** @type {string} */ key) => await this._persistence.configGet(key);
2450
+ const configSet = async (/** @type {string} */ key, /** @type {string} */ value) => await this._persistence.configSet(key, value);
2323
2451
 
2324
2452
  // Resolve the writer ID
2325
2453
  const resolvedWriterId = await resolveWriterId({
@@ -2330,13 +2458,13 @@ export default class WarpGraph {
2330
2458
  });
2331
2459
 
2332
2460
  return new Writer({
2333
- persistence: this._persistence,
2461
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2334
2462
  graphName: this._graphName,
2335
2463
  writerId: resolvedWriterId,
2336
2464
  versionVector: this._versionVector,
2337
- getCurrentState: () => this._cachedState,
2465
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2338
2466
  onDeleteWithData: this._onDeleteWithData,
2339
- onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
2467
+ onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
2340
2468
  codec: this._codec,
2341
2469
  });
2342
2470
  }
@@ -2384,13 +2512,13 @@ export default class WarpGraph {
2384
2512
  }
2385
2513
 
2386
2514
  return new Writer({
2387
- persistence: this._persistence,
2515
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
2388
2516
  graphName: this._graphName,
2389
2517
  writerId: freshWriterId,
2390
2518
  versionVector: this._versionVector,
2391
- getCurrentState: () => this._cachedState,
2519
+ getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
2392
2520
  onDeleteWithData: this._onDeleteWithData,
2393
- onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
2521
+ onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
2394
2522
  codec: this._codec,
2395
2523
  });
2396
2524
  }
@@ -2545,7 +2673,8 @@ export default class WarpGraph {
2545
2673
  */
2546
2674
  async hasNode(nodeId) {
2547
2675
  await this._ensureFreshState();
2548
- return orsetContains(this._cachedState.nodeAlive, nodeId);
2676
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2677
+ return orsetContains(s.nodeAlive, nodeId);
2549
2678
  }
2550
2679
 
2551
2680
  /**
@@ -2570,15 +2699,16 @@ export default class WarpGraph {
2570
2699
  */
2571
2700
  async getNodeProps(nodeId) {
2572
2701
  await this._ensureFreshState();
2702
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2573
2703
 
2574
2704
  // Check if node exists
2575
- if (!orsetContains(this._cachedState.nodeAlive, nodeId)) {
2705
+ if (!orsetContains(s.nodeAlive, nodeId)) {
2576
2706
  return null;
2577
2707
  }
2578
2708
 
2579
2709
  // Collect all properties for this node
2580
2710
  const props = new Map();
2581
- for (const [propKey, register] of this._cachedState.prop) {
2711
+ for (const [propKey, register] of s.prop) {
2582
2712
  const decoded = decodePropKey(propKey);
2583
2713
  if (decoded.nodeId === nodeId) {
2584
2714
  props.set(decoded.propKey, register.value);
@@ -2612,26 +2742,28 @@ export default class WarpGraph {
2612
2742
  */
2613
2743
  async getEdgeProps(from, to, label) {
2614
2744
  await this._ensureFreshState();
2745
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2615
2746
 
2616
2747
  // Check if edge exists
2617
2748
  const edgeKey = encodeEdgeKey(from, to, label);
2618
- if (!orsetContains(this._cachedState.edgeAlive, edgeKey)) {
2749
+ if (!orsetContains(s.edgeAlive, edgeKey)) {
2619
2750
  return null;
2620
2751
  }
2621
2752
 
2622
2753
  // Check node liveness for both endpoints
2623
- if (!orsetContains(this._cachedState.nodeAlive, from) ||
2624
- !orsetContains(this._cachedState.nodeAlive, to)) {
2754
+ if (!orsetContains(s.nodeAlive, from) ||
2755
+ !orsetContains(s.nodeAlive, to)) {
2625
2756
  return null;
2626
2757
  }
2627
2758
 
2628
2759
  // Determine the birth EventId for clean-slate filtering
2629
- const birthEvent = this._cachedState.edgeBirthEvent?.get(edgeKey);
2760
+ const birthEvent = s.edgeBirthEvent?.get(edgeKey);
2630
2761
 
2631
2762
  // Collect all properties for this edge, filtering out stale props
2632
2763
  // (props set before the edge's most recent re-add)
2764
+ /** @type {Record<string, any>} */
2633
2765
  const props = {};
2634
- for (const [propKey, register] of this._cachedState.prop) {
2766
+ for (const [propKey, register] of s.prop) {
2635
2767
  if (!isEdgePropKey(propKey)) {
2636
2768
  continue;
2637
2769
  }
@@ -2673,11 +2805,13 @@ export default class WarpGraph {
2673
2805
  */
2674
2806
  async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
2675
2807
  await this._ensureFreshState();
2808
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2676
2809
 
2810
+ /** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
2677
2811
  const neighbors = [];
2678
2812
 
2679
2813
  // Iterate over all visible edges
2680
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2814
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2681
2815
  const { from, to, label } = decodeEdgeKey(edgeKey);
2682
2816
 
2683
2817
  // Filter by label if specified
@@ -2688,15 +2822,15 @@ export default class WarpGraph {
2688
2822
  // Check edge direction and collect neighbors
2689
2823
  if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
2690
2824
  // Ensure target node is visible
2691
- if (orsetContains(this._cachedState.nodeAlive, to)) {
2692
- neighbors.push({ nodeId: to, label, direction: 'outgoing' });
2825
+ if (orsetContains(s.nodeAlive, to)) {
2826
+ neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
2693
2827
  }
2694
2828
  }
2695
2829
 
2696
2830
  if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
2697
2831
  // Ensure source node is visible
2698
- if (orsetContains(this._cachedState.nodeAlive, from)) {
2699
- neighbors.push({ nodeId: from, label, direction: 'incoming' });
2832
+ if (orsetContains(s.nodeAlive, from)) {
2833
+ neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
2700
2834
  }
2701
2835
  }
2702
2836
  }
@@ -2704,6 +2838,29 @@ export default class WarpGraph {
2704
2838
  return neighbors;
2705
2839
  }
2706
2840
 
2841
+ /**
2842
+ * Returns a defensive copy of the current materialized state.
2843
+ *
2844
+ * The returned object is a shallow clone: top-level ORSet, LWW, and
2845
+ * VersionVector instances are copied so that mutations by the caller
2846
+ * cannot corrupt the internal cache.
2847
+ *
2848
+ * **Requires a cached state.** Call materialize() first if not already cached.
2849
+ *
2850
+ * @returns {Promise<import('./services/JoinReducer.js').WarpStateV5 | null>}
2851
+ * Cloned state, or null if no state has been materialized yet.
2852
+ */
2853
+ async getStateSnapshot() {
2854
+ if (!this._cachedState && !this._autoMaterialize) {
2855
+ return null;
2856
+ }
2857
+ await this._ensureFreshState();
2858
+ if (!this._cachedState) {
2859
+ return null;
2860
+ }
2861
+ return cloneStateV5(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState));
2862
+ }
2863
+
2707
2864
  /**
2708
2865
  * Gets all visible nodes in the materialized state.
2709
2866
  *
@@ -2721,7 +2878,8 @@ export default class WarpGraph {
2721
2878
  */
2722
2879
  async getNodes() {
2723
2880
  await this._ensureFreshState();
2724
- return [...orsetElements(this._cachedState.nodeAlive)];
2881
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2882
+ return [...orsetElements(s.nodeAlive)];
2725
2883
  }
2726
2884
 
2727
2885
  /**
@@ -2744,12 +2902,13 @@ export default class WarpGraph {
2744
2902
  */
2745
2903
  async getEdges() {
2746
2904
  await this._ensureFreshState();
2905
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2747
2906
 
2748
2907
  // Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
2749
2908
  // Filters out stale props using full EventId ordering via compareEventIds
2750
2909
  // against the edge's birth EventId (clean-slate semantics on re-add)
2751
2910
  const edgePropsByKey = new Map();
2752
- for (const [propKey, register] of this._cachedState.prop) {
2911
+ for (const [propKey, register] of s.prop) {
2753
2912
  if (!isEdgePropKey(propKey)) {
2754
2913
  continue;
2755
2914
  }
@@ -2757,7 +2916,7 @@ export default class WarpGraph {
2757
2916
  const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
2758
2917
 
2759
2918
  // Clean-slate filter: skip props from before the edge's current incarnation
2760
- const birthEvent = this._cachedState.edgeBirthEvent?.get(ek);
2919
+ const birthEvent = s.edgeBirthEvent?.get(ek);
2761
2920
  if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
2762
2921
  continue;
2763
2922
  }
@@ -2771,11 +2930,11 @@ export default class WarpGraph {
2771
2930
  }
2772
2931
 
2773
2932
  const edges = [];
2774
- for (const edgeKey of orsetElements(this._cachedState.edgeAlive)) {
2933
+ for (const edgeKey of orsetElements(s.edgeAlive)) {
2775
2934
  const { from, to, label } = decodeEdgeKey(edgeKey);
2776
2935
  // Only include edges where both endpoints are visible
2777
- if (orsetContains(this._cachedState.nodeAlive, from) &&
2778
- orsetContains(this._cachedState.nodeAlive, to)) {
2936
+ if (orsetContains(s.nodeAlive, from) &&
2937
+ orsetContains(s.nodeAlive, to)) {
2779
2938
  const props = edgePropsByKey.get(edgeKey) || {};
2780
2939
  edges.push({ from, to, label, props });
2781
2940
  }
@@ -2794,7 +2953,8 @@ export default class WarpGraph {
2794
2953
  */
2795
2954
  async getPropertyCount() {
2796
2955
  await this._ensureFreshState();
2797
- return this._cachedState.prop.size;
2956
+ const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
2957
+ return s.prop.size;
2798
2958
  }
2799
2959
 
2800
2960
  // ============================================================================
@@ -2913,9 +3073,9 @@ export default class WarpGraph {
2913
3073
  try {
2914
3074
  validateGraphName(resolvedForkName);
2915
3075
  } catch (err) {
2916
- throw new ForkError(`Invalid fork name: ${err.message}`, {
3076
+ throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
2917
3077
  code: 'E_FORK_NAME_INVALID',
2918
- context: { forkName: resolvedForkName, originalError: err.message },
3078
+ context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
2919
3079
  });
2920
3080
  }
2921
3081
 
@@ -2934,9 +3094,9 @@ export default class WarpGraph {
2934
3094
  try {
2935
3095
  validateWriterId(resolvedForkWriterId);
2936
3096
  } catch (err) {
2937
- throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
3097
+ throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
2938
3098
  code: 'E_FORK_WRITER_ID_INVALID',
2939
- context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
3099
+ context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
2940
3100
  });
2941
3101
  }
2942
3102
 
@@ -2951,10 +3111,10 @@ export default class WarpGraph {
2951
3111
  writerId: resolvedForkWriterId,
2952
3112
  gcPolicy: this._gcPolicy,
2953
3113
  adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
2954
- checkpointPolicy: this._checkpointPolicy,
3114
+ checkpointPolicy: this._checkpointPolicy || undefined,
2955
3115
  autoMaterialize: this._autoMaterialize,
2956
3116
  onDeleteWithData: this._onDeleteWithData,
2957
- logger: this._logger,
3117
+ logger: this._logger || undefined,
2958
3118
  clock: this._clock,
2959
3119
  crypto: this._crypto,
2960
3120
  codec: this._codec,
@@ -2966,7 +3126,7 @@ export default class WarpGraph {
2966
3126
 
2967
3127
  return forkGraph;
2968
3128
  } catch (err) {
2969
- this._logTiming('fork', t0, { error: err });
3129
+ this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
2970
3130
  throw err;
2971
3131
  }
2972
3132
  }
@@ -3020,13 +3180,13 @@ export default class WarpGraph {
3020
3180
  const t0 = this._clock.now();
3021
3181
 
3022
3182
  try {
3023
- const wormhole = await createWormholeImpl({
3183
+ const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
3024
3184
  persistence: this._persistence,
3025
3185
  graphName: this._graphName,
3026
3186
  fromSha,
3027
3187
  toSha,
3028
3188
  codec: this._codec,
3029
- });
3189
+ }));
3030
3190
 
3031
3191
  this._logTiming('createWormhole', t0, {
3032
3192
  metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
@@ -3034,7 +3194,7 @@ export default class WarpGraph {
3034
3194
 
3035
3195
  return wormhole;
3036
3196
  } catch (err) {
3037
- this._logTiming('createWormhole', t0, { error: err });
3197
+ this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
3038
3198
  throw err;
3039
3199
  }
3040
3200
  }
@@ -3068,6 +3228,12 @@ export default class WarpGraph {
3068
3228
  async patchesFor(entityId) {
3069
3229
  await this._ensureFreshState();
3070
3230
 
3231
+ if (this._provenanceDegraded) {
3232
+ throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
3233
+ code: 'E_PROVENANCE_DEGRADED',
3234
+ });
3235
+ }
3236
+
3071
3237
  if (!this._provenanceIndex) {
3072
3238
  throw new QueryError('No provenance index. Call materialize() first.', {
3073
3239
  code: 'E_NO_STATE',
@@ -3129,6 +3295,12 @@ export default class WarpGraph {
3129
3295
  // Ensure fresh state before accessing provenance index
3130
3296
  await this._ensureFreshState();
3131
3297
 
3298
+ if (this._provenanceDegraded) {
3299
+ throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
3300
+ code: 'E_PROVENANCE_DEGRADED',
3301
+ });
3302
+ }
3303
+
3132
3304
  if (!this._provenanceIndex) {
3133
3305
  throw new QueryError('No provenance index. Call materialize() first.', {
3134
3306
  code: 'E_NO_STATE',
@@ -3163,7 +3335,7 @@ export default class WarpGraph {
3163
3335
  this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
3164
3336
 
3165
3337
  if (collectReceipts) {
3166
- const result = reduceV5(sortedPatches, undefined, { receipts: true });
3338
+ const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(sortedPatches, undefined, { receipts: true }));
3167
3339
  return {
3168
3340
  state: result.state,
3169
3341
  patchCount: sortedPatches.length,
@@ -3177,7 +3349,7 @@ export default class WarpGraph {
3177
3349
  patchCount: sortedPatches.length,
3178
3350
  };
3179
3351
  } catch (err) {
3180
- this._logTiming('materializeSlice', t0, { error: err });
3352
+ this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
3181
3353
  throw err;
3182
3354
  }
3183
3355
  }
@@ -3214,7 +3386,7 @@ export default class WarpGraph {
3214
3386
  visited.add(entityId);
3215
3387
 
3216
3388
  // Get all patches that affected this entity
3217
- const patchShas = this._provenanceIndex.patchesFor(entityId);
3389
+ const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
3218
3390
 
3219
3391
  for (const sha of patchShas) {
3220
3392
  if (cone.has(sha)) {
@@ -3226,8 +3398,9 @@ export default class WarpGraph {
3226
3398
  cone.set(sha, patch);
3227
3399
 
3228
3400
  // Add read dependencies to the queue
3229
- if (patch && patch.reads) {
3230
- for (const readEntity of patch.reads) {
3401
+ const patchReads = /** @type {any} */ (patch)?.reads; // TODO(ts-cleanup): type patch array
3402
+ if (patchReads) {
3403
+ for (const readEntity of patchReads) {
3231
3404
  if (!visited.has(readEntity)) {
3232
3405
  queue.push(readEntity);
3233
3406
  }
@@ -3274,7 +3447,7 @@ export default class WarpGraph {
3274
3447
 
3275
3448
  const patchMeta = decodePatchMessage(nodeInfo.message);
3276
3449
  const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
3277
- return this._codec.decode(patchBuffer);
3450
+ return /** @type {Object} */ (this._codec.decode(patchBuffer));
3278
3451
  }
3279
3452
 
3280
3453
  /**
@@ -3302,8 +3475,8 @@ export default class WarpGraph {
3302
3475
  * Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
3303
3476
  * This ensures deterministic ordering regardless of discovery order.
3304
3477
  *
3305
- * @param {Array<{patch: Object, sha: string}>} patches - Unsorted patch entries
3306
- * @returns {Array<{patch: Object, sha: string}>} Sorted patch entries
3478
+ * @param {Array<{patch: any, sha: string}>} patches - Unsorted patch entries
3479
+ * @returns {Array<{patch: any, sha: string}>} Sorted patch entries
3307
3480
  * @private
3308
3481
  */
3309
3482
  _sortPatchesCausally(patches) {