@git-stunts/git-warp 10.3.2 → 10.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +6 -3
  2. package/bin/warp-graph.js +371 -141
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +8 -3
  6. package/src/domain/WarpGraph.js +263 -147
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +19 -0
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/roaring.js +5 -5
  67. package/src/domain/utils/seekCacheKey.js +32 -0
  68. package/src/domain/warp/PatchSession.js +3 -3
  69. package/src/domain/warp/Writer.js +2 -2
  70. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  71. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  72. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  73. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  74. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  75. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  76. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  77. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  78. package/src/infrastructure/codecs/CborCodec.js +16 -8
  79. package/src/ports/BlobPort.js +2 -2
  80. package/src/ports/CodecPort.js +2 -2
  81. package/src/ports/CommitPort.js +8 -21
  82. package/src/ports/ConfigPort.js +3 -3
  83. package/src/ports/CryptoPort.js +7 -7
  84. package/src/ports/GraphPersistencePort.js +12 -14
  85. package/src/ports/HttpServerPort.js +1 -5
  86. package/src/ports/IndexStoragePort.js +1 -0
  87. package/src/ports/LoggerPort.js +9 -9
  88. package/src/ports/RefPort.js +5 -5
  89. package/src/ports/SeekCachePort.js +73 -0
  90. package/src/ports/TreePort.js +3 -3
  91. package/src/visualization/layouts/converters.js +14 -7
  92. package/src/visualization/layouts/elkAdapter.js +17 -4
  93. package/src/visualization/layouts/elkLayout.js +23 -7
  94. package/src/visualization/layouts/index.js +3 -3
  95. package/src/visualization/renderers/ascii/check.js +30 -17
  96. package/src/visualization/renderers/ascii/graph.js +92 -1
  97. package/src/visualization/renderers/ascii/history.js +28 -26
  98. package/src/visualization/renderers/ascii/info.js +9 -7
  99. package/src/visualization/renderers/ascii/materialize.js +20 -16
  100. package/src/visualization/renderers/ascii/opSummary.js +15 -7
  101. package/src/visualization/renderers/ascii/path.js +1 -1
  102. package/src/visualization/renderers/ascii/seek.js +19 -5
  103. package/src/visualization/renderers/ascii/table.js +1 -1
  104. package/src/visualization/renderers/svg/index.js +5 -1
@@ -1,4 +1,5 @@
1
1
  import defaultCodec from '../utils/defaultCodec.js';
2
+ import defaultCrypto from '../utils/defaultCrypto.js';
2
3
  import { orsetContains, orsetElements } from '../crdt/ORSet.js';
3
4
  import { decodeEdgeKey, decodePropKey } from './KeyCodec.js';
4
5
 
@@ -75,7 +76,7 @@ export function propVisibleV5(state, propKey) {
75
76
  * @param {import('./JoinReducer.js').WarpStateV5} state
76
77
  * @param {Object} [options]
77
78
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
78
- * @returns {Buffer}
79
+ * @returns {Buffer|Uint8Array}
79
80
  */
80
81
  export function serializeStateV5(state, { codec } = {}) {
81
82
  const c = codec || defaultCodec;
@@ -122,13 +123,14 @@ export function serializeStateV5(state, { codec } = {}) {
122
123
  * Computes SHA-256 hash of canonical state bytes.
123
124
  * @param {import('./JoinReducer.js').WarpStateV5} state
124
125
  * @param {Object} [options] - Options
125
- * @param {import('../../ports/CryptoPort.js').default} options.crypto - CryptoPort instance
126
+ * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance
126
127
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
127
- * @returns {Promise<string|null>} Hex-encoded SHA-256 hash, or null if no crypto
128
+ * @returns {Promise<string>} Hex-encoded SHA-256 hash
128
129
  */
129
- export async function computeStateHashV5(state, { crypto, codec } = {}) {
130
+ export async function computeStateHashV5(state, { crypto, codec } = /** @type {{crypto?: import('../../ports/CryptoPort.js').default, codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
131
+ const c = crypto || defaultCrypto;
130
132
  const serialized = serializeStateV5(state, { codec });
131
- return crypto ? await crypto.hash('sha256', serialized) : null;
133
+ return await c.hash('sha256', serialized);
132
134
  }
133
135
 
134
136
  /**
@@ -141,7 +143,7 @@ export async function computeStateHashV5(state, { crypto, codec } = {}) {
141
143
  */
142
144
  export function deserializeStateV5(buffer, { codec } = {}) {
143
145
  const c = codec || defaultCodec;
144
- return c.decode(buffer);
146
+ return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: *}>}} */ (c.decode(buffer));
145
147
  }
146
148
 
147
149
  // ============================================================================
@@ -1,4 +1,5 @@
1
1
  import defaultCodec from '../utils/defaultCodec.js';
2
+ import defaultCrypto from '../utils/defaultCrypto.js';
2
3
  import ShardCorruptionError from '../errors/ShardCorruptionError.js';
3
4
  import ShardValidationError from '../errors/ShardValidationError.js';
4
5
  import nullLogger from '../utils/nullLogger.js';
@@ -36,10 +37,9 @@ const BITMAP_BASE_OVERHEAD = 64;
36
37
  *
37
38
  * @param {Object} data - The data object to checksum
38
39
  * @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
39
- * @returns {Promise<string|null>} Hex-encoded SHA-256 hash
40
+ * @returns {Promise<string>} Hex-encoded SHA-256 hash
40
41
  */
41
42
  const computeChecksum = async (data, crypto) => {
42
- if (!crypto) { return null; }
43
43
  const json = canonicalStringify(data);
44
44
  return await crypto.hash('sha256', json);
45
45
  };
@@ -89,6 +89,8 @@ export default class StreamingBitmapIndexBuilder {
89
89
  * Receives { flushedBytes, totalFlushedBytes, flushCount }.
90
90
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for structured logging.
91
91
  * Defaults to NoOpLogger (no logging).
92
+ * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for hashing
93
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
92
94
  */
93
95
  constructor({ storage, maxMemoryBytes = DEFAULT_MAX_MEMORY_BYTES, onFlush, logger = nullLogger, crypto, codec }) {
94
96
  if (!storage) {
@@ -99,9 +101,9 @@ export default class StreamingBitmapIndexBuilder {
99
101
  }
100
102
 
101
103
  /** @type {import('../../ports/CryptoPort.js').default} */
102
- this._crypto = crypto;
104
+ this._crypto = crypto || defaultCrypto;
103
105
 
104
- /** @type {import('../../ports/CodecPort.js').default|undefined} */
106
+ /** @type {import('../../ports/CodecPort.js').default} */
105
107
  this._codec = codec || defaultCodec;
106
108
 
107
109
  /** @type {Object} */
@@ -122,7 +124,7 @@ export default class StreamingBitmapIndexBuilder {
122
124
  /** @type {string[]} ID → SHA reverse mapping (kept in memory) */
123
125
  this.idToSha = [];
124
126
 
125
- /** @type {Map<string, RoaringBitmap32>} Current in-memory bitmaps */
127
+ /** @type {Map<string, any>} Current in-memory bitmaps */
126
128
  this.bitmaps = new Map();
127
129
 
128
130
  /** @type {number} Estimated bytes used by current bitmaps */
@@ -137,8 +139,8 @@ export default class StreamingBitmapIndexBuilder {
137
139
  /** @type {number} Number of flush operations performed */
138
140
  this.flushCount = 0;
139
141
 
140
- /** @type {typeof import('roaring').RoaringBitmap32} Cached constructor */
141
- this._RoaringBitmap32 = getRoaringBitmap32();
142
+ /** @type {any} Cached Roaring bitmap constructor */ // TODO(ts-cleanup): type lazy singleton
143
+ this._RoaringBitmap32 = getRoaringBitmap32(); // TODO(ts-cleanup): type lazy singleton
142
144
  }
143
145
 
144
146
  /**
@@ -189,11 +191,12 @@ export default class StreamingBitmapIndexBuilder {
189
191
  * Groups bitmaps by type ('fwd' or 'rev') and SHA prefix (first 2 hex chars).
190
192
  * Each bitmap is serialized to a portable format and base64-encoded.
191
193
  *
192
- * @returns {{fwd: Object<string, Object<string, string>>, rev: Object<string, Object<string, string>>}}
194
+ * @returns {Record<string, Record<string, Record<string, string>>>}
193
195
  * Object with 'fwd' and 'rev' keys, each mapping prefix to SHA→base64Bitmap entries
194
196
  * @private
195
197
  */
196
198
  _serializeBitmapsToShards() {
199
+ /** @type {Record<string, Record<string, Record<string, string>>>} */
197
200
  const bitmapShards = { fwd: {}, rev: {} };
198
201
  for (const [key, bitmap] of this.bitmaps) {
199
202
  const type = key.substring(0, 3);
@@ -215,7 +218,7 @@ export default class StreamingBitmapIndexBuilder {
215
218
  * The resulting blob OIDs are tracked in `flushedChunks` for later merging.
216
219
  * Writes are performed in parallel for efficiency.
217
220
  *
218
- * @param {{fwd: Object<string, Object<string, string>>, rev: Object<string, Object<string, string>>}} bitmapShards
221
+ * @param {Record<string, Record<string, Record<string, string>>>} bitmapShards
219
222
  * Object with 'fwd' and 'rev' keys containing prefix-grouped bitmap data
220
223
  * @returns {Promise<void>} Resolves when all shards have been written
221
224
  * @async
@@ -235,11 +238,11 @@ export default class StreamingBitmapIndexBuilder {
235
238
  data: shardData,
236
239
  };
237
240
  const buffer = Buffer.from(JSON.stringify(envelope));
238
- const oid = await this.storage.writeBlob(buffer);
241
+ const oid = await /** @type {any} */ (this.storage).writeBlob(buffer); // TODO(ts-cleanup): narrow port type
239
242
  if (!this.flushedChunks.has(path)) {
240
243
  this.flushedChunks.set(path, []);
241
244
  }
242
- this.flushedChunks.get(path).push(oid);
245
+ /** @type {string[]} */ (this.flushedChunks.get(path)).push(oid);
243
246
  })
244
247
  );
245
248
  }
@@ -310,6 +313,7 @@ export default class StreamingBitmapIndexBuilder {
310
313
  * @private
311
314
  */
312
315
  _buildMetaShards() {
316
+ /** @type {Record<string, Record<string, number>>} */
313
317
  const idShards = {};
314
318
  for (const [sha, id] of this.shaToId) {
315
319
  const prefix = sha.substring(0, 2);
@@ -344,7 +348,7 @@ export default class StreamingBitmapIndexBuilder {
344
348
  data: map,
345
349
  };
346
350
  const buffer = Buffer.from(JSON.stringify(envelope));
347
- const oid = await this.storage.writeBlob(buffer);
351
+ const oid = await /** @type {any} */ (this.storage).writeBlob(buffer); // TODO(ts-cleanup): narrow port type
348
352
  return `100644 blob ${oid}\t${path}`;
349
353
  })
350
354
  );
@@ -436,18 +440,19 @@ export default class StreamingBitmapIndexBuilder {
436
440
 
437
441
  // Store frontier metadata for staleness detection
438
442
  if (frontier) {
443
+ /** @type {Record<string, number|undefined>} */
439
444
  const sorted = {};
440
445
  for (const key of Array.from(frontier.keys()).sort()) {
441
446
  sorted[key] = frontier.get(key);
442
447
  }
443
448
  const envelope = { version: 1, writerCount: frontier.size, frontier: sorted };
444
- const cborOid = await this.storage.writeBlob(Buffer.from(this._codec.encode(envelope)));
449
+ const cborOid = await /** @type {any} */ (this.storage).writeBlob(Buffer.from(/** @type {any} */ (this._codec).encode(envelope))); // TODO(ts-cleanup): narrow port type
445
450
  flatEntries.push(`100644 blob ${cborOid}\tfrontier.cbor`);
446
- const jsonOid = await this.storage.writeBlob(Buffer.from(canonicalStringify(envelope)));
451
+ const jsonOid = await /** @type {any} */ (this.storage).writeBlob(Buffer.from(canonicalStringify(envelope))); // TODO(ts-cleanup): narrow port type
447
452
  flatEntries.push(`100644 blob ${jsonOid}\tfrontier.json`);
448
453
  }
449
454
 
450
- const treeOid = await this.storage.writeTree(flatEntries);
455
+ const treeOid = await /** @type {any} */ (this.storage).writeTree(flatEntries); // TODO(ts-cleanup): narrow port type
451
456
 
452
457
  this.logger.debug('Index finalized', {
453
458
  operation: 'finalize',
@@ -501,7 +506,7 @@ export default class StreamingBitmapIndexBuilder {
501
506
  */
502
507
  _getOrCreateId(sha) {
503
508
  if (this.shaToId.has(sha)) {
504
- return this.shaToId.get(sha);
509
+ return /** @type {number} */ (this.shaToId.get(sha));
505
510
  }
506
511
  const id = this.idToSha.length;
507
512
  this.idToSha.push(sha);
@@ -564,7 +569,7 @@ export default class StreamingBitmapIndexBuilder {
564
569
  * @private
565
570
  */
566
571
  async _loadAndValidateChunk(oid) {
567
- const buffer = await this.storage.readBlob(oid);
572
+ const buffer = await /** @type {any} */ (this.storage).readBlob(oid); // TODO(ts-cleanup): narrow port type
568
573
  let envelope;
569
574
  try {
570
575
  envelope = JSON.parse(buffer.toString('utf-8'));
@@ -572,14 +577,13 @@ export default class StreamingBitmapIndexBuilder {
572
577
  throw new ShardCorruptionError('Failed to parse shard JSON', {
573
578
  oid,
574
579
  reason: 'invalid_format',
575
- originalError: err.message,
580
+ context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
576
581
  });
577
582
  }
578
583
 
579
584
  // Validate version
580
585
  if (envelope.version !== SHARD_VERSION) {
581
586
  throw new ShardValidationError('Shard version mismatch', {
582
- oid,
583
587
  expected: SHARD_VERSION,
584
588
  actual: envelope.version,
585
589
  field: 'version',
@@ -610,7 +614,7 @@ export default class StreamingBitmapIndexBuilder {
610
614
  * it using `orInPlace` to combine edge sets.
611
615
  *
612
616
  * @param {Object} opts - Options object
613
- * @param {Object<string, RoaringBitmap32>} opts.merged - Object mapping SHA to
617
+ * @param {Record<string, any>} opts.merged - Object mapping SHA to
614
618
  * RoaringBitmap32 instances (mutated in place)
615
619
  * @param {string} opts.sha - The SHA key for this bitmap (40-character hex string)
616
620
  * @param {string} opts.base64Bitmap - Base64-encoded serialized RoaringBitmap32 data
@@ -627,7 +631,7 @@ export default class StreamingBitmapIndexBuilder {
627
631
  throw new ShardCorruptionError('Failed to deserialize bitmap', {
628
632
  oid,
629
633
  reason: 'invalid_bitmap',
630
- originalError: err.message,
634
+ context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
631
635
  });
632
636
  }
633
637
 
@@ -671,6 +675,7 @@ export default class StreamingBitmapIndexBuilder {
671
675
  */
672
676
  async _mergeChunks(oids, { signal } = {}) {
673
677
  // Load all chunks and merge bitmaps by SHA
678
+ /** @type {Record<string, any>} */
674
679
  const merged = {};
675
680
 
676
681
  for (const oid of oids) {
@@ -683,6 +688,7 @@ export default class StreamingBitmapIndexBuilder {
683
688
  }
684
689
 
685
690
  // Serialize merged result
691
+ /** @type {Record<string, string>} */
686
692
  const result = {};
687
693
  for (const [sha, bitmap] of Object.entries(merged)) {
688
694
  result[sha] = bitmap.serialize(true).toString('base64');
@@ -701,9 +707,9 @@ export default class StreamingBitmapIndexBuilder {
701
707
  } catch (err) {
702
708
  throw new ShardCorruptionError('Failed to serialize merged shard', {
703
709
  reason: 'serialization_error',
704
- originalError: err.message,
710
+ context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
705
711
  });
706
712
  }
707
- return this.storage.writeBlob(serialized);
713
+ return /** @type {any} */ (this.storage).writeBlob(serialized); // TODO(ts-cleanup): narrow port type
708
714
  }
709
715
  }
@@ -56,18 +56,16 @@ import { vvDeserialize } from '../crdt/VersionVector.js';
56
56
  * **Mutation**: This function mutates the input patch object for efficiency.
57
57
  * The original object reference is returned.
58
58
  *
59
- * @param {Object} patch - The raw decoded patch from CBOR
60
- * @param {Object|Map} [patch.context] - The causal context (version vector).
61
- * If present as a plain object, will be converted to a Map.
62
- * @param {Array} patch.ops - The patch operations (not modified)
63
- * @returns {Object} The same patch object with context converted to Map
59
+ * @param {{ context?: Object | Map<any, any>, ops: any[] }} patch - The raw decoded patch from CBOR.
60
+ * If context is present as a plain object, it will be converted to a Map.
61
+ * @returns {{ context?: Object | Map<any, any>, ops: any[] }} The same patch object with context converted to Map
64
62
  * @private
65
63
  */
66
64
  function normalizePatch(patch) {
67
65
  // Convert context from plain object to Map (VersionVector)
68
66
  // CBOR deserialization returns plain objects, but join() expects a Map
69
67
  if (patch.context && !(patch.context instanceof Map)) {
70
- patch.context = vvDeserialize(patch.context);
68
+ patch.context = vvDeserialize(/** @type {{ [x: string]: number }} */ (patch.context));
71
69
  }
72
70
  return patch;
73
71
  }
@@ -85,12 +83,12 @@ function normalizePatch(patch) {
85
83
  * **Commit message format**: The message is encoded using WarpMessageCodec
86
84
  * and contains metadata (schema version, writer info) plus the patch OID.
87
85
  *
88
- * @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence layer
86
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence layer
89
87
  * (uses CommitPort.showNode() + BlobPort.readBlob() methods)
90
88
  * @param {string} sha - The 40-character commit SHA to load the patch from
91
89
  * @param {Object} [options]
92
90
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
93
- * @returns {Promise<Object>} The decoded and normalized patch object containing:
91
+ * @returns {Promise<{ context?: Object | Map<any, any>, ops: any[] }>} The decoded and normalized patch object containing:
94
92
  * - `ops`: Array of patch operations
95
93
  * - `context`: VersionVector (Map) of causal dependencies
96
94
  * - `writerId`: The writer who created this patch
@@ -101,7 +99,7 @@ function normalizePatch(patch) {
101
99
  * @throws {Error} If the patch blob cannot be CBOR-decoded (corrupted data)
102
100
  * @private
103
101
  */
104
- async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
102
+ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
105
103
  const codec = codecOpt || defaultCodec;
106
104
  // Read commit message to extract patch OID
107
105
  const message = await persistence.showNode(sha);
@@ -109,7 +107,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
109
107
 
110
108
  // Read and decode the patch blob
111
109
  const patchBuffer = await persistence.readBlob(decoded.patchOid);
112
- const patch = codec.decode(patchBuffer);
110
+ const patch = /** @type {{ context?: Object | Map<any, any>, ops: any[] }} */ (codec.decode(patchBuffer));
113
111
 
114
112
  // Normalize the patch (convert context from object to Map)
115
113
  return normalizePatch(patch);
@@ -129,7 +127,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
129
127
  * **Performance**: O(N) where N is the number of commits between fromSha and toSha.
130
128
  * Each commit requires two reads: commit info (for parent) and patch blob.
131
129
  *
132
- * @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence layer
130
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence layer
133
131
  * (uses CommitPort.getNodeInfo()/showNode() + BlobPort.readBlob() methods)
134
132
  * @param {string} graphName - Graph name (used in error messages, not for lookups)
135
133
  * @param {string} writerId - Writer ID (used in error messages, not for lookups)
@@ -154,7 +152,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
154
152
  * // Load ALL patches for a new writer
155
153
  * const patches = await loadPatchRange(persistence, 'events', 'new-writer', null, tipSha);
156
154
  */
157
- export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = {}) {
155
+ export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
158
156
  const patches = [];
159
157
  let cur = toSha;
160
158
 
@@ -172,9 +170,9 @@ export async function loadPatchRange(persistence, graphName, writerId, fromSha,
172
170
 
173
171
  // If fromSha was specified but we didn't reach it, we have divergence
174
172
  if (fromSha && cur === null) {
175
- const err = new Error(
173
+ const err = /** @type {Error & { code: string }} */ (new Error(
176
174
  `Divergence detected: ${toSha} does not descend from ${fromSha} for writer ${writerId}`
177
- );
175
+ ));
178
176
  err.code = 'E_SYNC_DIVERGENCE';
179
177
  throw err;
180
178
  }
@@ -214,11 +212,7 @@ export async function loadPatchRange(persistence, graphName, writerId, fromSha,
214
212
  * Maps writerId to the SHA of their latest patch commit.
215
213
  * @param {Map<string, string>} remoteFrontier - Remote writer heads.
216
214
  * Maps writerId to the SHA of their latest patch commit.
217
- * @returns {Object} Sync delta containing:
218
- * - `needFromRemote`: Map<writerId, {from: string|null, to: string}> - Patches local needs
219
- * - `needFromLocal`: Map<writerId, {from: string|null, to: string}> - Patches remote needs
220
- * - `newWritersForLocal`: string[] - Writers that local has never seen
221
- * - `newWritersForRemote`: string[] - Writers that remote has never seen
215
+ * @returns {{ needFromRemote: Map<string, {from: string|null, to: string}>, needFromLocal: Map<string, {from: string|null, to: string}>, newWritersForLocal: string[], newWritersForRemote: string[] }} Sync delta
222
216
  *
223
217
  * @example
224
218
  * const local = new Map([['w1', 'sha-a'], ['w2', 'sha-b']]);
@@ -333,13 +327,14 @@ export function computeSyncDelta(localFrontier, remoteFrontier) {
333
327
  */
334
328
  export function createSyncRequest(frontier) {
335
329
  // Convert Map to plain object for serialization
330
+ /** @type {{ [x: string]: string }} */
336
331
  const frontierObj = {};
337
332
  for (const [writerId, sha] of frontier) {
338
333
  frontierObj[writerId] = sha;
339
334
  }
340
335
 
341
336
  return {
342
- type: 'sync-request',
337
+ type: /** @type {'sync-request'} */ ('sync-request'),
343
338
  frontier: frontierObj,
344
339
  };
345
340
  }
@@ -363,7 +358,7 @@ export function createSyncRequest(frontier) {
363
358
  *
364
359
  * @param {SyncRequest} request - Incoming sync request containing the requester's frontier
365
360
  * @param {Map<string, string>} localFrontier - Local frontier (what this node has)
366
- * @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence
361
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence
367
362
  * layer for loading patches (uses CommitPort + BlobPort methods)
368
363
  * @param {string} graphName - Graph name for error messages and logging
369
364
  * @returns {Promise<SyncResponse>} Response containing local frontier and patches.
@@ -379,7 +374,7 @@ export function createSyncRequest(frontier) {
379
374
  * res.json(response);
380
375
  * });
381
376
  */
382
- export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = {}) {
377
+ export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
383
378
  // Convert incoming frontier from object to Map
384
379
  const remoteFrontier = new Map(Object.entries(request.frontier));
385
380
 
@@ -406,7 +401,7 @@ export async function processSyncRequest(request, localFrontier, persistence, gr
406
401
  } catch (err) {
407
402
  // If we detect divergence, skip this writer
408
403
  // The requester may need to handle this separately
409
- if (err.code === 'E_SYNC_DIVERGENCE' || err.message.includes('Divergence detected')) {
404
+ if (/** @type {any} */ (err).code === 'E_SYNC_DIVERGENCE' || /** @type {any} */ (err).message?.includes('Divergence detected')) { // TODO(ts-cleanup): type error
410
405
  continue;
411
406
  }
412
407
  throw err;
@@ -414,13 +409,14 @@ export async function processSyncRequest(request, localFrontier, persistence, gr
414
409
  }
415
410
 
416
411
  // Convert local frontier to plain object
412
+ /** @type {{ [x: string]: string }} */
417
413
  const frontierObj = {};
418
414
  for (const [writerId, sha] of localFrontier) {
419
415
  frontierObj[writerId] = sha;
420
416
  }
421
417
 
422
418
  return {
423
- type: 'sync-response',
419
+ type: /** @type {'sync-response'} */ ('sync-response'),
424
420
  frontier: frontierObj,
425
421
  patches,
426
422
  };
@@ -495,7 +491,7 @@ export function applySyncResponse(response, state, frontier) {
495
491
  // will prevent silent data loss until the reader is upgraded.
496
492
  assertOpsCompatible(normalizedPatch.ops, SCHEMA_V3);
497
493
  // Apply patch to state
498
- join(newState, normalizedPatch, sha);
494
+ join(newState, /** @type {*} */ (normalizedPatch), sha); // TODO(ts-cleanup): type patch array
499
495
  applied++;
500
496
  }
501
497
 
@@ -580,13 +576,14 @@ export function syncNeeded(localFrontier, remoteFrontier) {
580
576
  * }
581
577
  */
582
578
  export function createEmptySyncResponse(frontier) {
579
+ /** @type {{ [x: string]: string }} */
583
580
  const frontierObj = {};
584
581
  for (const [writerId, sha] of frontier) {
585
582
  frontierObj[writerId] = sha;
586
583
  }
587
584
 
588
585
  return {
589
- type: 'sync-response',
586
+ type: /** @type {'sync-response'} */ ('sync-response'),
590
587
  frontier: frontierObj,
591
588
  patches: [],
592
589
  };
@@ -60,10 +60,11 @@ function unwrapValue(value) {
60
60
  *
61
61
  * @param {import('./JoinReducer.js').WarpStateV5} state - Current state
62
62
  * @param {string} nodeId - Node ID to extract
63
- * @returns {{ id: string, exists: boolean, props: Object<string, *> }}
63
+ * @returns {{ id: string, exists: boolean, props: Record<string, *> }}
64
64
  */
65
65
  function extractNodeSnapshot(state, nodeId) {
66
66
  const exists = orsetContains(state.nodeAlive, nodeId);
67
+ /** @type {Record<string, *>} */
67
68
  const props = {};
68
69
 
69
70
  if (exists) {
@@ -108,7 +109,7 @@ export class TemporalQuery {
108
109
  * @param {string} nodeId - The node ID to evaluate
109
110
  * @param {Function} predicate - Predicate receiving node snapshot
110
111
  * `{ id, exists, props }`. Should return boolean.
111
- * @param {{ since?: number }} [options={}] - Options
112
+ * @param {Object} [options={}] - Options
112
113
  * @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
113
114
  * Only patches with lamport >= since are considered.
114
115
  * @returns {Promise<boolean>} True if predicate held at every tick
@@ -161,7 +162,7 @@ export class TemporalQuery {
161
162
  * @param {string} nodeId - The node ID to evaluate
162
163
  * @param {Function} predicate - Predicate receiving node snapshot
163
164
  * `{ id, exists, props }`. Should return boolean.
164
- * @param {{ since?: number }} [options={}] - Options
165
+ * @param {Object} [options={}] - Options
165
166
  * @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
166
167
  * Only patches with lamport >= since are considered.
167
168
  * @returns {Promise<boolean>} True if predicate held at any tick
@@ -94,8 +94,8 @@ function zeroCost() {
94
94
  /**
95
95
  * Counts how many items in `source` are absent from `targetSet`.
96
96
  *
97
- * @param {Array|Set} source - Source collection
98
- * @param {Set} targetSet - Target set to test against
97
+ * @param {Array<string>|Set<string>} source - Source collection
98
+ * @param {Set<string>} targetSet - Target set to test against
99
99
  * @returns {number}
100
100
  */
101
101
  function countMissing(source, targetSet) {
@@ -141,7 +141,7 @@ function computeEdgeLoss(state, nodesASet, nodesBSet) {
141
141
  * Counts lost properties for a single node between two observer configs.
142
142
  *
143
143
  * @param {Map<string, boolean>} nodeProps - Property keys for the node
144
- * @param {{ configA: Object, configB: Object, nodeInB: boolean }} opts
144
+ * @param {{ configA: {expose?: string[], redact?: string[]}, configB: {expose?: string[], redact?: string[]}, nodeInB: boolean }} opts
145
145
  * @returns {{ propsInA: number, lostProps: number }}
146
146
  */
147
147
  function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
@@ -157,7 +157,7 @@ function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
157
157
  * Computes property loss across all A-visible nodes.
158
158
  *
159
159
  * @param {*} state - WarpStateV5
160
- * @param {{ nodesA: string[], nodesBSet: Set<string>, configA: Object, configB: Object }} opts
160
+ * @param {{ nodesA: string[], nodesBSet: Set<string>, configA: {expose?: string[], redact?: string[]}, configB: {expose?: string[], redact?: string[]} }} opts
161
161
  * @returns {number} propLoss fraction
162
162
  */
163
163
  function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
@@ -43,7 +43,7 @@ function validateSha(sha, paramName) {
43
43
 
44
44
  /**
45
45
  * Verifies that a SHA exists in the repository.
46
- * @param {Object} persistence - Git persistence adapter
46
+ * @param {{ nodeExists: (sha: string) => Promise<boolean> }} persistence - Git persistence adapter
47
47
  * @param {string} sha - The SHA to verify
48
48
  * @param {string} paramName - Parameter name for error messages
49
49
  * @throws {WormholeError} If SHA doesn't exist
@@ -62,10 +62,11 @@ async function verifyShaExists(persistence, sha, paramName) {
62
62
  /**
63
63
  * Processes a single commit in the wormhole chain.
64
64
  * @param {Object} opts - Options
65
- * @param {Object} opts.persistence - Git persistence adapter
65
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} opts.persistence - Git persistence adapter
66
66
  * @param {string} opts.sha - The commit SHA
67
67
  * @param {string} opts.graphName - Expected graph name
68
68
  * @param {string|null} opts.expectedWriter - Expected writer ID (null for first commit)
69
+ * @param {import('../../ports/CodecPort.js').default} [opts.codec] - Codec for deserialization
69
70
  * @returns {Promise<{patch: Object, sha: string, writerId: string, parentSha: string|null}>}
70
71
  * @throws {WormholeError} On validation errors
71
72
  * @private
@@ -100,7 +101,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
100
101
  }
101
102
 
102
103
  const patchBuffer = await persistence.readBlob(patchMeta.patchOid);
103
- const patch = codec.decode(patchBuffer);
104
+ const patch = /** @type {Object} */ (codec.decode(patchBuffer));
104
105
 
105
106
  return {
106
107
  patch,
@@ -135,10 +136,11 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
135
136
  * are inclusive in the wormhole.
136
137
  *
137
138
  * @param {Object} options - Wormhole creation options
138
- * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git persistence adapter
139
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
139
140
  * @param {string} options.graphName - Name of the graph
140
141
  * @param {string} options.fromSha - SHA of the first (oldest) patch commit
141
142
  * @param {string} options.toSha - SHA of the last (newest) patch commit
143
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
142
144
  * @returns {Promise<WormholeEdge>} The created wormhole
143
145
  * @throws {WormholeError} If fromSha or toSha doesn't exist (E_WORMHOLE_SHA_NOT_FOUND)
144
146
  * @throws {WormholeError} If fromSha is not an ancestor of toSha (E_WORMHOLE_INVALID_RANGE)
@@ -156,7 +158,7 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
156
158
  // Reverse to get oldest-first order (as required by ProvenancePayload)
157
159
  patches.reverse();
158
160
 
159
- const writerId = patches.length > 0 ? patches[0].writerId : null;
161
+ const writerId = patches.length > 0 ? patches[0].writerId : /** @type {string} */ ('');
160
162
  // Strip writerId to match ProvenancePayload's PatchEntry typedef ({patch, sha})
161
163
  const payload = new ProvenancePayload(patches.map(({ patch, sha }) => ({ patch, sha })));
162
164
 
@@ -170,10 +172,11 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
170
172
  * validating each commit along the way.
171
173
  *
172
174
  * @param {Object} options
173
- * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git persistence adapter
175
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
174
176
  * @param {string} options.graphName - Expected graph name
175
177
  * @param {string} options.fromSha - SHA of the first (oldest) patch commit
176
178
  * @param {string} options.toSha - SHA of the last (newest) patch commit
179
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
177
180
  * @returns {Promise<Array<{patch: Object, sha: string, writerId: string}>>} Patches in newest-first order
178
181
  * @throws {WormholeError} If fromSha is not an ancestor of toSha or range is empty
179
182
  * @private
@@ -230,7 +233,7 @@ async function collectPatchRange({ persistence, graphName, fromSha, toSha, codec
230
233
  * @param {WormholeEdge} first - The earlier (older) wormhole
231
234
  * @param {WormholeEdge} second - The later (newer) wormhole
232
235
  * @param {Object} [options] - Composition options
233
- * @param {import('../../ports/GraphPersistencePort.js').default} [options.persistence] - Git persistence adapter (for validation)
236
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} [options.persistence] - Git persistence adapter (for validation)
234
237
  * @returns {Promise<WormholeEdge>} The composed wormhole
235
238
  * @throws {WormholeError} If wormholes are from different writers (E_WORMHOLE_MULTI_WRITER)
236
239
  * @throws {WormholeError} If wormholes are not consecutive (E_WORMHOLE_INVALID_RANGE)
@@ -318,9 +321,10 @@ export function deserializeWormhole(json) {
318
321
  });
319
322
  }
320
323
 
324
+ const /** @type {Record<string, *>} */ typedJson = /** @type {Record<string, *>} */ (json);
321
325
  const requiredFields = ['fromSha', 'toSha', 'writerId', 'patchCount', 'payload'];
322
326
  for (const field of requiredFields) {
323
- if (json[field] === undefined) {
327
+ if (typedJson[field] === undefined) {
324
328
  throw new WormholeError(`Invalid wormhole JSON: missing required field '${field}'`, {
325
329
  code: 'E_INVALID_WORMHOLE_JSON',
326
330
  context: { missingField: field },
@@ -328,19 +332,19 @@ export function deserializeWormhole(json) {
328
332
  }
329
333
  }
330
334
 
331
- if (typeof json.patchCount !== 'number' || json.patchCount < 0) {
335
+ if (typeof typedJson.patchCount !== 'number' || typedJson.patchCount < 0) {
332
336
  throw new WormholeError('Invalid wormhole JSON: patchCount must be a non-negative number', {
333
337
  code: 'E_INVALID_WORMHOLE_JSON',
334
- context: { patchCount: json.patchCount },
338
+ context: { patchCount: typedJson.patchCount },
335
339
  });
336
340
  }
337
341
 
338
342
  return {
339
- fromSha: json.fromSha,
340
- toSha: json.toSha,
341
- writerId: json.writerId,
342
- patchCount: json.patchCount,
343
- payload: ProvenancePayload.fromJSON(json.payload),
343
+ fromSha: typedJson.fromSha,
344
+ toSha: typedJson.toSha,
345
+ writerId: typedJson.writerId,
346
+ patchCount: typedJson.patchCount,
347
+ payload: ProvenancePayload.fromJSON(typedJson.payload),
344
348
  };
345
349
  }
346
350
 
@@ -67,11 +67,12 @@ function validateOp(op, index) {
67
67
  throw new Error(`ops[${index}] must be an object`);
68
68
  }
69
69
 
70
- validateOpType(op.op, index);
71
- validateOpTarget(op.target, index);
72
- validateOpResult(op.result, index);
70
+ const entry = /** @type {Record<string, *>} */ (op);
71
+ validateOpType(entry.op, index);
72
+ validateOpTarget(entry.target, index);
73
+ validateOpResult(entry.result, index);
73
74
 
74
- if (op.reason !== undefined && typeof op.reason !== 'string') {
75
+ if (entry.reason !== undefined && typeof entry.reason !== 'string') {
75
76
  throw new Error(`ops[${index}].reason must be a string or undefined`);
76
77
  }
77
78
  }
@@ -208,6 +209,7 @@ export function createTickReceipt({ patchSha, writer, lamport, ops }) {
208
209
  // Build frozen op copies (defensive: don't alias caller's objects)
209
210
  const frozenOps = Object.freeze(
210
211
  ops.map((o) => {
212
+ /** @type {{ op: string, target: string, result: 'applied' | 'superseded' | 'redundant', reason?: string }} */
211
213
  const entry = { op: o.op, target: o.target, result: o.result };
212
214
  if (o.reason !== undefined) {
213
215
  entry.reason = o.reason;
@@ -275,9 +277,11 @@ export function canonicalJson(receipt) {
275
277
  */
276
278
  function sortedReplacer(_key, value) {
277
279
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
280
+ /** @type {{ [x: string]: * }} */
278
281
  const sorted = {};
279
- for (const k of Object.keys(value).sort()) {
280
- sorted[k] = value[k];
282
+ const obj = /** @type {{ [x: string]: * }} */ (value);
283
+ for (const k of Object.keys(obj).sort()) {
284
+ sorted[k] = obj[k];
281
285
  }
282
286
  return sorted;
283
287
  }