@git-stunts/git-warp 12.2.0 → 12.3.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 (59) hide show
  1. package/README.md +9 -6
  2. package/bin/cli/commands/trust.js +37 -1
  3. package/bin/cli/infrastructure.js +14 -1
  4. package/bin/cli/schemas.js +4 -4
  5. package/bin/presenters/text.js +10 -3
  6. package/bin/warp-graph.js +4 -1
  7. package/index.d.ts +17 -1
  8. package/package.json +1 -1
  9. package/src/domain/WarpGraph.js +1 -1
  10. package/src/domain/crdt/Dot.js +5 -0
  11. package/src/domain/crdt/LWW.js +3 -1
  12. package/src/domain/crdt/ORSet.js +33 -23
  13. package/src/domain/crdt/VersionVector.js +12 -0
  14. package/src/domain/errors/PatchError.js +27 -0
  15. package/src/domain/errors/StorageError.js +8 -0
  16. package/src/domain/errors/WriterError.js +5 -0
  17. package/src/domain/errors/index.js +1 -0
  18. package/src/domain/services/AuditReceiptService.js +2 -1
  19. package/src/domain/services/AuditVerifierService.js +33 -2
  20. package/src/domain/services/BitmapIndexBuilder.js +14 -9
  21. package/src/domain/services/BoundaryTransitionRecord.js +1 -0
  22. package/src/domain/services/CheckpointMessageCodec.js +5 -0
  23. package/src/domain/services/CheckpointService.js +29 -2
  24. package/src/domain/services/GCPolicy.js +25 -4
  25. package/src/domain/services/GraphTraversal.js +3 -1
  26. package/src/domain/services/IncrementalIndexUpdater.js +179 -36
  27. package/src/domain/services/JoinReducer.js +311 -75
  28. package/src/domain/services/KeyCodec.js +48 -0
  29. package/src/domain/services/MaterializedViewService.js +14 -3
  30. package/src/domain/services/MessageSchemaDetector.js +35 -5
  31. package/src/domain/services/OpNormalizer.js +79 -0
  32. package/src/domain/services/PatchBuilderV2.js +240 -160
  33. package/src/domain/services/QueryBuilder.js +4 -0
  34. package/src/domain/services/SyncAuthService.js +3 -0
  35. package/src/domain/services/SyncController.js +12 -31
  36. package/src/domain/services/SyncProtocol.js +76 -32
  37. package/src/domain/services/WarpMessageCodec.js +2 -0
  38. package/src/domain/trust/TrustCrypto.js +8 -5
  39. package/src/domain/trust/TrustRecordService.js +50 -36
  40. package/src/domain/types/TickReceipt.js +6 -4
  41. package/src/domain/types/WarpTypesV2.js +77 -5
  42. package/src/domain/utils/CachedValue.js +34 -5
  43. package/src/domain/utils/EventId.js +4 -1
  44. package/src/domain/utils/LRUCache.js +3 -1
  45. package/src/domain/utils/RefLayout.js +4 -0
  46. package/src/domain/utils/canonicalStringify.js +48 -18
  47. package/src/domain/utils/defaultClock.js +1 -0
  48. package/src/domain/utils/matchGlob.js +7 -0
  49. package/src/domain/warp/PatchSession.js +30 -24
  50. package/src/domain/warp/Writer.js +12 -1
  51. package/src/domain/warp/_wiredMethods.d.ts +1 -1
  52. package/src/domain/warp/checkpoint.methods.js +36 -7
  53. package/src/domain/warp/fork.methods.js +1 -1
  54. package/src/domain/warp/materialize.methods.js +44 -5
  55. package/src/domain/warp/materializeAdvanced.methods.js +50 -10
  56. package/src/domain/warp/patch.methods.js +21 -11
  57. package/src/infrastructure/adapters/GitGraphAdapter.js +55 -52
  58. package/src/infrastructure/codecs/CborCodec.js +2 -0
  59. package/src/domain/utils/fnv1a.js +0 -20
@@ -21,6 +21,9 @@ import IncrementalIndexUpdater from './IncrementalIndexUpdater.js';
21
21
  import { orsetElements, orsetContains } from '../crdt/ORSet.js';
22
22
  import { decodeEdgeKey } from './KeyCodec.js';
23
23
 
24
+ /** Prefix for property shard paths in the index tree. */
25
+ const PROPS_PREFIX = 'props_';
26
+
24
27
  /**
25
28
  * @typedef {import('./BitmapNeighborProvider.js').LogicalIndex} LogicalIndex
26
29
  */
@@ -66,7 +69,7 @@ function buildInMemoryPropertyReader(tree, codec) {
66
69
  /** @type {Record<string, string>} */
67
70
  const propShardOids = {};
68
71
  for (const path of Object.keys(tree)) {
69
- if (path.startsWith('props_')) {
72
+ if (path.startsWith(PROPS_PREFIX)) {
70
73
  propShardOids[path] = path;
71
74
  }
72
75
  }
@@ -93,7 +96,7 @@ function partitionShardOids(shardOids) {
93
96
  const propOids = {};
94
97
 
95
98
  for (const [path, oid] of Object.entries(shardOids)) {
96
- if (path.startsWith('props_')) {
99
+ if (path.startsWith(PROPS_PREFIX)) {
97
100
  propOids[path] = oid;
98
101
  } else {
99
102
  indexOids[path] = oid;
@@ -105,6 +108,10 @@ function partitionShardOids(shardOids) {
105
108
  /**
106
109
  * Mulberry32 PRNG — deterministic 32-bit generator from a seed.
107
110
  *
111
+ * mulberry32 is a fast 32-bit PRNG by Tommy Ettinger. The magic constants
112
+ * (0x6D2B79F5, shifts 15/13/16) are part of the published algorithm.
113
+ * See: https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
114
+ *
108
115
  * @param {number} seed
109
116
  * @returns {() => number} Returns values in [0, 1)
110
117
  */
@@ -134,6 +141,10 @@ function sampleNodes(allNodes, sampleRate, seed) {
134
141
  }
135
142
  const rng = mulberry32(seed);
136
143
  const sampled = allNodes.filter(() => rng() < sampleRate);
144
+ // When the initial sample is empty (e.g., graph has fewer nodes than
145
+ // sample size), we fall back to using all available nodes. This changes
146
+ // the distribution but is acceptable since the sample is only used for
147
+ // layout heuristics.
137
148
  if (sampled.length === 0) {
138
149
  sampled.push(allNodes[Math.floor(rng() * allNodes.length)]);
139
150
  }
@@ -343,7 +354,7 @@ export default class MaterializedViewService {
343
354
  * @returns {VerifyResult}
344
355
  */
345
356
  verifyIndex({ state, logicalIndex, options = {} }) {
346
- const seed = options.seed ?? (Date.now() & 0x7FFFFFFF);
357
+ const seed = options.seed ?? (Math.random() * 0x7FFFFFFF >>> 0);
347
358
  const sampleRate = options.sampleRate ?? 0.1;
348
359
  const allNodes = [...orsetElements(state.nodeAlive)].sort();
349
360
  const sampled = sampleNodes(allNodes, sampleRate, seed);
@@ -20,17 +20,33 @@ import { getCodec, TRAILER_KEYS } from './MessageCodecInternal.js';
20
20
  // -----------------------------------------------------------------------------
21
21
 
22
22
  /**
23
- * Schema version for classic node-only patches (V5 format).
23
+ * Patch schema version for classic node-only patches (V5 format).
24
24
  * @type {number}
25
25
  */
26
26
  export const SCHEMA_V2 = 2;
27
27
 
28
28
  /**
29
- * Schema version for patches that may contain edge property PropSet ops.
29
+ * Patch schema version for patches that may contain edge property PropSet ops.
30
30
  * @type {number}
31
31
  */
32
32
  export const SCHEMA_V3 = 3;
33
33
 
34
+ /**
35
+ * Alias: patch schema v2 (classic node-only patches).
36
+ * Use this when you need to be explicit that you mean *patch* schema,
37
+ * not checkpoint schema.
38
+ * @type {number}
39
+ */
40
+ export const PATCH_SCHEMA_V2 = SCHEMA_V2;
41
+
42
+ /**
43
+ * Alias: patch schema v3 (edge-property-aware patches).
44
+ * Use this when you need to be explicit that you mean *patch* schema,
45
+ * not checkpoint schema.
46
+ * @type {number}
47
+ */
48
+ export const PATCH_SCHEMA_V3 = SCHEMA_V3;
49
+
34
50
  // -----------------------------------------------------------------------------
35
51
  // Schema Version Detection
36
52
  // -----------------------------------------------------------------------------
@@ -50,6 +66,14 @@ export function detectSchemaVersion(ops) {
50
66
  return SCHEMA_V2;
51
67
  }
52
68
  for (const op of ops) {
69
+ if (!op || typeof op !== 'object') {
70
+ continue;
71
+ }
72
+ // Canonical EdgePropSet always implies schema 3
73
+ if (op.type === 'EdgePropSet') {
74
+ return SCHEMA_V3;
75
+ }
76
+ // Legacy raw PropSet with edge-property encoding
53
77
  if (op.type === 'PropSet' && typeof op.node === 'string' && op.node.startsWith(EDGE_PROP_PREFIX)) {
54
78
  return SCHEMA_V3;
55
79
  }
@@ -90,10 +114,16 @@ export function assertOpsCompatible(ops, maxSchema) {
90
114
  return;
91
115
  }
92
116
  for (const op of ops) {
117
+ if (!op || typeof op !== 'object') {
118
+ continue;
119
+ }
93
120
  if (
94
- op.type === 'PropSet' &&
95
- typeof op.node === 'string' &&
96
- op.node.startsWith(EDGE_PROP_PREFIX)
121
+ // Canonical EdgePropSet (ADR 1) — should never appear on wire pre-ADR 2,
122
+ // but reject defensively for v2 readers
123
+ op.type === 'EdgePropSet' ||
124
+ (op.type === 'PropSet' &&
125
+ typeof op.node === 'string' &&
126
+ op.node.startsWith(EDGE_PROP_PREFIX))
97
127
  ) {
98
128
  throw new SchemaUnsupportedError(
99
129
  'Upgrade to >=7.3.0 (WEIGHTED) to sync edge properties.',
@@ -0,0 +1,79 @@
1
+ /**
2
+ * OpNormalizer — raw ↔ canonical operation conversion.
3
+ *
4
+ * ADR 1 (Canonicalize Edge Property Operations Internally) requires that
5
+ * reducers, provenance, receipts, and queries operate on canonical ops:
6
+ *
7
+ * Raw (persisted): NodeAdd, NodeRemove, EdgeAdd, EdgeRemove, PropSet, BlobValue
8
+ * Canonical (internal): NodeAdd, NodeRemove, EdgeAdd, EdgeRemove, NodePropSet, EdgePropSet, BlobValue
9
+ *
10
+ * **Current normalization location:** Normalization is performed at the
11
+ * reducer entry points (`applyFast`, `applyWithReceipt`, `applyWithDiff`
12
+ * in JoinReducer.js), not at the CBOR decode boundary as originally
13
+ * planned in ADR 1. This is a pragmatic deviation — the reducer calls
14
+ * `normalizeRawOp()` on each op before dispatch. Lowering happens in
15
+ * `PatchBuilderV2.build()`/`commit()` via `lowerCanonicalOp()`.
16
+ *
17
+ * @module domain/services/OpNormalizer
18
+ */
19
+
20
+ import { createNodePropSetV2, createEdgePropSetV2, createPropSetV2 } from '../types/WarpTypesV2.js';
21
+ import { isLegacyEdgePropNode, decodeLegacyEdgePropNode, encodeLegacyEdgePropNode } from './KeyCodec.js';
22
+
23
+ /**
24
+ * Normalizes a single raw (persisted) op into its canonical form.
25
+ *
26
+ * - Raw `PropSet` with \x01-prefixed node → canonical `EdgePropSet`
27
+ * - Raw `PropSet` without prefix → canonical `NodePropSet`
28
+ * - All other op types pass through unchanged.
29
+ *
30
+ * @param {import('../types/WarpTypesV2.js').RawOpV2 | {type: string}} rawOp
31
+ * @returns {import('../types/WarpTypesV2.js').CanonicalOpV2 | {type: string}}
32
+ */
33
+ export function normalizeRawOp(rawOp) {
34
+ if (!rawOp || typeof rawOp !== 'object' || typeof rawOp.type !== 'string') {
35
+ return rawOp;
36
+ }
37
+ if (rawOp.type !== 'PropSet') {
38
+ return rawOp;
39
+ }
40
+ const op = /** @type {import('../types/WarpTypesV2.js').OpV2PropSet} */ (rawOp);
41
+ if (isLegacyEdgePropNode(op.node)) {
42
+ const { from, to, label } = decodeLegacyEdgePropNode(op.node);
43
+ return createEdgePropSetV2(from, to, label, op.key, op.value);
44
+ }
45
+ return createNodePropSetV2(op.node, op.key, op.value);
46
+ }
47
+
48
+ /**
49
+ * Lowers a single canonical op back to raw (persisted) form.
50
+ *
51
+ * - Canonical `NodePropSet` → raw `PropSet`
52
+ * - Canonical `EdgePropSet` → raw `PropSet` with legacy \x01-prefixed node
53
+ * - All other op types pass through unchanged.
54
+ *
55
+ * In M13, this always produces legacy raw PropSet for property ops.
56
+ * A future graph capability cutover (ADR 2) may allow emitting raw
57
+ * `EdgePropSet` directly.
58
+ *
59
+ * @param {import('../types/WarpTypesV2.js').CanonicalOpV2 | {type: string}} canonicalOp
60
+ * @returns {import('../types/WarpTypesV2.js').RawOpV2 | {type: string}}
61
+ */
62
+ export function lowerCanonicalOp(canonicalOp) {
63
+ switch (canonicalOp.type) {
64
+ case 'NodePropSet': {
65
+ const op = /** @type {import('../types/WarpTypesV2.js').OpV2NodePropSet} */ (canonicalOp);
66
+ return createPropSetV2(op.node, op.key, op.value);
67
+ }
68
+ case 'EdgePropSet': {
69
+ const op = /** @type {import('../types/WarpTypesV2.js').OpV2EdgePropSet} */ (canonicalOp);
70
+ return createPropSetV2(
71
+ encodeLegacyEdgePropNode(op.from, op.to, op.label),
72
+ op.key,
73
+ op.value,
74
+ );
75
+ }
76
+ default:
77
+ return canonicalOp;
78
+ }
79
+ }