@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
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Builder for constructing CBOR-based bitmap indexes over the logical graph.
3
+ *
4
+ * Produces sharded index with stable numeric IDs, append-only label registry,
5
+ * per-label forward/reverse bitmaps, and alive bitmaps per shard.
6
+ *
7
+ * Shard output:
8
+ * meta_XX.cbor — nodeId → globalId mappings, nextLocalId, alive bitmap
9
+ * labels.cbor — label registry (append-only)
10
+ * fwd_XX.cbor — forward edge bitmaps (all + byLabel)
11
+ * rev_XX.cbor — reverse edge bitmaps (all + byLabel)
12
+ * receipt.cbor — build metadata
13
+ *
14
+ * @module domain/services/LogicalBitmapIndexBuilder
15
+ */
16
+
17
+ import defaultCodec from '../utils/defaultCodec.js';
18
+ import computeShardKey from '../utils/shardKey.js';
19
+ import { getRoaringBitmap32 } from '../utils/roaring.js';
20
+ import { ShardIdOverflowError } from '../errors/index.js';
21
+
22
+ /** Maximum local IDs per shard (2^24). */
23
+ const MAX_LOCAL_ID = 1 << 24;
24
+
25
+ export default class LogicalBitmapIndexBuilder {
26
+ /**
27
+ * @param {Object} [options]
28
+ * @param {import('../../ports/CodecPort.js').default} [options.codec]
29
+ */
30
+ constructor({ codec } = {}) {
31
+ this._codec = codec || defaultCodec;
32
+
33
+ /** @type {Map<string, number>} nodeId → globalId */
34
+ this._nodeToGlobal = new Map();
35
+
36
+ /** @type {Map<string, string>} globalId(string) → nodeId */
37
+ this._globalToNode = new Map();
38
+
39
+ /** Per-shard next local ID counters. @type {Map<string, number>} */
40
+ this._shardNextLocal = new Map();
41
+
42
+ /** Alive bitmap per shard. @type {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>} */
43
+ this._aliveBitmaps = new Map();
44
+
45
+ /** Label → labelId (append-only). @type {Map<string, number>} */
46
+ this._labelToId = new Map();
47
+
48
+ /** @type {number} */
49
+ this._nextLabelId = 0;
50
+
51
+ /**
52
+ * Forward edge bitmaps.
53
+ * Key: `${shardKey}:all:${globalId}` or `${shardKey}:${labelId}:${globalId}`
54
+ * @type {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>}
55
+ */
56
+ this._fwdBitmaps = new Map();
57
+
58
+ /** Reverse edge bitmaps. Same key scheme as _fwdBitmaps. @type {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>} */
59
+ this._revBitmaps = new Map();
60
+
61
+ /** Per-shard node list for O(shard) serialize. @type {Map<string, Array<[string, number]>>} */
62
+ this._shardNodes = new Map();
63
+ }
64
+
65
+ /**
66
+ * Registers a node and returns its stable global ID.
67
+ * GlobalId = (shardByte << 24) | localId.
68
+ *
69
+ * @param {string} nodeId
70
+ * @returns {number} globalId
71
+ * @throws {ShardIdOverflowError} If the shard is full
72
+ */
73
+ registerNode(nodeId) {
74
+ const existing = this._nodeToGlobal.get(nodeId);
75
+ if (existing !== undefined) {
76
+ return existing;
77
+ }
78
+
79
+ const shardKey = computeShardKey(nodeId);
80
+ const shardByte = parseInt(shardKey, 16);
81
+ const nextLocal = this._shardNextLocal.get(shardKey) ?? 0;
82
+
83
+ if (nextLocal >= MAX_LOCAL_ID) {
84
+ throw new ShardIdOverflowError(
85
+ `Shard '${shardKey}' exceeded max local ID (${MAX_LOCAL_ID})`,
86
+ { shardKey, nextLocalId: nextLocal },
87
+ );
88
+ }
89
+
90
+ const globalId = ((shardByte << 24) | nextLocal) >>> 0;
91
+ this._nodeToGlobal.set(nodeId, globalId);
92
+ this._globalToNode.set(String(globalId), nodeId);
93
+ this._shardNextLocal.set(shardKey, nextLocal + 1);
94
+
95
+ let shardList = this._shardNodes.get(shardKey);
96
+ if (!shardList) {
97
+ shardList = [];
98
+ this._shardNodes.set(shardKey, shardList);
99
+ }
100
+ shardList.push([nodeId, globalId]);
101
+
102
+ return globalId;
103
+ }
104
+
105
+ /**
106
+ * Marks a node as alive in its shard's alive bitmap.
107
+ *
108
+ * @param {string} nodeId
109
+ */
110
+ markAlive(nodeId) {
111
+ const globalId = this._nodeToGlobal.get(nodeId);
112
+ if (globalId === undefined) {
113
+ return;
114
+ }
115
+ const shardKey = computeShardKey(nodeId);
116
+ let bitmap = this._aliveBitmaps.get(shardKey);
117
+ if (!bitmap) {
118
+ const RoaringBitmap32 = getRoaringBitmap32();
119
+ bitmap = new RoaringBitmap32();
120
+ this._aliveBitmaps.set(shardKey, bitmap);
121
+ }
122
+ bitmap.add(globalId);
123
+ }
124
+
125
+ /**
126
+ * Registers a label and returns its append-only labelId.
127
+ *
128
+ * @param {string} label
129
+ * @returns {number}
130
+ */
131
+ registerLabel(label) {
132
+ const existing = this._labelToId.get(label);
133
+ if (existing !== undefined) {
134
+ return existing;
135
+ }
136
+ const id = this._nextLabelId++;
137
+ this._labelToId.set(label, id);
138
+ return id;
139
+ }
140
+
141
+ /**
142
+ * Adds a directed edge, populating forward/reverse bitmaps
143
+ * for both the 'all' bucket and the per-label bucket.
144
+ *
145
+ * @param {string} fromId - Source node ID (must be registered)
146
+ * @param {string} toId - Target node ID (must be registered)
147
+ * @param {string} label - Edge label (must be registered)
148
+ */
149
+ addEdge(fromId, toId, label) {
150
+ const fromGlobal = this._nodeToGlobal.get(fromId);
151
+ const toGlobal = this._nodeToGlobal.get(toId);
152
+ if (fromGlobal === undefined || toGlobal === undefined) {
153
+ return;
154
+ }
155
+
156
+ const labelId = this._labelToId.get(label);
157
+ if (labelId === undefined) {
158
+ return;
159
+ }
160
+
161
+ const fromShard = computeShardKey(fromId);
162
+ const toShard = computeShardKey(toId);
163
+
164
+ // Forward: from's shard, keyed by fromGlobal, value contains toGlobal
165
+ this._addToBitmap(this._fwdBitmaps, { shardKey: fromShard, bucket: 'all', owner: fromGlobal, target: toGlobal });
166
+ this._addToBitmap(this._fwdBitmaps, { shardKey: fromShard, bucket: String(labelId), owner: fromGlobal, target: toGlobal });
167
+
168
+ // Reverse: to's shard, keyed by toGlobal, value contains fromGlobal
169
+ this._addToBitmap(this._revBitmaps, { shardKey: toShard, bucket: 'all', owner: toGlobal, target: fromGlobal });
170
+ this._addToBitmap(this._revBitmaps, { shardKey: toShard, bucket: String(labelId), owner: toGlobal, target: fromGlobal });
171
+ }
172
+
173
+ /**
174
+ * Seeds ID mappings from a previously built meta shard for ID stability.
175
+ *
176
+ * @param {string} shardKey
177
+ * @param {{ nodeToGlobal: Array<[string, number]>|Record<string, number>, nextLocalId: number }} metaShard
178
+ */
179
+ loadExistingMeta(shardKey, metaShard) {
180
+ const entries = Array.isArray(metaShard.nodeToGlobal)
181
+ ? metaShard.nodeToGlobal
182
+ : Object.entries(metaShard.nodeToGlobal);
183
+ let shardList = this._shardNodes.get(shardKey);
184
+ if (!shardList) {
185
+ shardList = [];
186
+ this._shardNodes.set(shardKey, shardList);
187
+ }
188
+ for (const [nodeId, globalId] of entries) {
189
+ this._nodeToGlobal.set(nodeId, /** @type {number} */ (globalId));
190
+ this._globalToNode.set(String(globalId), /** @type {string} */ (nodeId));
191
+ shardList.push([/** @type {string} */ (nodeId), /** @type {number} */ (globalId)]);
192
+ }
193
+ const current = this._shardNextLocal.get(shardKey) ?? 0;
194
+ if (metaShard.nextLocalId > current) {
195
+ this._shardNextLocal.set(shardKey, metaShard.nextLocalId);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Seeds the label registry from a previous build for append-only stability.
201
+ *
202
+ * @param {Record<string, number>|Array<[string, number]>} registry - label → labelId
203
+ */
204
+ loadExistingLabels(registry) {
205
+ const entries = Array.isArray(registry) ? registry : Object.entries(registry);
206
+ let maxId = this._nextLabelId;
207
+ for (const [label, id] of entries) {
208
+ this._labelToId.set(label, id);
209
+ if (id >= maxId) {
210
+ maxId = id + 1;
211
+ }
212
+ }
213
+ this._nextLabelId = maxId;
214
+ }
215
+
216
+ /**
217
+ * Serializes the full index to a Record<string, Uint8Array>.
218
+ *
219
+ * @returns {Record<string, Uint8Array>}
220
+ */
221
+ serialize() {
222
+ /** @type {Record<string, Uint8Array>} */
223
+ const tree = {};
224
+
225
+ // Collect all shard keys that have any data
226
+ const allShardKeys = new Set([
227
+ ...this._shardNextLocal.keys(),
228
+ ]);
229
+
230
+ // Meta shards
231
+ for (const shardKey of allShardKeys) {
232
+ // Use array of [nodeId, globalId] pairs to avoid __proto__ key issues
233
+ // Sort by nodeId for deterministic output
234
+ const nodeToGlobal = (this._shardNodes.get(shardKey) ?? [])
235
+ .slice()
236
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
237
+
238
+ const aliveBitmap = this._aliveBitmaps.get(shardKey);
239
+ const aliveBytes = aliveBitmap ? aliveBitmap.serialize(true) : new Uint8Array(0);
240
+
241
+ const shard = {
242
+ nodeToGlobal,
243
+ nextLocalId: this._shardNextLocal.get(shardKey) ?? 0,
244
+ alive: aliveBytes,
245
+ };
246
+
247
+ tree[`meta_${shardKey}.cbor`] = this._codec.encode(shard).slice();
248
+ }
249
+
250
+ // Labels registry
251
+ /** @type {Array<[string, number]>} */
252
+ const labelRegistry = [];
253
+ for (const [label, id] of this._labelToId) {
254
+ labelRegistry.push([label, id]);
255
+ }
256
+ tree['labels.cbor'] = this._codec.encode(labelRegistry).slice();
257
+
258
+ // Forward/reverse edge shards
259
+ this._serializeEdgeShards(tree, 'fwd', this._fwdBitmaps);
260
+ this._serializeEdgeShards(tree, 'rev', this._revBitmaps);
261
+
262
+ // Receipt
263
+ const receipt = {
264
+ version: 1,
265
+ nodeCount: this._nodeToGlobal.size,
266
+ labelCount: this._labelToId.size,
267
+ shardCount: allShardKeys.size,
268
+ };
269
+ tree['receipt.cbor'] = this._codec.encode(receipt).slice();
270
+
271
+ return tree;
272
+ }
273
+
274
+ /**
275
+ * @param {Record<string, Uint8Array>} tree
276
+ * @param {string} direction - 'fwd' or 'rev'
277
+ * @param {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>} bitmaps
278
+ * @private
279
+ */
280
+ _serializeEdgeShards(tree, direction, bitmaps) {
281
+ // Group by shardKey
282
+ /** @type {Map<string, Record<string, Record<string, Uint8Array>>>} */
283
+ const byShardKey = new Map();
284
+
285
+ for (const [key, bitmap] of bitmaps) {
286
+ // key: `${shardKey}:${bucketName}:${globalId}`
287
+ const firstColon = key.indexOf(':');
288
+ const secondColon = key.indexOf(':', firstColon + 1);
289
+ const shardKey = key.substring(0, firstColon);
290
+ const bucketName = key.substring(firstColon + 1, secondColon);
291
+ const globalIdStr = key.substring(secondColon + 1);
292
+
293
+ if (!byShardKey.has(shardKey)) {
294
+ byShardKey.set(shardKey, {});
295
+ }
296
+ const shardData = /** @type {Record<string, Record<string, Uint8Array>>} */ (byShardKey.get(shardKey));
297
+ if (!shardData[bucketName]) {
298
+ shardData[bucketName] = {};
299
+ }
300
+ shardData[bucketName][globalIdStr] = bitmap.serialize(true);
301
+ }
302
+
303
+ for (const [shardKey, shardData] of byShardKey) {
304
+ tree[`${direction}_${shardKey}.cbor`] = this._codec.encode(shardData).slice();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * @param {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>} store
310
+ * @param {{ shardKey: string, bucket: string, owner: number, target: number }} opts
311
+ * @private
312
+ */
313
+ _addToBitmap(store, { shardKey, bucket, owner, target }) {
314
+ const key = `${shardKey}:${bucket}:${owner}`;
315
+ let bitmap = store.get(key);
316
+ if (!bitmap) {
317
+ const RoaringBitmap32 = getRoaringBitmap32();
318
+ bitmap = new RoaringBitmap32();
319
+ store.set(key, bitmap);
320
+ }
321
+ bitmap.add(target);
322
+ }
323
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Orchestrates a full logical bitmap index build from WarpStateV5.
3
+ *
4
+ * Extracts the visible projection (nodes, edges, properties) from materialized
5
+ * state and delegates to LogicalBitmapIndexBuilder + PropertyIndexBuilder.
6
+ *
7
+ * @module domain/services/LogicalIndexBuildService
8
+ */
9
+
10
+ import defaultCodec from '../utils/defaultCodec.js';
11
+ import nullLogger from '../utils/nullLogger.js';
12
+ import LogicalBitmapIndexBuilder from './LogicalBitmapIndexBuilder.js';
13
+ import PropertyIndexBuilder from './PropertyIndexBuilder.js';
14
+ import { orsetElements } from '../crdt/ORSet.js';
15
+ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
16
+ import { nodeVisibleV5, edgeVisibleV5 } from './StateSerializerV5.js';
17
+
18
+ export default class LogicalIndexBuildService {
19
+ /**
20
+ * @param {Object} [options]
21
+ * @param {import('../../ports/CodecPort.js').default} [options.codec]
22
+ * @param {import('../../ports/LoggerPort.js').default} [options.logger]
23
+ */
24
+ constructor({ codec, logger } = {}) {
25
+ this._codec = codec || defaultCodec;
26
+ this._logger = logger || nullLogger;
27
+ }
28
+
29
+ /**
30
+ * Builds a complete logical index from materialized state.
31
+ *
32
+ * @param {import('./JoinReducer.js').WarpStateV5} state
33
+ * @param {Object} [options]
34
+ * @param {Record<string, { nodeToGlobal: Record<string, number>, nextLocalId: number }>} [options.existingMeta] - Prior meta shards for ID stability
35
+ * @param {Record<string, number>|Array<[string, number]>} [options.existingLabels] - Prior label registry for append-only stability
36
+ * @returns {{ tree: Record<string, Uint8Array>, receipt: Record<string, unknown> }}
37
+ */
38
+ build(state, options = {}) {
39
+ const indexBuilder = new LogicalBitmapIndexBuilder({ codec: this._codec });
40
+ const propBuilder = new PropertyIndexBuilder({ codec: this._codec });
41
+
42
+ // Seed existing data for stability
43
+ if (options.existingMeta) {
44
+ for (const [shardKey, meta] of Object.entries(options.existingMeta)) {
45
+ indexBuilder.loadExistingMeta(shardKey, meta);
46
+ }
47
+ }
48
+ if (options.existingLabels) {
49
+ indexBuilder.loadExistingLabels(options.existingLabels);
50
+ }
51
+
52
+ // 1. Register and mark alive all visible nodes (sorted for deterministic ID assignment)
53
+ const aliveNodes = [...orsetElements(state.nodeAlive)].sort();
54
+ for (const nodeId of aliveNodes) {
55
+ indexBuilder.registerNode(nodeId);
56
+ indexBuilder.markAlive(nodeId);
57
+ }
58
+
59
+ // 2. Collect visible edges and register labels (sorted for deterministic ID assignment)
60
+ const visibleEdges = [];
61
+ for (const edgeKey of orsetElements(state.edgeAlive)) {
62
+ if (edgeVisibleV5(state, edgeKey)) {
63
+ visibleEdges.push(decodeEdgeKey(edgeKey));
64
+ }
65
+ }
66
+ visibleEdges.sort((a, b) => {
67
+ if (a.from !== b.from) {
68
+ return a.from < b.from ? -1 : 1;
69
+ }
70
+ if (a.to !== b.to) {
71
+ return a.to < b.to ? -1 : 1;
72
+ }
73
+ if (a.label !== b.label) {
74
+ return a.label < b.label ? -1 : 1;
75
+ }
76
+ return 0;
77
+ });
78
+ const uniqueLabels = [...new Set(visibleEdges.map(e => e.label))].sort();
79
+ for (const label of uniqueLabels) {
80
+ indexBuilder.registerLabel(label);
81
+ }
82
+
83
+ // 3. Add edges
84
+ for (const { from, to, label } of visibleEdges) {
85
+ indexBuilder.addEdge(from, to, label);
86
+ }
87
+
88
+ // 4. Build property index from visible props
89
+ for (const [propKey, register] of state.prop) {
90
+ if (isEdgePropKey(propKey)) {
91
+ continue;
92
+ }
93
+ const { nodeId, propKey: key } = decodePropKey(propKey);
94
+ if (nodeVisibleV5(state, nodeId)) {
95
+ propBuilder.addProperty(nodeId, key, register.value);
96
+ }
97
+ }
98
+
99
+ // 5. Serialize
100
+ const indexTree = indexBuilder.serialize();
101
+ const propTree = propBuilder.serialize();
102
+ const tree = { ...indexTree, ...propTree };
103
+
104
+ const receipt = /** @type {Record<string, unknown>} */ (this._codec.decode(indexTree['receipt.cbor']));
105
+
106
+ return { tree, receipt };
107
+ }
108
+ }