@git-stunts/git-warp 11.5.0 → 12.0.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 (51) hide show
  1. package/README.md +145 -1
  2. package/bin/cli/commands/registry.js +4 -0
  3. package/bin/cli/commands/reindex.js +41 -0
  4. package/bin/cli/commands/verify-index.js +59 -0
  5. package/bin/cli/infrastructure.js +7 -2
  6. package/bin/cli/schemas.js +19 -0
  7. package/bin/cli/types.js +2 -0
  8. package/index.d.ts +49 -12
  9. package/package.json +2 -2
  10. package/src/domain/WarpGraph.js +62 -2
  11. package/src/domain/errors/ShardIdOverflowError.js +28 -0
  12. package/src/domain/errors/index.js +1 -0
  13. package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
  14. package/src/domain/services/BitmapIndexReader.js +32 -10
  15. package/src/domain/services/BitmapNeighborProvider.js +178 -0
  16. package/src/domain/services/CheckpointMessageCodec.js +3 -3
  17. package/src/domain/services/CheckpointService.js +77 -12
  18. package/src/domain/services/GraphTraversal.js +1239 -0
  19. package/src/domain/services/IncrementalIndexUpdater.js +765 -0
  20. package/src/domain/services/JoinReducer.js +310 -46
  21. package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
  22. package/src/domain/services/LogicalIndexBuildService.js +108 -0
  23. package/src/domain/services/LogicalIndexReader.js +315 -0
  24. package/src/domain/services/LogicalTraversal.js +321 -202
  25. package/src/domain/services/MaterializedViewService.js +379 -0
  26. package/src/domain/services/ObserverView.js +138 -47
  27. package/src/domain/services/PatchBuilderV2.js +3 -3
  28. package/src/domain/services/PropertyIndexBuilder.js +64 -0
  29. package/src/domain/services/PropertyIndexReader.js +111 -0
  30. package/src/domain/services/SyncController.js +576 -0
  31. package/src/domain/services/TemporalQuery.js +128 -14
  32. package/src/domain/types/PatchDiff.js +90 -0
  33. package/src/domain/types/WarpTypesV2.js +4 -4
  34. package/src/domain/utils/MinHeap.js +45 -17
  35. package/src/domain/utils/canonicalCbor.js +36 -0
  36. package/src/domain/utils/fnv1a.js +20 -0
  37. package/src/domain/utils/roaring.js +14 -3
  38. package/src/domain/utils/shardKey.js +40 -0
  39. package/src/domain/utils/toBytes.js +17 -0
  40. package/src/domain/utils/validateShardOid.js +13 -0
  41. package/src/domain/warp/_internal.js +0 -9
  42. package/src/domain/warp/_wiredMethods.d.ts +8 -2
  43. package/src/domain/warp/checkpoint.methods.js +21 -5
  44. package/src/domain/warp/materialize.methods.js +17 -5
  45. package/src/domain/warp/materializeAdvanced.methods.js +142 -3
  46. package/src/domain/warp/query.methods.js +78 -12
  47. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
  48. package/src/ports/BlobPort.js +1 -1
  49. package/src/ports/NeighborProviderPort.js +59 -0
  50. package/src/ports/SeekCachePort.js +4 -3
  51. package/src/domain/warp/sync.methods.js +0 -554
@@ -9,13 +9,14 @@
9
9
  * }
10
10
  */
11
11
 
12
- import { createORSet, orsetAdd, orsetRemove, orsetJoin } from '../crdt/ORSet.js';
12
+ import { createORSet, orsetAdd, orsetRemove, orsetJoin, orsetContains } from '../crdt/ORSet.js';
13
13
  import { createVersionVector, vvMerge, vvClone, vvDeserialize } from '../crdt/VersionVector.js';
14
14
  import { lwwSet, lwwMax } from '../crdt/LWW.js';
15
15
  import { createEventId, compareEventIds } from '../utils/EventId.js';
16
16
  import { createTickReceipt, OP_TYPES } from '../types/TickReceipt.js';
17
17
  import { encodeDot } from '../crdt/Dot.js';
18
- import { encodeEdgeKey, encodePropKey } from './KeyCodec.js';
18
+ import { encodeEdgeKey, decodeEdgeKey, encodePropKey } from './KeyCodec.js';
19
+ import { createEmptyDiff, mergeDiffs } from '../types/PatchDiff.js';
19
20
 
20
21
  // Re-export key codec functions for backward compatibility
21
22
  export {
@@ -342,48 +343,267 @@ function foldPatchDot(frontier, writer, lamport) {
342
343
  }
343
344
 
344
345
  /**
345
- * Joins a patch into state, applying all operations in order.
346
+ * Merges a patch's context into state and folds the patch dot.
347
+ * @param {WarpStateV5} state
348
+ * @param {Object} patch
349
+ * @param {string} patch.writer
350
+ * @param {number} patch.lamport
351
+ * @param {Map<string, number>|{[x: string]: number}} patch.context
352
+ */
353
+ function updateFrontierFromPatch(state, patch) {
354
+ const contextVV = patch.context instanceof Map
355
+ ? patch.context
356
+ : vvDeserialize(patch.context || {});
357
+ state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
358
+ foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
359
+ }
360
+
361
+ /**
362
+ * Applies a patch to state without receipt collection (zero overhead).
346
363
  *
347
- * This is the primary function for incorporating a single patch into WARP state.
348
- * It iterates through all operations in the patch, creates EventIds for causality
349
- * tracking, and applies each operation using `applyOpV2`.
364
+ * @param {WarpStateV5} state - The state to mutate in place
365
+ * @param {Object} patch - The patch to apply
366
+ * @param {string} patch.writer
367
+ * @param {number} patch.lamport
368
+ * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops
369
+ * @param {Map<string, number>|{[x: string]: number}} patch.context
370
+ * @param {string} patchSha - Git SHA of the patch commit
371
+ * @returns {WarpStateV5} The mutated state
372
+ */
373
+ export function applyFast(state, patch, patchSha) {
374
+ for (let i = 0; i < patch.ops.length; i++) {
375
+ const eventId = createEventId(patch.lamport, patch.writer, patchSha, i);
376
+ applyOpV2(state, patch.ops[i], eventId);
377
+ }
378
+ updateFrontierFromPatch(state, patch);
379
+ return state;
380
+ }
381
+
382
+ /**
383
+ * Builds a reverse map from dot string → element ID for an OR-Set.
350
384
  *
351
- * **Receipt Collection Mode**:
352
- * When `collectReceipts` is true, this function also computes the outcome of each
353
- * operation (applied, redundant, or superseded) and returns a TickReceipt for
354
- * provenance tracking. This has a small performance cost, so it's disabled by default.
385
+ * Only includes mappings for dots that appear in the given targetDots set,
386
+ * allowing early termination once all target dots are accounted for.
355
387
  *
356
- * **Warning**: This function mutates `state` in place. For immutable operations,
357
- * clone the state first using `cloneStateV5()`.
388
+ * @param {import('../crdt/ORSet.js').ORSet} orset
389
+ * @param {Set<string>} targetDots - The dots we care about
390
+ * @returns {Map<string, string>} dot → elementId
391
+ */
392
+ function buildDotToElement(orset, targetDots) {
393
+ const dotToElement = new Map();
394
+ let remaining = targetDots.size;
395
+ for (const [element, dots] of orset.entries) {
396
+ if (remaining === 0) { break; }
397
+ for (const d of dots) {
398
+ if (targetDots.has(d)) {
399
+ dotToElement.set(d, element);
400
+ remaining--;
401
+ if (remaining === 0) { break; }
402
+ }
403
+ }
404
+ }
405
+ return dotToElement;
406
+ }
407
+
408
+ /**
409
+ * Collects the set of alive elements that own at least one of the target dots.
358
410
  *
359
- * @param {WarpStateV5} state - The state to mutate. Modified in place.
360
- * @param {Object} patch - The patch to apply
361
- * @param {string} patch.writer - Writer ID who created this patch
362
- * @param {number} patch.lamport - Lamport timestamp of this patch
363
- * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops - Array of operations to apply
364
- * @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
365
- * @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
366
- * @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
367
- * @returns {WarpStateV5|{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}}
368
- * Returns mutated state directly when collectReceipts is false;
369
- * returns {state, receipt} object when collectReceipts is true
411
+ * Uses a reverse-index from dot → element to avoid scanning every entry in the
412
+ * OR-Set. Complexity: O(total_dots_in_orset) for index build (with early exit)
413
+ * + O(|targetDots|) for lookups, vs the previous O(N * |targetDots|) full scan.
414
+ *
415
+ * @param {import('../crdt/ORSet.js').ORSet} orset
416
+ * @param {Set<string>} observedDots
417
+ * @returns {Set<string>} element IDs that were alive and own at least one observed dot
370
418
  */
371
- export function join(state, patch, patchSha, collectReceipts) {
372
- // ZERO-COST: when collectReceipts is falsy, skip all receipt logic
373
- if (!collectReceipts) {
374
- for (let i = 0; i < patch.ops.length; i++) {
375
- const eventId = createEventId(patch.lamport, patch.writer, patchSha, i);
376
- applyOpV2(state, patch.ops[i], eventId);
419
+ function aliveElementsForDots(orset, observedDots) {
420
+ const result = new Set();
421
+ const dotToElement = buildDotToElement(orset, observedDots);
422
+ for (const d of observedDots) {
423
+ const element = dotToElement.get(d);
424
+ if (element !== undefined && !result.has(element) && orsetContains(orset, element)) {
425
+ result.add(element);
426
+ }
427
+ }
428
+ return result;
429
+ }
430
+
431
+ /**
432
+ * @typedef {Object} SnapshotBeforeOp
433
+ * @property {boolean} [nodeWasAlive]
434
+ * @property {boolean} [edgeWasAlive]
435
+ * @property {string} [edgeKey]
436
+ * @property {unknown} [prevPropValue]
437
+ * @property {string} [propKey]
438
+ * @property {Set<string>} [aliveBeforeNodes]
439
+ * @property {Set<string>} [aliveBeforeEdges]
440
+ */
441
+
442
+ /**
443
+ * Snapshots alive-ness of a node or edge before an op is applied.
444
+ *
445
+ * @param {WarpStateV5} state
446
+ * @param {import('../types/WarpTypesV2.js').OpV2} op
447
+ * @returns {SnapshotBeforeOp}
448
+ */
449
+ function snapshotBeforeOp(state, op) {
450
+ switch (op.type) {
451
+ case 'NodeAdd':
452
+ return { nodeWasAlive: orsetContains(state.nodeAlive, op.node) };
453
+ case 'NodeRemove': {
454
+ const rawDots = /** @type {Iterable<string>} */ (op.observedDots);
455
+ /** @type {Set<string>} */
456
+ const nodeDots = rawDots instanceof Set ? rawDots : new Set(rawDots);
457
+ const aliveBeforeNodes = aliveElementsForDots(state.nodeAlive, nodeDots);
458
+ return { aliveBeforeNodes };
459
+ }
460
+ case 'EdgeAdd': {
461
+ const ek = encodeEdgeKey(op.from, op.to, op.label);
462
+ return { edgeWasAlive: orsetContains(state.edgeAlive, ek), edgeKey: ek };
463
+ }
464
+ case 'EdgeRemove': {
465
+ const rawEdgeDots = /** @type {Iterable<string>} */ (op.observedDots);
466
+ /** @type {Set<string>} */
467
+ const edgeDots = rawEdgeDots instanceof Set ? rawEdgeDots : new Set(rawEdgeDots);
468
+ const aliveBeforeEdges = aliveElementsForDots(state.edgeAlive, edgeDots);
469
+ return { aliveBeforeEdges };
470
+ }
471
+ case 'PropSet': {
472
+ const pk = encodePropKey(op.node, op.key);
473
+ const reg = state.prop.get(pk);
474
+ return { prevPropValue: reg ? reg.value : undefined, propKey: pk };
475
+ }
476
+ default:
477
+ return {};
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Computes diff entries by comparing pre/post alive-ness after an op.
483
+ *
484
+ * @param {import('../types/PatchDiff.js').PatchDiff} diff
485
+ * @param {WarpStateV5} state
486
+ * @param {import('../types/WarpTypesV2.js').OpV2} op
487
+ * @param {SnapshotBeforeOp} before
488
+ */
489
+ function accumulateOpDiff(diff, state, op, before) {
490
+ switch (op.type) {
491
+ case 'NodeAdd': {
492
+ if (!before.nodeWasAlive && orsetContains(state.nodeAlive, op.node)) {
493
+ diff.nodesAdded.push(op.node);
494
+ }
495
+ break;
496
+ }
497
+ case 'NodeRemove': {
498
+ collectNodeRemovals(diff, state, before);
499
+ break;
500
+ }
501
+ case 'EdgeAdd': {
502
+ if (!before.edgeWasAlive && orsetContains(state.edgeAlive, /** @type {string} */ (before.edgeKey))) {
503
+ const { from, to, label } = op;
504
+ diff.edgesAdded.push({ from, to, label });
505
+ }
506
+ break;
507
+ }
508
+ case 'EdgeRemove': {
509
+ collectEdgeRemovals(diff, state, before);
510
+ break;
511
+ }
512
+ case 'PropSet': {
513
+ const reg = state.prop.get(/** @type {string} */ (before.propKey));
514
+ const newVal = reg ? reg.value : undefined;
515
+ if (newVal !== before.prevPropValue) {
516
+ diff.propsChanged.push({
517
+ nodeId: op.node,
518
+ key: op.key,
519
+ value: newVal,
520
+ prevValue: before.prevPropValue,
521
+ });
522
+ }
523
+ break;
524
+ }
525
+ default:
526
+ break;
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Records removal only for elements that were alive before AND dead after.
532
+ *
533
+ * @param {import('../types/PatchDiff.js').PatchDiff} diff
534
+ * @param {WarpStateV5} state
535
+ * @param {SnapshotBeforeOp} before
536
+ */
537
+ function collectNodeRemovals(diff, state, before) {
538
+ if (!before.aliveBeforeNodes) { return; }
539
+ for (const element of before.aliveBeforeNodes) {
540
+ if (!orsetContains(state.nodeAlive, element)) {
541
+ diff.nodesRemoved.push(element);
542
+ }
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Records removal only for edges that were alive before AND dead after.
548
+ *
549
+ * @param {import('../types/PatchDiff.js').PatchDiff} diff
550
+ * @param {WarpStateV5} state
551
+ * @param {SnapshotBeforeOp} before
552
+ */
553
+ function collectEdgeRemovals(diff, state, before) {
554
+ if (!before.aliveBeforeEdges) { return; }
555
+ for (const edgeKey of before.aliveBeforeEdges) {
556
+ if (!orsetContains(state.edgeAlive, edgeKey)) {
557
+ diff.edgesRemoved.push(decodeEdgeKey(edgeKey));
377
558
  }
378
- const contextVV = patch.context instanceof Map
379
- ? patch.context
380
- : vvDeserialize(patch.context);
381
- state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
382
- foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
383
- return state;
384
559
  }
560
+ }
561
+
562
+ /**
563
+ * Applies a patch to state with diff tracking for incremental index updates.
564
+ *
565
+ * Captures alive-ness transitions: only records a diff entry when the
566
+ * alive-ness of a node/edge actually changes, or when an LWW property
567
+ * winner changes. Redundant ops produce no diff entries.
568
+ *
569
+ * @param {WarpStateV5} state - The state to mutate in place
570
+ * @param {Object} patch - The patch to apply
571
+ * @param {string} patch.writer
572
+ * @param {number} patch.lamport
573
+ * @param {Array<Object>} patch.ops
574
+ * @param {Map<string, number>|{[x: string]: number}} patch.context
575
+ * @param {string} patchSha - Git SHA of the patch commit
576
+ * @returns {{state: WarpStateV5, diff: import('../types/PatchDiff.js').PatchDiff}}
577
+ */
578
+ export function applyWithDiff(state, patch, patchSha) {
579
+ const diff = createEmptyDiff();
580
+
581
+ for (let i = 0; i < patch.ops.length; i++) {
582
+ const op = patch.ops[i];
583
+ const eventId = createEventId(patch.lamport, patch.writer, patchSha, i);
584
+ const typedOp = /** @type {import('../types/WarpTypesV2.js').OpV2} */ (op);
585
+ const before = snapshotBeforeOp(state, typedOp);
586
+ applyOpV2(state, typedOp, eventId);
587
+ accumulateOpDiff(diff, state, typedOp, before);
588
+ }
589
+
590
+ updateFrontierFromPatch(state, patch);
591
+ return { state, diff };
592
+ }
385
593
 
386
- // Receipt-enabled path
594
+ /**
595
+ * Applies a patch to state with receipt collection for provenance tracking.
596
+ *
597
+ * @param {WarpStateV5} state - The state to mutate in place
598
+ * @param {Object} patch - The patch to apply
599
+ * @param {string} patch.writer
600
+ * @param {number} patch.lamport
601
+ * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops
602
+ * @param {Map<string, number>|{[x: string]: number}} patch.context
603
+ * @param {string} patchSha - Git SHA of the patch commit
604
+ * @returns {{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}}
605
+ */
606
+ export function applyWithReceipt(state, patch, patchSha) {
387
607
  /** @type {import('../types/TickReceipt.js').OpOutcome[]} */
388
608
  const opResults = [];
389
609
  for (let i = 0; i < patch.ops.length; i++) {
@@ -433,11 +653,7 @@ export function join(state, patch, patchSha, collectReceipts) {
433
653
  opResults.push(entry);
434
654
  }
435
655
 
436
- const contextVV = patch.context instanceof Map
437
- ? patch.context
438
- : vvDeserialize(patch.context);
439
- state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
440
- foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
656
+ updateFrontierFromPatch(state, patch);
441
657
 
442
658
  const receipt = createTickReceipt({
443
659
  patchSha,
@@ -449,6 +665,39 @@ export function join(state, patch, patchSha, collectReceipts) {
449
665
  return { state, receipt };
450
666
  }
451
667
 
668
+ /**
669
+ * Joins a patch into state, applying all operations in order.
670
+ *
671
+ * This is the primary function for incorporating a single patch into WARP state.
672
+ * It iterates through all operations in the patch, creates EventIds for causality
673
+ * tracking, and applies each operation using `applyOpV2`.
674
+ *
675
+ * **Receipt Collection Mode**:
676
+ * When `collectReceipts` is true, this function also computes the outcome of each
677
+ * operation (applied, redundant, or superseded) and returns a TickReceipt for
678
+ * provenance tracking. This has a small performance cost, so it's disabled by default.
679
+ *
680
+ * **Warning**: This function mutates `state` in place. For immutable operations,
681
+ * clone the state first using `cloneStateV5()`.
682
+ *
683
+ * @param {WarpStateV5} state - The state to mutate. Modified in place.
684
+ * @param {Object} patch - The patch to apply
685
+ * @param {string} patch.writer - Writer ID who created this patch
686
+ * @param {number} patch.lamport - Lamport timestamp of this patch
687
+ * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops - Array of operations to apply
688
+ * @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
689
+ * @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
690
+ * @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
691
+ * @returns {WarpStateV5|{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}}
692
+ * Returns mutated state directly when collectReceipts is false;
693
+ * returns {state, receipt} object when collectReceipts is true
694
+ */
695
+ export function join(state, patch, patchSha, collectReceipts) {
696
+ return collectReceipts
697
+ ? applyWithReceipt(state, patch, patchSha)
698
+ : applyFast(state, patch, patchSha);
699
+ }
700
+
452
701
  /**
453
702
  * Joins two V5 states together using CRDT merge semantics.
454
703
  *
@@ -549,9 +798,15 @@ function mergeEdgeBirthEvent(a, b) {
549
798
  * @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
550
799
  * @param {Object} [options] - Optional configuration
551
800
  * @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
552
- * @returns {WarpStateV5|{state: WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}}
553
- * Returns state directly when receipts is false;
554
- * returns {state, receipts} when receipts is true
801
+ * @param {boolean} [options.trackDiff=false] - When true, collect and return PatchDiff
802
+ * @returns {WarpStateV5|{state: WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}|{state: WarpStateV5, diff: import('../types/PatchDiff.js').PatchDiff}}
803
+ * Returns state directly when no options;
804
+ * returns {state, receipts} when receipts is true;
805
+ * returns {state, diff} when trackDiff is true
806
+ *
807
+ * @note When initialState is provided, the returned diff records transitions
808
+ * relative to that state. The caller must ensure any index tree passed to
809
+ * IncrementalIndexUpdater was built from the same initialState.
555
810
  */
556
811
  export function reduceV5(patches, initialState, options) {
557
812
  const state = initialState ? cloneStateV5(initialState) : createEmptyStateV5();
@@ -560,14 +815,23 @@ export function reduceV5(patches, initialState, options) {
560
815
  if (options && options.receipts) {
561
816
  const receipts = [];
562
817
  for (const { patch, sha } of patches) {
563
- const result = /** @type {{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}} */ (join(state, patch, sha, true));
818
+ const result = applyWithReceipt(state, patch, sha);
564
819
  receipts.push(result.receipt);
565
820
  }
566
821
  return { state, receipts };
567
822
  }
568
823
 
824
+ if (options && options.trackDiff) {
825
+ let merged = createEmptyDiff();
826
+ for (const { patch, sha } of patches) {
827
+ const { diff } = applyWithDiff(state, patch, sha);
828
+ merged = mergeDiffs(merged, diff);
829
+ }
830
+ return { state, diff: merged };
831
+ }
832
+
569
833
  for (const { patch, sha } of patches) {
570
- join(state, patch, sha);
834
+ applyFast(state, patch, sha);
571
835
  }
572
836
  return state;
573
837
  }