@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
@@ -40,17 +40,17 @@ export default class IndexRebuildService {
40
40
  * Creates an IndexRebuildService instance.
41
41
  *
42
42
  * @param {Object} options - Configuration options
43
- * @param {Object} options.graphService - Graph service providing node iteration.
44
- * Must implement `iterateNodes({ ref, limit }) => AsyncGenerator<GraphNode>`.
43
+ * @param {{ iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }} options.graphService - Graph service providing node iteration.
45
44
  * @param {import('../../ports/IndexStoragePort.js').default} options.storage - Storage adapter
46
45
  * for persisting index blobs and trees. Typically GitGraphAdapter.
47
46
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for
48
47
  * structured logging. Defaults to null logger (no logging).
48
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
49
49
  * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for checksums
50
50
  * @throws {Error} If graphService is not provided
51
51
  * @throws {Error} If storage adapter is not provided
52
52
  */
53
- constructor({ graphService, storage, logger = nullLogger, codec, crypto }) {
53
+ constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
54
54
  if (!graphService) {
55
55
  throw new Error('IndexRebuildService requires a graphService');
56
56
  }
@@ -156,7 +156,7 @@ export default class IndexRebuildService {
156
156
  operation: 'rebuild',
157
157
  ref,
158
158
  mode,
159
- error: err.message,
159
+ error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
160
160
  durationMs,
161
161
  });
162
162
  throw err;
@@ -247,12 +247,12 @@ export default class IndexRebuildService {
247
247
  * @private
248
248
  */
249
249
  async _rebuildStreaming(ref, { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier }) {
250
- const builder = new StreamingBitmapIndexBuilder({
250
+ const builder = new StreamingBitmapIndexBuilder(/** @type {*} */ ({ // TODO(ts-cleanup): narrow port type
251
251
  storage: this.storage,
252
252
  maxMemoryBytes,
253
253
  onFlush,
254
254
  crypto: this._crypto,
255
- });
255
+ }));
256
256
 
257
257
  let processedNodes = 0;
258
258
 
@@ -266,7 +266,7 @@ export default class IndexRebuildService {
266
266
  if (processedNodes % 10000 === 0) {
267
267
  checkAborted(signal, 'rebuild');
268
268
  if (onProgress) {
269
- const stats = builder.getMemoryStats();
269
+ const stats = /** @type {any} */ (builder).getMemoryStats(); // TODO(ts-cleanup): narrow port type
270
270
  onProgress({
271
271
  processedNodes,
272
272
  currentMemoryBytes: stats.estimatedBitmapBytes,
@@ -275,7 +275,7 @@ export default class IndexRebuildService {
275
275
  }
276
276
  }
277
277
 
278
- return await builder.finalize({ signal, frontier });
278
+ return await /** @type {any} */ (builder).finalize({ signal, frontier }); // TODO(ts-cleanup): narrow port type
279
279
  }
280
280
 
281
281
  /**
@@ -302,10 +302,10 @@ export default class IndexRebuildService {
302
302
  const treeStructure = await builder.serialize({ frontier });
303
303
  const flatEntries = [];
304
304
  for (const [path, buffer] of Object.entries(treeStructure)) {
305
- const oid = await this.storage.writeBlob(buffer);
305
+ const oid = await /** @type {import('../../ports/BlobPort.js').default} */ (/** @type {unknown} */ (this.storage)).writeBlob(buffer);
306
306
  flatEntries.push(`100644 blob ${oid}\t${path}`);
307
307
  }
308
- return await this.storage.writeTree(flatEntries);
308
+ return await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).writeTree(flatEntries);
309
309
  }
310
310
 
311
311
  /**
@@ -384,12 +384,12 @@ export default class IndexRebuildService {
384
384
  }
385
385
 
386
386
  const startTime = performance.now();
387
- const shardOids = await this.storage.readTreeOids(treeOid);
387
+ const shardOids = await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).readTreeOids(treeOid);
388
388
  const shardCount = Object.keys(shardOids).length;
389
389
 
390
390
  // Staleness check
391
391
  if (currentFrontier) {
392
- const indexFrontier = await loadIndexFrontier(shardOids, this.storage, { codec: this._codec });
392
+ const indexFrontier = await loadIndexFrontier(shardOids, /** @type {*} */ (this.storage), { codec: this._codec }); // TODO(ts-cleanup): narrow port type
393
393
  if (indexFrontier) {
394
394
  const result = checkStaleness(indexFrontier, currentFrontier);
395
395
  if (result.stale) {
@@ -5,7 +5,11 @@
5
5
 
6
6
  import defaultCodec from '../utils/defaultCodec.js';
7
7
 
8
- /** @private */
8
+ /**
9
+ * @param {*} envelope
10
+ * @param {string} label
11
+ * @private
12
+ */
9
13
  function validateEnvelope(envelope, label) {
10
14
  if (!envelope || typeof envelope !== 'object' || !envelope.frontier || typeof envelope.frontier !== 'object') {
11
15
  throw new Error(`invalid frontier envelope for ${label}`);
@@ -16,17 +20,17 @@ function validateEnvelope(envelope, label) {
16
20
  * Loads the frontier from an index tree's shard OIDs.
17
21
  *
18
22
  * @param {Record<string, string>} shardOids - Map of path → blob OID from readTreeOids
19
- * @param {import('../../ports/IndexStoragePort.js').default} storage - Storage adapter
23
+ * @param {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} storage - Storage adapter
20
24
  * @param {Object} [options]
21
25
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
22
26
  * @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
23
27
  */
24
- export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
28
+ export async function loadIndexFrontier(shardOids, storage, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
25
29
  const c = codec || defaultCodec;
26
30
  const cborOid = shardOids['frontier.cbor'];
27
31
  if (cborOid) {
28
32
  const buffer = await storage.readBlob(cborOid);
29
- const envelope = c.decode(buffer);
33
+ const envelope = /** @type {{ frontier: Record<string, string> }} */ (c.decode(buffer));
30
34
  validateEnvelope(envelope, 'frontier.cbor');
31
35
  return new Map(Object.entries(envelope.frontier));
32
36
  }
@@ -34,7 +38,7 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
34
38
  const jsonOid = shardOids['frontier.json'];
35
39
  if (jsonOid) {
36
40
  const buffer = await storage.readBlob(jsonOid);
37
- const envelope = JSON.parse(buffer.toString('utf-8'));
41
+ const envelope = /** @type {{ frontier: Record<string, string> }} */ (JSON.parse(buffer.toString('utf-8')));
38
42
  validateEnvelope(envelope, 'frontier.json');
39
43
  return new Map(Object.entries(envelope.frontier));
40
44
  }
@@ -51,7 +55,10 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
51
55
  * @property {string[]} removedWriters - Writers in index but not current
52
56
  */
53
57
 
54
- /** @private */
58
+ /**
59
+ * @param {{ stale: boolean, advancedWriters: string[], newWriters: string[], removedWriters: string[] }} opts
60
+ * @private
61
+ */
55
62
  function buildReason({ stale, advancedWriters, newWriters, removedWriters }) {
56
63
  if (!stale) {
57
64
  return 'index is current';
@@ -29,7 +29,7 @@ export {
29
29
  * @typedef {Object} WarpStateV5
30
30
  * @property {import('../crdt/ORSet.js').ORSet} nodeAlive - ORSet of alive nodes
31
31
  * @property {import('../crdt/ORSet.js').ORSet} edgeAlive - ORSet of alive edges
32
- * @property {Map<string, import('../crdt/LWW.js').LWWRegister>} prop - Properties with LWW
32
+ * @property {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} prop - Properties with LWW
33
33
  * @property {import('../crdt/VersionVector.js').VersionVector} observedFrontier - Observed version vector
34
34
  * @property {Map<string, import('../utils/EventId.js').EventId>} edgeBirthEvent - EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility)
35
35
  */
@@ -88,14 +88,14 @@ export function createEmptyStateV5() {
88
88
  export function applyOpV2(state, op, eventId) {
89
89
  switch (op.type) {
90
90
  case 'NodeAdd':
91
- orsetAdd(state.nodeAlive, op.node, op.dot);
91
+ orsetAdd(state.nodeAlive, /** @type {string} */ (op.node), /** @type {import('../crdt/Dot.js').Dot} */ (op.dot));
92
92
  break;
93
93
  case 'NodeRemove':
94
- orsetRemove(state.nodeAlive, op.observedDots);
94
+ orsetRemove(state.nodeAlive, /** @type {Set<string>} */ (/** @type {unknown} */ (op.observedDots)));
95
95
  break;
96
96
  case 'EdgeAdd': {
97
- const edgeKey = encodeEdgeKey(op.from, op.to, op.label);
98
- orsetAdd(state.edgeAlive, edgeKey, op.dot);
97
+ const edgeKey = encodeEdgeKey(/** @type {string} */ (op.from), /** @type {string} */ (op.to), /** @type {string} */ (op.label));
98
+ orsetAdd(state.edgeAlive, edgeKey, /** @type {import('../crdt/Dot.js').Dot} */ (op.dot));
99
99
  // Track the EventId at which this edge incarnation was born.
100
100
  // On re-add after remove, the greater EventId replaces the old one,
101
101
  // allowing the query layer to filter out stale properties.
@@ -108,13 +108,13 @@ export function applyOpV2(state, op, eventId) {
108
108
  break;
109
109
  }
110
110
  case 'EdgeRemove':
111
- orsetRemove(state.edgeAlive, op.observedDots);
111
+ orsetRemove(state.edgeAlive, /** @type {Set<string>} */ (/** @type {unknown} */ (op.observedDots)));
112
112
  break;
113
113
  case 'PropSet': {
114
114
  // Uses EventId-based LWW, same as v4
115
- const key = encodePropKey(op.node, op.key);
115
+ const key = encodePropKey(/** @type {string} */ (op.node), /** @type {string} */ (op.key));
116
116
  const current = state.prop.get(key);
117
- state.prop.set(key, lwwMax(current, lwwSet(eventId, op.value)));
117
+ state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(current, lwwSet(eventId, op.value))));
118
118
  break;
119
119
  }
120
120
  default:
@@ -290,7 +290,7 @@ function edgeRemoveOutcome(orset, op) {
290
290
  * - `superseded`: An existing value with higher EventId wins
291
291
  * - `redundant`: Exact same write (identical EventId)
292
292
  *
293
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister>} propMap - The properties map keyed by encoded prop keys
293
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} propMap - The properties map keyed by encoded prop keys
294
294
  * @param {Object} op - The PropSet operation
295
295
  * @param {string} op.node - Node ID owning the property
296
296
  * @param {string} op.key - Property key/name
@@ -347,8 +347,8 @@ function propSetOutcome(propMap, op, eventId) {
347
347
  * @param {Object} patch - The patch to apply
348
348
  * @param {string} patch.writer - Writer ID who created this patch
349
349
  * @param {number} patch.lamport - Lamport timestamp of this patch
350
- * @param {Object[]} patch.ops - Array of operations to apply
351
- * @param {Map|Object} patch.context - Version vector context (Map or serialized form)
350
+ * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>} patch.ops - Array of operations to apply
351
+ * @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
352
352
  * @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
353
353
  * @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
354
354
  * @returns {WarpStateV5|{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}}
@@ -370,30 +370,32 @@ export function join(state, patch, patchSha, collectReceipts) {
370
370
  }
371
371
 
372
372
  // Receipt-enabled path
373
+ /** @type {import('../types/TickReceipt.js').OpOutcome[]} */
373
374
  const opResults = [];
374
375
  for (let i = 0; i < patch.ops.length; i++) {
375
376
  const op = patch.ops[i];
376
377
  const eventId = createEventId(patch.lamport, patch.writer, patchSha, i);
377
378
 
378
379
  // Determine outcome BEFORE applying the op (state is pre-op)
380
+ /** @type {{target: string, result: string, reason?: string}} */
379
381
  let outcome;
380
382
  switch (op.type) {
381
383
  case 'NodeAdd':
382
- outcome = nodeAddOutcome(state.nodeAlive, op);
384
+ outcome = nodeAddOutcome(state.nodeAlive, /** @type {{node: string, dot: import('../crdt/Dot.js').Dot}} */ (op));
383
385
  break;
384
386
  case 'NodeRemove':
385
- outcome = nodeRemoveOutcome(state.nodeAlive, op);
387
+ outcome = nodeRemoveOutcome(state.nodeAlive, /** @type {{node?: string, observedDots: string[]}} */ (op));
386
388
  break;
387
389
  case 'EdgeAdd': {
388
- const edgeKey = encodeEdgeKey(op.from, op.to, op.label);
389
- outcome = edgeAddOutcome(state.edgeAlive, op, edgeKey);
390
+ const edgeKey = encodeEdgeKey(/** @type {string} */ (op.from), /** @type {string} */ (op.to), /** @type {string} */ (op.label));
391
+ outcome = edgeAddOutcome(state.edgeAlive, /** @type {{from: string, to: string, label: string, dot: import('../crdt/Dot.js').Dot}} */ (op), edgeKey);
390
392
  break;
391
393
  }
392
394
  case 'EdgeRemove':
393
- outcome = edgeRemoveOutcome(state.edgeAlive, op);
395
+ outcome = edgeRemoveOutcome(state.edgeAlive, /** @type {{from?: string, to?: string, label?: string, observedDots: string[]}} */ (op));
394
396
  break;
395
397
  case 'PropSet':
396
- outcome = propSetOutcome(state.prop, op, eventId);
398
+ outcome = propSetOutcome(state.prop, /** @type {{node: string, key: string, value: *}} */ (op), eventId);
397
399
  break;
398
400
  default:
399
401
  // Unknown or BlobValue — always applied
@@ -404,12 +406,13 @@ export function join(state, patch, patchSha, collectReceipts) {
404
406
  // Apply the op (mutates state)
405
407
  applyOpV2(state, op, eventId);
406
408
 
407
- const receiptOp = RECEIPT_OP_TYPE[op.type] || op.type;
409
+ const receiptOp = /** @type {Record<string, string>} */ (RECEIPT_OP_TYPE)[op.type] || op.type;
408
410
  // Skip unknown/forward-compatible op types that aren't valid receipt ops
409
411
  if (!VALID_RECEIPT_OPS.has(receiptOp)) {
410
412
  continue;
411
413
  }
412
- const entry = { op: receiptOp, target: outcome.target, result: outcome.result };
414
+ /** @type {import('../types/TickReceipt.js').OpOutcome} */
415
+ const entry = { op: receiptOp, target: outcome.target, result: /** @type {'applied'|'superseded'|'redundant'} */ (outcome.result) };
413
416
  if (outcome.reason) {
414
417
  entry.reason = outcome.reason;
415
418
  }
@@ -467,16 +470,16 @@ export function joinStates(a, b) {
467
470
  *
468
471
  * This is a pure function that does not mutate its inputs.
469
472
  *
470
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister>} a - First property map
471
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister>} b - Second property map
472
- * @returns {Map<string, import('../crdt/LWW.js').LWWRegister>} New map containing merged properties
473
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} a - First property map
474
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} b - Second property map
475
+ * @returns {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} New map containing merged properties
473
476
  */
474
477
  function mergeProps(a, b) {
475
478
  const result = new Map(a);
476
479
 
477
480
  for (const [key, regB] of b) {
478
481
  const regA = result.get(key);
479
- result.set(key, lwwMax(regA, regB));
482
+ result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(regA, regB)));
480
483
  }
481
484
 
482
485
  return result;
@@ -527,9 +530,7 @@ function mergeEdgeBirthEvent(a, b) {
527
530
  * - When `options.receipts` is true, returns a TickReceipt per patch for
528
531
  * provenance tracking and debugging.
529
532
  *
530
- * @param {Array<{patch: Object, sha: string}>} patches - Array of patch objects with their Git SHAs
531
- * @param {Object} patches[].patch - The decoded patch object (writer, lamport, ops, context)
532
- * @param {string} patches[].sha - The Git SHA of the patch commit
533
+ * @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
533
534
  * @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
534
535
  * @param {Object} [options] - Optional configuration
535
536
  * @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
@@ -544,7 +545,7 @@ export function reduceV5(patches, initialState, options) {
544
545
  if (options && options.receipts) {
545
546
  const receipts = [];
546
547
  for (const { patch, sha } of patches) {
547
- const result = join(state, patch, sha, true);
548
+ const result = /** @type {{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}} */ (join(state, patch, sha, true));
548
549
  receipts.push(result.receipt);
549
550
  }
550
551
  return { state, receipts };
@@ -145,14 +145,14 @@ export default class LogicalTraversal {
145
145
  * @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
146
146
  * @param {string|string[]} [options.labelFilter] - Edge label(s) to include
147
147
  * @param {number} [options.maxDepth] - Maximum depth to traverse
148
- * @returns {Promise<{dir: 'out'|'in'|'both', labelSet: Set<string>|null, adjacency: Object, depthLimit: number}>}
148
+ * @returns {Promise<{dir: 'out'|'in'|'both', labelSet: Set<string>|null, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}, depthLimit: number}>}
149
149
  * The normalized traversal parameters
150
150
  * @throws {TraversalError} If the start node is not found (NODE_NOT_FOUND)
151
151
  * @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
152
152
  * @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
153
153
  */
154
154
  async _prepare(start, { dir, labelFilter, maxDepth }) {
155
- const materialized = await this._graph._materializeGraph();
155
+ const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
156
156
 
157
157
  if (!(await this._graph.hasNode(start))) {
158
158
  throw new TraversalError(`Start node not found: ${start}`, {
@@ -187,7 +187,7 @@ export default class LogicalTraversal {
187
187
  const result = [];
188
188
 
189
189
  while (queue.length > 0) {
190
- const current = queue.shift();
190
+ const current = /** @type {{nodeId: string, depth: number}} */ (queue.shift());
191
191
  if (visited.has(current.nodeId)) {
192
192
  continue;
193
193
  }
@@ -237,7 +237,7 @@ export default class LogicalTraversal {
237
237
  const result = [];
238
238
 
239
239
  while (stack.length > 0) {
240
- const current = stack.pop();
240
+ const current = /** @type {{nodeId: string, depth: number}} */ (stack.pop());
241
241
  if (visited.has(current.nodeId)) {
242
242
  continue;
243
243
  }
@@ -298,7 +298,7 @@ export default class LogicalTraversal {
298
298
  visited.add(from);
299
299
 
300
300
  while (queue.length > 0) {
301
- const current = queue.shift();
301
+ const current = /** @type {{nodeId: string, depth: number}} */ (queue.shift());
302
302
  if (current.depth >= depthLimit) {
303
303
  continue;
304
304
  }
@@ -319,10 +319,11 @@ export default class LogicalTraversal {
319
319
 
320
320
  if (edge.neighborId === to) {
321
321
  const path = [to];
322
+ /** @type {string|undefined} */
322
323
  let cursor = current.nodeId;
323
324
  while (cursor) {
324
325
  path.push(cursor);
325
- cursor = parent.get(cursor) || null;
326
+ cursor = parent.get(cursor);
326
327
  }
327
328
  path.reverse();
328
329
  return { found: true, path, length: path.length - 1 };
@@ -12,6 +12,7 @@
12
12
  * @private
13
13
  */
14
14
 
15
+ // @ts-expect-error -- no declaration file for @git-stunts/trailer-codec
15
16
  import { TrailerCodec, TrailerCodecService } from '@git-stunts/trailer-codec';
16
17
 
17
18
  // -----------------------------------------------------------------------------
@@ -62,6 +63,7 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
62
63
  // -----------------------------------------------------------------------------
63
64
 
64
65
  // Lazy singleton codec instance
66
+ /** @type {*} */ // TODO(ts-cleanup): type lazy singleton
65
67
  let _codec = null;
66
68
 
67
69
  /**
@@ -102,7 +102,7 @@ export default class ObserverView {
102
102
  this._graph = graph;
103
103
 
104
104
  /** @type {LogicalTraversal} */
105
- this.traverse = new LogicalTraversal(this);
105
+ this.traverse = new LogicalTraversal(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
106
106
  }
107
107
 
108
108
  /**
@@ -124,11 +124,11 @@ export default class ObserverView {
124
124
  * Builds a filtered adjacency structure that only includes edges
125
125
  * where both endpoints pass the match filter.
126
126
  *
127
- * @returns {Promise<{state: *, stateHash: string, adjacency: {outgoing: Map, incoming: Map}}>}
127
+ * @returns {Promise<{state: *, stateHash: string, adjacency: {outgoing: Map<string, *[]>, incoming: Map<string, *[]>}}>}
128
128
  * @private
129
129
  */
130
130
  async _materializeGraph() {
131
- const materialized = await this._graph._materializeGraph();
131
+ const materialized = await /** @type {*} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
132
132
  const { state, stateHash } = materialized;
133
133
 
134
134
  // Build filtered adjacency: only edges where both endpoints match
@@ -159,8 +159,8 @@ export default class ObserverView {
159
159
  incoming.get(to).push({ neighborId: from, label });
160
160
  }
161
161
 
162
- const sortNeighbors = (list) => {
163
- list.sort((a, b) => {
162
+ const sortNeighbors = (/** @type {{ neighborId: string, label: string }[]} */ list) => {
163
+ list.sort((/** @type {{ neighborId: string, label: string }} */ a, /** @type {{ neighborId: string, label: string }} */ b) => {
164
164
  if (a.neighborId !== b.neighborId) {
165
165
  return a.neighborId < b.neighborId ? -1 : 1;
166
166
  }
@@ -260,6 +260,6 @@ export default class ObserverView {
260
260
  * @returns {QueryBuilder} A query builder scoped to this observer
261
261
  */
262
262
  query() {
263
- return new QueryBuilder(this);
263
+ return new QueryBuilder(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
264
264
  }
265
265
  }
@@ -85,8 +85,8 @@ export class PatchBuilderV2 {
85
85
  * @param {{ warn: Function }} [options.logger] - Logger for non-fatal warnings
86
86
  */
87
87
  constructor({ persistence, graphName, writerId, lamport, versionVector, getCurrentState, expectedParentSha = null, onCommitSuccess = null, onDeleteWithData = 'warn', codec, logger }) {
88
- /** @type {import('../../ports/GraphPersistencePort.js').default} */
89
- this._persistence = persistence;
88
+ /** @type {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} */
89
+ this._persistence = /** @type {*} */ (persistence); // TODO(ts-cleanup): narrow port type
90
90
 
91
91
  /** @type {string} */
92
92
  this._graphName = graphName;
@@ -214,7 +214,7 @@ export class PatchBuilderV2 {
214
214
  const { edges } = findAttachedData(state, nodeId);
215
215
  for (const edgeKey of edges) {
216
216
  const [from, to, label] = edgeKey.split('\0');
217
- const edgeDots = [...orsetGetDots(state.edgeAlive, edgeKey)];
217
+ const edgeDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ ([...orsetGetDots(state.edgeAlive, edgeKey)]));
218
218
  this._ops.push(createEdgeRemoveV2(from, to, label, edgeDots));
219
219
  // Provenance: cascade-generated EdgeRemove reads the edge key (to observe its dots)
220
220
  this._reads.add(edgeKey);
@@ -251,7 +251,7 @@ export class PatchBuilderV2 {
251
251
  }
252
252
  }
253
253
 
254
- const observedDots = state ? [...orsetGetDots(state.nodeAlive, nodeId)] : [];
254
+ const observedDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ (state ? [...orsetGetDots(state.nodeAlive, nodeId)] : []));
255
255
  this._ops.push(createNodeRemoveV2(nodeId, observedDots));
256
256
  // Provenance: NodeRemove reads the node (to observe its dots)
257
257
  this._reads.add(nodeId);
@@ -325,7 +325,7 @@ export class PatchBuilderV2 {
325
325
  // Get observed dots from current state (orsetGetDots returns already-encoded dot strings)
326
326
  const state = this._getCurrentState();
327
327
  const edgeKey = encodeEdgeKey(from, to, label);
328
- const observedDots = state ? [...orsetGetDots(state.edgeAlive, edgeKey)] : [];
328
+ const observedDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ (state ? [...orsetGetDots(state.edgeAlive, edgeKey)] : []));
329
329
  this._ops.push(createEdgeRemoveV2(from, to, label, observedDots));
330
330
  // Provenance: EdgeRemove reads the edge key (to observe its dots)
331
331
  this._reads.add(edgeKey);
@@ -454,7 +454,7 @@ export class PatchBuilderV2 {
454
454
  schema,
455
455
  writer: this._writerId,
456
456
  lamport: this._lamport,
457
- context: this._vv,
457
+ context: /** @type {*} */ (this._vv), // TODO(ts-cleanup): narrow port type
458
458
  ops: this._ops,
459
459
  reads: [...this._reads].sort(),
460
460
  writes: [...this._writes].sort(),
@@ -515,10 +515,10 @@ export class PatchBuilderV2 {
515
515
  const currentRefSha = await this._persistence.readRef(writerRef);
516
516
 
517
517
  if (currentRefSha !== this._expectedParentSha) {
518
- const err = new WriterError(
518
+ const err = /** @type {WriterError & { expectedSha: string|null, actualSha: string|null }} */ (new WriterError(
519
519
  'WRITER_CAS_CONFLICT',
520
520
  'Commit failed: writer ref was updated by another process. Re-materialize and retry.'
521
- );
521
+ ));
522
522
  err.expectedSha = this._expectedParentSha;
523
523
  err.actualSha = currentRefSha;
524
524
  throw err;
@@ -556,7 +556,7 @@ export class PatchBuilderV2 {
556
556
  const patchCbor = this._codec.encode(patch);
557
557
 
558
558
  // 5. Write patch.cbor blob
559
- const patchBlobOid = await this._persistence.writeBlob(patchCbor);
559
+ const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
560
560
 
561
561
  // 6. Create tree with the blob
562
562
  // Format for mktree: "mode type oid\tpath"
@@ -72,13 +72,7 @@ export function encodePatchMessage({ graph, writer, lamport, patchOid, schema =
72
72
  * Decodes a patch commit message.
73
73
  *
74
74
  * @param {string} message - The raw commit message
75
- * @returns {Object} The decoded patch message
76
- * @returns {string} return.kind - Always 'patch'
77
- * @returns {string} return.graph - The graph name
78
- * @returns {string} return.writer - The writer ID
79
- * @returns {number} return.lamport - The Lamport timestamp
80
- * @returns {string} return.patchOid - The patch blob OID
81
- * @returns {number} return.schema - The schema version
75
+ * @returns {{ kind: 'patch', graph: string, writer: string, lamport: number, patchOid: string, schema: number }} The decoded patch message
82
76
  * @throws {Error} If the message is not a valid patch message
83
77
  *
84
78
  * @example
@@ -52,7 +52,6 @@ class ProvenanceIndex {
52
52
  /**
53
53
  * Internal index mapping nodeId/edgeKey to Set of patch SHAs.
54
54
  * @type {Map<string, Set<string>>}
55
- * @private
56
55
  */
57
56
  #index;
58
57
 
@@ -120,7 +119,6 @@ class ProvenanceIndex {
120
119
  *
121
120
  * @param {string} entityId - The node ID or edge key
122
121
  * @param {string} patchSha - The patch SHA
123
- * @private
124
122
  */
125
123
  #addEntry(entityId, patchSha) {
126
124
  let shas = this.#index.get(entityId);
@@ -227,12 +225,12 @@ class ProvenanceIndex {
227
225
  * Returns sorted entries for deterministic output.
228
226
  *
229
227
  * @returns {Array<[string, string[]]>} Sorted array of [entityId, sortedShas[]] pairs
230
- * @private
231
228
  */
232
229
  #sortedEntries() {
230
+ /** @type {Array<[string, string[]]>} */
233
231
  const entries = [];
234
232
  for (const [entityId, shas] of this.#index) {
235
- entries.push([entityId, [...shas].sort()]);
233
+ entries.push(/** @type {[string, string[]]} */ ([entityId, [...shas].sort()]));
236
234
  }
237
235
  entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
238
236
  return entries;
@@ -246,7 +244,7 @@ class ProvenanceIndex {
246
244
  *
247
245
  * @param {Object} [options]
248
246
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
249
- * @returns {Buffer} CBOR-encoded index
247
+ * @returns {Buffer|Uint8Array} CBOR-encoded index
250
248
  */
251
249
  serialize({ codec } = {}) {
252
250
  const c = codec || defaultCodec;
@@ -258,7 +256,6 @@ class ProvenanceIndex {
258
256
  *
259
257
  * @param {Array<[string, string[]]>} entries - Array of [entityId, shas[]] pairs
260
258
  * @returns {Map<string, Set<string>>} The built index
261
- * @private
262
259
  */
263
260
  static #buildIndex(entries) {
264
261
  const index = new Map();
@@ -279,7 +276,8 @@ class ProvenanceIndex {
279
276
  */
280
277
  static deserialize(buffer, { codec } = {}) {
281
278
  const c = codec || defaultCodec;
282
- const obj = c.decode(buffer);
279
+ /** @type {{ version?: number, entries?: Array<[string, string[]]> }} */
280
+ const obj = /** @type {any} */ (c.decode(buffer)); // TODO(ts-cleanup): narrow port type
283
281
 
284
282
  if (obj.version !== 1) {
285
283
  throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
@@ -304,7 +302,7 @@ class ProvenanceIndex {
304
302
  /**
305
303
  * Creates a ProvenanceIndex from a JSON representation.
306
304
  *
307
- * @param {Object} json - Object with version and entries array
305
+ * @param {{ version?: number, entries?: Array<[string, string[]]> }} json - Object with version and entries array
308
306
  * @returns {ProvenanceIndex} The deserialized index
309
307
  * @throws {Error} If the JSON contains an unsupported version
310
308
  */
@@ -68,7 +68,6 @@ class ProvenancePayload {
68
68
  /**
69
69
  * The internal array of patch entries. Frozen after construction.
70
70
  * @type {ReadonlyArray<PatchEntry>}
71
- * @private
72
71
  */
73
72
  #patches;
74
73
 
@@ -173,7 +172,7 @@ class ProvenancePayload {
173
172
  // Use JoinReducer's reduceV5 for deterministic materialization.
174
173
  // Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
175
174
  // but returns bare WarpStateV5 when no options passed (as here).
176
- return reduceV5(this.#patches, initialState);
175
+ return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {*} */ (this.#patches), initialState)); // TODO(ts-cleanup): type patch array
177
176
  }
178
177
 
179
178
  /**