@git-stunts/git-warp 10.1.2 → 10.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +31 -4
  2. package/bin/warp-graph.js +1242 -59
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +13 -3
  6. package/src/domain/WarpGraph.js +487 -140
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +106 -15
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/parseCursorBlob.js +51 -0
  67. package/src/domain/utils/roaring.js +5 -5
  68. package/src/domain/utils/seekCacheKey.js +32 -0
  69. package/src/domain/warp/PatchSession.js +3 -3
  70. package/src/domain/warp/Writer.js +2 -2
  71. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  72. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  73. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  74. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  75. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  76. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  77. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  78. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  79. package/src/infrastructure/codecs/CborCodec.js +16 -8
  80. package/src/ports/BlobPort.js +2 -2
  81. package/src/ports/CodecPort.js +2 -2
  82. package/src/ports/CommitPort.js +8 -21
  83. package/src/ports/ConfigPort.js +3 -3
  84. package/src/ports/CryptoPort.js +7 -7
  85. package/src/ports/GraphPersistencePort.js +12 -14
  86. package/src/ports/HttpServerPort.js +1 -5
  87. package/src/ports/IndexStoragePort.js +1 -0
  88. package/src/ports/LoggerPort.js +9 -9
  89. package/src/ports/RefPort.js +5 -5
  90. package/src/ports/SeekCachePort.js +73 -0
  91. package/src/ports/TreePort.js +3 -3
  92. package/src/visualization/layouts/converters.js +14 -7
  93. package/src/visualization/layouts/elkAdapter.js +24 -11
  94. package/src/visualization/layouts/elkLayout.js +23 -7
  95. package/src/visualization/layouts/index.js +3 -3
  96. package/src/visualization/renderers/ascii/check.js +30 -17
  97. package/src/visualization/renderers/ascii/graph.js +122 -16
  98. package/src/visualization/renderers/ascii/history.js +29 -90
  99. package/src/visualization/renderers/ascii/index.js +1 -1
  100. package/src/visualization/renderers/ascii/info.js +9 -7
  101. package/src/visualization/renderers/ascii/materialize.js +20 -16
  102. package/src/visualization/renderers/ascii/opSummary.js +81 -0
  103. package/src/visualization/renderers/ascii/path.js +1 -1
  104. package/src/visualization/renderers/ascii/seek.js +344 -0
  105. package/src/visualization/renderers/ascii/table.js +1 -1
  106. package/src/visualization/renderers/svg/index.js +5 -1
@@ -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
  /**
@@ -12,8 +12,8 @@ const DEFAULT_PATTERN = '*';
12
12
  * @typedef {Object} QueryNodeSnapshot
13
13
  * @property {string} id - The unique identifier of the node
14
14
  * @property {Record<string, unknown>} props - Frozen snapshot of node properties
15
- * @property {Array<{label: string, to: string}>} edgesOut - Outgoing edges sorted by label then target
16
- * @property {Array<{label: string, from: string}>} edgesIn - Incoming edges sorted by label then source
15
+ * @property {ReadonlyArray<{label: string, to?: string, from?: string}>} edgesOut - Outgoing edges sorted by label then target
16
+ * @property {ReadonlyArray<{label: string, to?: string, from?: string}>} edgesIn - Incoming edges sorted by label then source
17
17
  */
18
18
 
19
19
  /**
@@ -271,6 +271,7 @@ function cloneValue(value) {
271
271
  * @private
272
272
  */
273
273
  function buildPropsSnapshot(propsMap) {
274
+ /** @type {Record<string, unknown>} */
274
275
  const props = {};
275
276
  const keys = [...propsMap.keys()].sort();
276
277
  for (const key of keys) {
@@ -299,8 +300,8 @@ function buildEdgesSnapshot(edges, directionKey) {
299
300
  if (a.label !== b.label) {
300
301
  return a.label < b.label ? -1 : 1;
301
302
  }
302
- const aPeer = a[directionKey];
303
- const bPeer = b[directionKey];
303
+ const aPeer = /** @type {string} */ (a[directionKey]);
304
+ const bPeer = /** @type {string} */ (b[directionKey]);
304
305
  return aPeer < bPeer ? -1 : aPeer > bPeer ? 1 : 0;
305
306
  });
306
307
  return deepFreeze(list);
@@ -493,9 +494,13 @@ export default class QueryBuilder {
493
494
  */
494
495
  constructor(graph) {
495
496
  this._graph = graph;
497
+ /** @type {string|null} */
496
498
  this._pattern = null;
499
+ /** @type {Array<{type: string, fn?: (node: QueryNodeSnapshot) => boolean, label?: string, depth?: [number, number]}>} */
497
500
  this._operations = [];
501
+ /** @type {string[]|null} */
498
502
  this._select = null;
503
+ /** @type {AggregateSpec|null} */
499
504
  this._aggregate = null;
500
505
  }
501
506
 
@@ -531,7 +536,7 @@ export default class QueryBuilder {
531
536
  */
532
537
  where(fn) {
533
538
  assertPredicate(fn);
534
- const predicate = isPlainObject(fn) ? objectToPredicate(fn) : fn;
539
+ const predicate = isPlainObject(fn) ? objectToPredicate(/** @type {Record<string, unknown>} */ (fn)) : /** @type {(node: QueryNodeSnapshot) => boolean} */ (fn);
535
540
  this._operations.push({ type: 'where', fn: predicate });
536
541
  return this;
537
542
  }
@@ -628,11 +633,6 @@ export default class QueryBuilder {
628
633
  * The "props." prefix is optional and will be stripped automatically.
629
634
  *
630
635
  * @param {AggregateSpec} spec - Aggregation specification
631
- * @param {boolean} [spec.count] - If true, include count of matched nodes
632
- * @param {string} [spec.sum] - Property path to sum
633
- * @param {string} [spec.avg] - Property path to average
634
- * @param {string} [spec.min] - Property path to find minimum
635
- * @param {string} [spec.max] - Property path to find maximum
636
636
  * @returns {QueryBuilder} This builder for chaining
637
637
  * @throws {QueryError} If spec is not a plain object (code: E_QUERY_AGGREGATE_TYPE)
638
638
  * @throws {QueryError} If numeric aggregation keys are not strings (code: E_QUERY_AGGREGATE_TYPE)
@@ -646,11 +646,12 @@ export default class QueryBuilder {
646
646
  });
647
647
  }
648
648
  const numericKeys = ['sum', 'avg', 'min', 'max'];
649
+ const specAny = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (spec));
649
650
  for (const key of numericKeys) {
650
- if (spec[key] !== undefined && typeof spec[key] !== 'string') {
651
+ if (specAny[key] !== undefined && typeof specAny[key] !== 'string') {
651
652
  throw new QueryError(`aggregate() expects ${key} to be a string path`, {
652
653
  code: 'E_QUERY_AGGREGATE_TYPE',
653
- context: { key, receivedType: typeof spec[key] },
654
+ context: { key, receivedType: typeof specAny[key] },
654
655
  });
655
656
  }
656
657
  }
@@ -674,7 +675,7 @@ export default class QueryBuilder {
674
675
  * @throws {QueryError} If an unknown select field is specified (code: E_QUERY_SELECT_FIELD)
675
676
  */
676
677
  async run() {
677
- const materialized = await this._graph._materializeGraph();
678
+ const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
678
679
  const { adjacency, stateHash } = materialized;
679
680
  const allNodes = sortIds(await this._graph.getNodes());
680
681
 
@@ -696,15 +697,16 @@ export default class QueryBuilder {
696
697
  };
697
698
  })
698
699
  );
700
+ const predicate = /** @type {(node: QueryNodeSnapshot) => boolean} */ (op.fn);
699
701
  const filtered = snapshots
700
- .filter(({ snapshot }) => op.fn(snapshot))
702
+ .filter(({ snapshot }) => predicate(snapshot))
701
703
  .map(({ nodeId }) => nodeId);
702
704
  workingSet = sortIds(filtered);
703
705
  continue;
704
706
  }
705
707
 
706
708
  if (op.type === 'outgoing' || op.type === 'incoming') {
707
- const [minD, maxD] = op.depth;
709
+ const [minD, maxD] = /** @type {[number, number]} */ (op.depth);
708
710
  if (minD === 1 && maxD === 1) {
709
711
  workingSet = applyHop({
710
712
  direction: op.type,
@@ -718,7 +720,7 @@ export default class QueryBuilder {
718
720
  label: op.label,
719
721
  workingSet,
720
722
  adjacency,
721
- depth: op.depth,
723
+ depth: /** @type {[number, number]} */ (op.depth),
722
724
  });
723
725
  }
724
726
  }
@@ -778,21 +780,24 @@ export default class QueryBuilder {
778
780
  * @private
779
781
  */
780
782
  async _runAggregate(workingSet, stateHash) {
781
- const spec = this._aggregate;
783
+ const spec = /** @type {AggregateSpec} */ (this._aggregate);
784
+ /** @type {AggregateResult} */
782
785
  const result = { stateHash };
786
+ const specRec = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (spec));
783
787
 
784
788
  if (spec.count) {
785
789
  result.count = workingSet.length;
786
790
  }
787
791
 
788
792
  const numericAggs = ['sum', 'avg', 'min', 'max'];
789
- const activeAggs = numericAggs.filter((key) => spec[key]);
793
+ const activeAggs = numericAggs.filter((key) => specRec[key]);
790
794
 
791
795
  if (activeAggs.length > 0) {
796
+ /** @type {Map<string, {segments: string[], values: number[]}>} */
792
797
  const propsByAgg = new Map();
793
798
  for (const key of activeAggs) {
794
799
  propsByAgg.set(key, {
795
- segments: spec[key].replace(/^props\./, '').split('.'),
800
+ segments: /** @type {string} */ (specRec[key]).replace(/^props\./, '').split('.'),
796
801
  values: [],
797
802
  });
798
803
  }
@@ -800,6 +805,7 @@ export default class QueryBuilder {
800
805
  for (const nodeId of workingSet) {
801
806
  const propsMap = (await this._graph.getNodeProps(nodeId)) || new Map();
802
807
  for (const { segments, values } of propsByAgg.values()) {
808
+ /** @type {*} */ // TODO(ts-cleanup): type deep property traversal
803
809
  let value = propsMap.get(segments[0]);
804
810
  for (let i = 1; i < segments.length; i++) {
805
811
  if (value && typeof value === 'object') {
@@ -817,15 +823,15 @@ export default class QueryBuilder {
817
823
 
818
824
  for (const [key, { values }] of propsByAgg) {
819
825
  if (key === 'sum') {
820
- result.sum = values.length > 0 ? values.reduce((a, b) => a + b, 0) : 0;
826
+ result.sum = values.length > 0 ? values.reduce((/** @type {number} */ a, /** @type {number} */ b) => a + b, 0) : 0;
821
827
  } else if (key === 'avg') {
822
- result.avg = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
828
+ result.avg = values.length > 0 ? values.reduce((/** @type {number} */ a, /** @type {number} */ b) => a + b, 0) / values.length : 0;
823
829
  } else if (key === 'min') {
824
830
  result.min =
825
- values.length > 0 ? values.reduce((m, v) => (v < m ? v : m), Infinity) : 0;
831
+ values.length > 0 ? values.reduce((/** @type {number} */ m, /** @type {number} */ v) => (v < m ? v : m), Infinity) : 0;
826
832
  } else if (key === 'max') {
827
833
  result.max =
828
- values.length > 0 ? values.reduce((m, v) => (v > m ? v : m), -Infinity) : 0;
834
+ values.length > 0 ? values.reduce((/** @type {number} */ m, /** @type {number} */ v) => (v > m ? v : m), -Infinity) : 0;
829
835
  }
830
836
  }
831
837
  }
@@ -86,8 +86,8 @@ function compareProps(a, b) {
86
86
 
87
87
  /**
88
88
  * Checks if two arrays are deeply equal.
89
- * @param {Array} a
90
- * @param {Array} b
89
+ * @param {Array<*>} a
90
+ * @param {Array<*>} b
91
91
  * @returns {boolean}
92
92
  */
93
93
  function arraysEqual(a, b) {
@@ -104,8 +104,8 @@ function arraysEqual(a, b) {
104
104
 
105
105
  /**
106
106
  * Checks if two objects are deeply equal.
107
- * @param {Object} a
108
- * @param {Object} b
107
+ * @param {Record<string, *>} a
108
+ * @param {Record<string, *>} b
109
109
  * @returns {boolean}
110
110
  */
111
111
  function objectsEqual(a, b) {
@@ -155,9 +155,9 @@ function deepEqual(a, b) {
155
155
 
156
156
  /**
157
157
  * Computes set difference: elements in `after` not in `before`.
158
- * @param {Set} before
159
- * @param {Set} after
160
- * @returns {Array}
158
+ * @param {Set<string>} before
159
+ * @param {Set<string>} after
160
+ * @returns {Array<string>}
161
161
  */
162
162
  function setAdded(before, after) {
163
163
  const result = [];