@git-stunts/git-warp 12.2.1 → 12.4.1
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.
- package/README.md +5 -5
- package/bin/cli/commands/info.js +1 -5
- package/bin/cli/infrastructure.js +6 -9
- package/bin/cli/shared.js +8 -0
- package/bin/presenters/text.js +10 -3
- package/bin/warp-graph.js +6 -6
- package/package.json +1 -1
- package/src/domain/WarpGraph.js +5 -35
- package/src/domain/crdt/ORSet.js +3 -0
- package/src/domain/crdt/VersionVector.js +1 -1
- package/src/domain/entities/GraphNode.js +1 -6
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/PatchError.js +1 -1
- package/src/domain/errors/PersistenceError.js +45 -0
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +1 -4
- package/src/domain/services/AnchorMessageCodec.js +1 -3
- package/src/domain/services/AuditMessageCodec.js +1 -5
- package/src/domain/services/AuditReceiptService.js +4 -18
- package/src/domain/services/AuditVerifierService.js +3 -7
- package/src/domain/services/BitmapIndexBuilder.js +6 -12
- package/src/domain/services/BitmapIndexReader.js +7 -20
- package/src/domain/services/BitmapNeighborProvider.js +1 -3
- package/src/domain/services/BoundaryTransitionRecord.js +7 -23
- package/src/domain/services/CheckpointMessageCodec.js +6 -6
- package/src/domain/services/CheckpointSerializerV5.js +8 -12
- package/src/domain/services/CheckpointService.js +28 -40
- package/src/domain/services/CommitDagTraversalService.js +1 -3
- package/src/domain/services/DagPathFinding.js +9 -59
- package/src/domain/services/DagTopology.js +4 -16
- package/src/domain/services/DagTraversal.js +7 -31
- package/src/domain/services/Frontier.js +4 -6
- package/src/domain/services/GitLogParser.js +1 -2
- package/src/domain/services/GraphTraversal.js +14 -114
- package/src/domain/services/HealthCheckService.js +3 -9
- package/src/domain/services/HookInstaller.js +2 -8
- package/src/domain/services/HttpSyncServer.js +24 -25
- package/src/domain/services/IncrementalIndexUpdater.js +4 -6
- package/src/domain/services/IndexRebuildService.js +6 -52
- package/src/domain/services/IndexStalenessChecker.js +2 -3
- package/src/domain/services/JoinReducer.js +200 -100
- package/src/domain/services/KeyCodec.js +48 -0
- package/src/domain/services/LogicalBitmapIndexBuilder.js +1 -2
- package/src/domain/services/LogicalIndexBuildService.js +2 -6
- package/src/domain/services/LogicalIndexReader.js +1 -2
- package/src/domain/services/LogicalTraversal.js +13 -64
- package/src/domain/services/MaterializedViewService.js +5 -19
- package/src/domain/services/MessageSchemaDetector.js +35 -5
- package/src/domain/services/MigrationService.js +1 -4
- package/src/domain/services/ObserverView.js +1 -7
- package/src/domain/services/OpNormalizer.js +79 -0
- package/src/domain/services/PatchBuilderV2.js +67 -38
- package/src/domain/services/PatchMessageCodec.js +1 -6
- package/src/domain/services/PropertyIndexBuilder.js +1 -2
- package/src/domain/services/PropertyIndexReader.js +1 -4
- package/src/domain/services/ProvenanceIndex.js +5 -7
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -16
- package/src/domain/services/StateDiff.js +3 -9
- package/src/domain/services/StateSerializerV5.js +10 -10
- package/src/domain/services/StreamingBitmapIndexBuilder.js +13 -41
- package/src/domain/services/SyncAuthService.js +8 -32
- package/src/domain/services/SyncController.js +5 -25
- package/src/domain/services/SyncProtocol.js +10 -13
- package/src/domain/services/SyncTrustGate.js +4 -9
- package/src/domain/services/TemporalQuery.js +9 -27
- package/src/domain/services/TranslationCost.js +2 -8
- package/src/domain/services/WarpMessageCodec.js +2 -0
- package/src/domain/services/WarpStateIndexBuilder.js +2 -4
- package/src/domain/services/WormholeService.js +9 -25
- package/src/domain/trust/TrustCrypto.js +9 -10
- package/src/domain/trust/TrustEvaluator.js +1 -8
- package/src/domain/trust/TrustRecordService.js +5 -10
- package/src/domain/types/TickReceipt.js +9 -11
- package/src/domain/types/WarpTypes.js +1 -5
- package/src/domain/types/WarpTypesV2.js +78 -13
- package/src/domain/utils/CachedValue.js +1 -4
- package/src/domain/utils/MinHeap.js +3 -3
- package/src/domain/utils/RefLayout.js +26 -0
- package/src/domain/utils/WriterId.js +2 -7
- package/src/domain/utils/canonicalCbor.js +1 -1
- package/src/domain/utils/defaultClock.js +1 -0
- package/src/domain/utils/defaultCodec.js +1 -1
- package/src/domain/utils/parseCursorBlob.js +4 -4
- package/src/domain/warp/PatchSession.js +3 -8
- package/src/domain/warp/Writer.js +9 -12
- package/src/domain/warp/_wire.js +2 -2
- package/src/domain/warp/_wiredMethods.d.ts +5 -7
- package/src/domain/warp/checkpoint.methods.js +1 -1
- package/src/domain/warp/fork.methods.js +2 -6
- package/src/domain/warp/materializeAdvanced.methods.js +3 -3
- package/src/domain/warp/patch.methods.js +8 -8
- package/src/domain/warp/provenance.methods.js +5 -5
- package/src/domain/warp/query.methods.js +9 -18
- package/src/domain/warp/subscribe.methods.js +2 -8
- package/src/globals.d.ts +7 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -18
- package/src/infrastructure/adapters/ConsoleLogger.js +2 -9
- package/src/infrastructure/adapters/DenoHttpAdapter.js +15 -15
- package/src/infrastructure/adapters/GitGraphAdapter.js +234 -58
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +9 -2
- package/src/infrastructure/adapters/NodeHttpAdapter.js +14 -14
- package/src/infrastructure/adapters/WebCryptoAdapter.js +1 -2
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/HttpServerPort.js +24 -2
- package/src/ports/RefPort.js +2 -1
- package/src/visualization/renderers/ascii/box.js +1 -1
- package/src/visualization/renderers/ascii/check.js +1 -5
- package/src/visualization/renderers/ascii/history.js +1 -6
- package/src/visualization/renderers/ascii/path.js +4 -22
- package/src/visualization/renderers/ascii/progress.js +1 -4
- package/src/visualization/renderers/ascii/seek.js +1 -5
- package/src/visualization/renderers/ascii/table.js +1 -3
|
@@ -83,10 +83,7 @@ export default class LogicalTraversal {
|
|
|
83
83
|
* multiple starts or no start at all (topologicalSort, commonAncestors).
|
|
84
84
|
*
|
|
85
85
|
* @private
|
|
86
|
-
* @param {
|
|
87
|
-
* @param {'out'|'in'|'both'} [opts.dir] - Edge direction to follow
|
|
88
|
-
* @param {string|string[]} [opts.labelFilter] - Edge label(s) to include
|
|
89
|
-
* @param {number} [opts.maxDepth] - Maximum depth to traverse
|
|
86
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], maxDepth?: number }} opts - The traversal options
|
|
90
87
|
* @returns {Promise<{engine: GraphTraversal, direction: 'out'|'in'|'both', options: {labels?: Set<string>}|undefined, depthLimit: number}>}
|
|
91
88
|
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
92
89
|
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
@@ -120,10 +117,7 @@ export default class LogicalTraversal {
|
|
|
120
117
|
*
|
|
121
118
|
* @private
|
|
122
119
|
* @param {string} start - The starting node ID for traversal
|
|
123
|
-
* @param {
|
|
124
|
-
* @param {'out'|'in'|'both'} [opts.dir] - Edge direction to follow
|
|
125
|
-
* @param {string|string[]} [opts.labelFilter] - Edge label(s) to include
|
|
126
|
-
* @param {number} [opts.maxDepth] - Maximum depth to traverse
|
|
120
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], maxDepth?: number }} opts - The traversal options
|
|
127
121
|
* @returns {Promise<{engine: GraphTraversal, direction: 'out'|'in'|'both', options: {labels?: Set<string>}|undefined, depthLimit: number}>}
|
|
128
122
|
* @throws {TraversalError} If the start node is not found (NODE_NOT_FOUND)
|
|
129
123
|
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
@@ -146,10 +140,7 @@ export default class LogicalTraversal {
|
|
|
146
140
|
* Breadth-first traversal.
|
|
147
141
|
*
|
|
148
142
|
* @param {string} start - Starting node ID
|
|
149
|
-
* @param {
|
|
150
|
-
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
151
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
152
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
143
|
+
* @param {{ maxDepth?: number, dir?: 'out'|'in'|'both', labelFilter?: string|string[] }} [options] - Traversal options
|
|
153
144
|
* @returns {Promise<string[]>} Node IDs in visit order
|
|
154
145
|
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
155
146
|
*/
|
|
@@ -169,10 +160,7 @@ export default class LogicalTraversal {
|
|
|
169
160
|
* Depth-first traversal (pre-order).
|
|
170
161
|
*
|
|
171
162
|
* @param {string} start - Starting node ID
|
|
172
|
-
* @param {
|
|
173
|
-
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
174
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
175
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
163
|
+
* @param {{ maxDepth?: number, dir?: 'out'|'in'|'both', labelFilter?: string|string[] }} [options] - Traversal options
|
|
176
164
|
* @returns {Promise<string[]>} Node IDs in visit order
|
|
177
165
|
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
178
166
|
*/
|
|
@@ -193,10 +181,7 @@ export default class LogicalTraversal {
|
|
|
193
181
|
*
|
|
194
182
|
* @param {string} from - Source node ID
|
|
195
183
|
* @param {string} to - Target node ID
|
|
196
|
-
* @param {
|
|
197
|
-
* @param {number} [options.maxDepth] - Maximum search depth
|
|
198
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
199
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
184
|
+
* @param {{ maxDepth?: number, dir?: 'out'|'in'|'both', labelFilter?: string|string[] }} [options] - Traversal options
|
|
200
185
|
* @returns {Promise<{found: boolean, path: string[], length: number}>}
|
|
201
186
|
* When `found` is true, `path` contains the node IDs from `from` to `to` and
|
|
202
187
|
* `length` is the hop count. When `found` is false, `path` is empty and `length` is -1.
|
|
@@ -219,9 +204,7 @@ export default class LogicalTraversal {
|
|
|
219
204
|
* Connected component (undirected by default).
|
|
220
205
|
*
|
|
221
206
|
* @param {string} start - Starting node ID
|
|
222
|
-
* @param {
|
|
223
|
-
* @param {number} [options.maxDepth] - Maximum depth to traverse (default: 1000)
|
|
224
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
207
|
+
* @param {{ maxDepth?: number, labelFilter?: string|string[] }} [options] - Traversal options
|
|
225
208
|
* @returns {Promise<string[]>} Node IDs in visit order
|
|
226
209
|
* @throws {TraversalError} If the start node is not found
|
|
227
210
|
*/
|
|
@@ -236,11 +219,7 @@ export default class LogicalTraversal {
|
|
|
236
219
|
*
|
|
237
220
|
* @param {string} from - Source node ID
|
|
238
221
|
* @param {string} to - Target node ID
|
|
239
|
-
* @param {
|
|
240
|
-
* @param {number} [options.maxDepth] - Maximum search depth
|
|
241
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
242
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
243
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
222
|
+
* @param {{ maxDepth?: number, dir?: 'out'|'in'|'both', labelFilter?: string|string[], signal?: AbortSignal }} [options] - Traversal options
|
|
244
223
|
* @returns {Promise<{reachable: boolean}>}
|
|
245
224
|
*/
|
|
246
225
|
async isReachable(from, to, options = {}) {
|
|
@@ -262,12 +241,7 @@ export default class LogicalTraversal {
|
|
|
262
241
|
*
|
|
263
242
|
* @param {string} from - Source node ID
|
|
264
243
|
* @param {string} to - Target node ID
|
|
265
|
-
* @param {
|
|
266
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
267
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
268
|
-
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
269
|
-
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
270
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
244
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], weightFn?: (from: string, to: string, label: string) => number | Promise<number>, nodeWeightFn?: (nodeId: string) => number | Promise<number>, signal?: AbortSignal }} [options] - Traversal options
|
|
271
245
|
* @returns {Promise<{path: string[], totalCost: number}>}
|
|
272
246
|
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
273
247
|
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
@@ -292,13 +266,7 @@ export default class LogicalTraversal {
|
|
|
292
266
|
*
|
|
293
267
|
* @param {string} from - Source node ID
|
|
294
268
|
* @param {string} to - Target node ID
|
|
295
|
-
* @param {
|
|
296
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
297
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
298
|
-
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
299
|
-
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
300
|
-
* @param {(nodeId: string, goalId: string) => number} [options.heuristicFn] - Heuristic function
|
|
301
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
269
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], weightFn?: (from: string, to: string, label: string) => number | Promise<number>, nodeWeightFn?: (nodeId: string) => number | Promise<number>, heuristicFn?: (nodeId: string, goalId: string) => number, signal?: AbortSignal }} [options] - Traversal options
|
|
302
270
|
* @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>}
|
|
303
271
|
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
304
272
|
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
@@ -326,13 +294,7 @@ export default class LogicalTraversal {
|
|
|
326
294
|
*
|
|
327
295
|
* @param {string} from - Source node ID
|
|
328
296
|
* @param {string} to - Target node ID
|
|
329
|
-
* @param {
|
|
330
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
331
|
-
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
332
|
-
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
333
|
-
* @param {(nodeId: string, goalId: string) => number} [options.forwardHeuristic] - Forward heuristic
|
|
334
|
-
* @param {(nodeId: string, goalId: string) => number} [options.backwardHeuristic] - Backward heuristic
|
|
335
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
297
|
+
* @param {{ labelFilter?: string|string[], weightFn?: (from: string, to: string, label: string) => number | Promise<number>, nodeWeightFn?: (nodeId: string) => number | Promise<number>, forwardHeuristic?: (nodeId: string, goalId: string) => number, backwardHeuristic?: (nodeId: string, goalId: string) => number, signal?: AbortSignal }} [options] - Traversal options
|
|
336
298
|
* @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>}
|
|
337
299
|
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
338
300
|
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
@@ -365,11 +327,7 @@ export default class LogicalTraversal {
|
|
|
365
327
|
* Topological sort (Kahn's algorithm).
|
|
366
328
|
*
|
|
367
329
|
* @param {string|string[]} start - One or more start nodes
|
|
368
|
-
* @param {
|
|
369
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
370
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
371
|
-
* @param {boolean} [options.throwOnCycle] - Whether to throw on cycle detection
|
|
372
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
330
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], throwOnCycle?: boolean, signal?: AbortSignal }} [options] - Traversal options
|
|
373
331
|
* @returns {Promise<{sorted: string[], hasCycle: boolean}>}
|
|
374
332
|
* @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if throwOnCycle and cycle found
|
|
375
333
|
* @throws {TraversalError} code 'NODE_NOT_FOUND' if a start node does not exist
|
|
@@ -405,11 +363,7 @@ export default class LogicalTraversal {
|
|
|
405
363
|
* Direction is fixed to 'in' (backward BFS).
|
|
406
364
|
*
|
|
407
365
|
* @param {string[]} nodes - Nodes to find common ancestors of
|
|
408
|
-
* @param {
|
|
409
|
-
* @param {number} [options.maxDepth] - Maximum search depth
|
|
410
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
411
|
-
* @param {number} [options.maxResults] - Maximum number of results
|
|
412
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
366
|
+
* @param {{ maxDepth?: number, labelFilter?: string|string[], maxResults?: number, signal?: AbortSignal }} [options] - Traversal options
|
|
413
367
|
* @returns {Promise<{ancestors: string[]}>}
|
|
414
368
|
* @throws {TraversalError} code 'NODE_NOT_FOUND' if a node does not exist
|
|
415
369
|
*/
|
|
@@ -443,12 +397,7 @@ export default class LogicalTraversal {
|
|
|
443
397
|
*
|
|
444
398
|
* @param {string} from - Source node ID
|
|
445
399
|
* @param {string} to - Target node ID
|
|
446
|
-
* @param {
|
|
447
|
-
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
448
|
-
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
449
|
-
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
450
|
-
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
451
|
-
* @param {AbortSignal} [options.signal] - Abort signal
|
|
400
|
+
* @param {{ dir?: 'out'|'in'|'both', labelFilter?: string|string[], weightFn?: (from: string, to: string, label: string) => number | Promise<number>, nodeWeightFn?: (nodeId: string) => number | Promise<number>, signal?: AbortSignal }} [options] - Traversal options
|
|
452
401
|
* @returns {Promise<{path: string[], totalCost: number}>}
|
|
453
402
|
* @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles
|
|
454
403
|
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
@@ -207,11 +207,7 @@ function canonicalizeNeighborSignatures(edges) {
|
|
|
207
207
|
/**
|
|
208
208
|
* Compares bitmap index neighbors against ground-truth adjacency for one node.
|
|
209
209
|
*
|
|
210
|
-
* @param {
|
|
211
|
-
* @param {string} params.nodeId
|
|
212
|
-
* @param {string} params.direction
|
|
213
|
-
* @param {LogicalIndex} params.logicalIndex
|
|
214
|
-
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.truthMap
|
|
210
|
+
* @param {{ nodeId: string, direction: string, logicalIndex: LogicalIndex, truthMap: Map<string, Array<{neighborId: string, label: string}>> }} params
|
|
215
211
|
* @returns {VerifyError|null}
|
|
216
212
|
*/
|
|
217
213
|
function compareNodeDirection({ nodeId, direction, logicalIndex, truthMap }) {
|
|
@@ -232,9 +228,7 @@ function compareNodeDirection({ nodeId, direction, logicalIndex, truthMap }) {
|
|
|
232
228
|
|
|
233
229
|
export default class MaterializedViewService {
|
|
234
230
|
/**
|
|
235
|
-
* @param {
|
|
236
|
-
* @param {import('../../ports/CodecPort.js').default} [options.codec]
|
|
237
|
-
* @param {import('../../ports/LoggerPort.js').default} [options.logger]
|
|
231
|
+
* @param {{ codec?: import('../../ports/CodecPort.js').default, logger?: import('../../ports/LoggerPort.js').default }} [options]
|
|
238
232
|
*/
|
|
239
233
|
constructor({ codec, logger } = {}) {
|
|
240
234
|
this._codec = codec || defaultCodec;
|
|
@@ -308,10 +302,7 @@ export default class MaterializedViewService {
|
|
|
308
302
|
/**
|
|
309
303
|
* Applies a PatchDiff incrementally to an existing index tree.
|
|
310
304
|
*
|
|
311
|
-
* @param {
|
|
312
|
-
* @param {Record<string, Uint8Array>} params.existingTree
|
|
313
|
-
* @param {import('../types/PatchDiff.js').PatchDiff} params.diff
|
|
314
|
-
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
305
|
+
* @param {{ existingTree: Record<string, Uint8Array>, diff: import('../types/PatchDiff.js').PatchDiff, state: import('./JoinReducer.js').WarpStateV5 }} params
|
|
315
306
|
* @returns {BuildResult}
|
|
316
307
|
*/
|
|
317
308
|
applyDiff({ existingTree, diff, state }) {
|
|
@@ -345,16 +336,11 @@ export default class MaterializedViewService {
|
|
|
345
336
|
* Verifies index integrity by sampling alive nodes and comparing
|
|
346
337
|
* bitmap neighbor queries against adjacency-based ground truth.
|
|
347
338
|
*
|
|
348
|
-
* @param {
|
|
349
|
-
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
350
|
-
* @param {LogicalIndex} params.logicalIndex
|
|
351
|
-
* @param {Object} [params.options]
|
|
352
|
-
* @param {number} [params.options.seed] - PRNG seed for reproducible sampling
|
|
353
|
-
* @param {number} [params.options.sampleRate] - Fraction of nodes to check (>0 and <=1, default 0.1)
|
|
339
|
+
* @param {{ state: import('./JoinReducer.js').WarpStateV5, logicalIndex: LogicalIndex, options?: { seed?: number, sampleRate?: number } }} params
|
|
354
340
|
* @returns {VerifyResult}
|
|
355
341
|
*/
|
|
356
342
|
verifyIndex({ state, logicalIndex, options = {} }) {
|
|
357
|
-
const seed = options.seed ?? (
|
|
343
|
+
const seed = options.seed ?? (Math.random() * 0x7FFFFFFF >>> 0);
|
|
358
344
|
const sampleRate = options.sampleRate ?? 0.1;
|
|
359
345
|
const allNodes = [...orsetElements(state.nodeAlive)].sort();
|
|
360
346
|
const sampled = sampleNodes(allNodes, sampleRate, seed);
|
|
@@ -20,17 +20,33 @@ import { getCodec, TRAILER_KEYS } from './MessageCodecInternal.js';
|
|
|
20
20
|
// -----------------------------------------------------------------------------
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
op.
|
|
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.',
|
|
@@ -13,10 +13,7 @@ import { createVersionVector, vvIncrement } from '../crdt/VersionVector.js';
|
|
|
13
13
|
* writer, rebuilds the ORSet structures, and copies only properties belonging
|
|
14
14
|
* to visible nodes (dropping dangling props from deleted nodes).
|
|
15
15
|
*
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {Map<string, {value: boolean}>} v4State.nodeAlive - V4 node alive map
|
|
18
|
-
* @param {Map<string, {value: boolean}>} v4State.edgeAlive - V4 edge alive map
|
|
19
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} v4State.prop - V4 property map
|
|
16
|
+
* @param {{ nodeAlive: Map<string, {value: boolean}>, edgeAlive: Map<string, {value: boolean}>, prop: Map<string, import('../crdt/LWW.js').LWWRegister<unknown>> }} v4State - The V4 materialized state (visible projection)
|
|
20
17
|
* @param {string} migrationWriterId - Writer ID to use for synthetic dots
|
|
21
18
|
* @returns {import('./JoinReducer.js').WarpStateV5} The migrated V5 state
|
|
22
19
|
*/
|
|
@@ -156,13 +156,7 @@ export default class ObserverView {
|
|
|
156
156
|
/**
|
|
157
157
|
* Creates a new ObserverView.
|
|
158
158
|
*
|
|
159
|
-
* @param {
|
|
160
|
-
* @param {string} options.name - Observer name
|
|
161
|
-
* @param {Object} options.config - Observer configuration
|
|
162
|
-
* @param {string|string[]} options.config.match - Glob pattern(s) for visible nodes
|
|
163
|
-
* @param {string[]} [options.config.expose] - Property keys to include
|
|
164
|
-
* @param {string[]} [options.config.redact] - Property keys to exclude (takes precedence over expose)
|
|
165
|
-
* @param {import('../WarpGraph.js').default} options.graph - The source WarpGraph instance
|
|
159
|
+
* @param {{ name: string, config: { match: string|string[], expose?: string[], redact?: string[] }, graph: import('../WarpGraph.js').default }} options
|
|
166
160
|
*/
|
|
167
161
|
constructor({ name, config, graph }) {
|
|
168
162
|
/** @type {string} */
|
|
@@ -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
|
+
}
|
|
@@ -20,10 +20,12 @@ import {
|
|
|
20
20
|
createNodeRemoveV2,
|
|
21
21
|
createEdgeAddV2,
|
|
22
22
|
createEdgeRemoveV2,
|
|
23
|
-
|
|
23
|
+
createNodePropSetV2,
|
|
24
|
+
createEdgePropSetV2,
|
|
24
25
|
createPatchV2,
|
|
25
26
|
} from '../types/WarpTypesV2.js';
|
|
26
|
-
import { encodeEdgeKey, EDGE_PROP_PREFIX, CONTENT_PROPERTY_KEY } from './KeyCodec.js';
|
|
27
|
+
import { encodeEdgeKey, FIELD_SEPARATOR, EDGE_PROP_PREFIX, CONTENT_PROPERTY_KEY } from './KeyCodec.js';
|
|
28
|
+
import { lowerCanonicalOp } from './OpNormalizer.js';
|
|
27
29
|
import { encodePatchMessage, decodePatchMessage, detectMessageKind } from './WarpMessageCodec.js';
|
|
28
30
|
import { buildWriterRef } from '../utils/RefLayout.js';
|
|
29
31
|
import WriterError from '../errors/WriterError.js';
|
|
@@ -66,6 +68,30 @@ function findAttachedData(state, nodeId) {
|
|
|
66
68
|
return { edges, props, hasData: edges.length > 0 || props.length > 0 };
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Validates that an identifier does not contain reserved bytes that would
|
|
73
|
+
* make the legacy edge-property encoding ambiguous.
|
|
74
|
+
*
|
|
75
|
+
* Rejects:
|
|
76
|
+
* - Identifiers containing \0 (field separator)
|
|
77
|
+
* - Identifiers starting with \x01 (edge property prefix)
|
|
78
|
+
*
|
|
79
|
+
* @param {string} value - Identifier to validate
|
|
80
|
+
* @param {string} label - Human-readable label for error messages
|
|
81
|
+
* @throws {Error} If the identifier contains reserved bytes
|
|
82
|
+
*/
|
|
83
|
+
function _assertNoReservedBytes(value, label) {
|
|
84
|
+
if (typeof value !== 'string') {
|
|
85
|
+
throw new Error(`${label} must be a string, got ${typeof value}`);
|
|
86
|
+
}
|
|
87
|
+
if (value.includes(FIELD_SEPARATOR)) {
|
|
88
|
+
throw new Error(`${label} must not contain null bytes (\\0): ${JSON.stringify(value)}`);
|
|
89
|
+
}
|
|
90
|
+
if (value.length > 0 && value[0] === EDGE_PROP_PREFIX) {
|
|
91
|
+
throw new Error(`${label} must not start with reserved prefix \\x01: ${JSON.stringify(value)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
/**
|
|
70
96
|
* Fluent builder for creating WARP v5 patches with dots and observed-remove semantics.
|
|
71
97
|
*/
|
|
@@ -73,19 +99,7 @@ export class PatchBuilderV2 {
|
|
|
73
99
|
/**
|
|
74
100
|
* Creates a new PatchBuilderV2.
|
|
75
101
|
*
|
|
76
|
-
* @param {
|
|
77
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git adapter
|
|
78
|
-
* (uses CommitPort + RefPort + BlobPort + TreePort methods)
|
|
79
|
-
* @param {string} options.graphName - Graph namespace
|
|
80
|
-
* @param {string} options.writerId - This writer's ID
|
|
81
|
-
* @param {number} options.lamport - Lamport timestamp for this patch
|
|
82
|
-
* @param {import('../crdt/VersionVector.js').VersionVector} options.versionVector - Current version vector
|
|
83
|
-
* @param {Function} options.getCurrentState - Function that returns the current materialized state
|
|
84
|
-
* @param {string|null} [options.expectedParentSha] - Expected parent SHA for race detection
|
|
85
|
-
* @param {Function|null} [options.onCommitSuccess] - Callback invoked after successful commit
|
|
86
|
-
* @param {'reject'|'cascade'|'warn'} [options.onDeleteWithData='warn'] - Policy when deleting a node with attached data
|
|
87
|
-
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
88
|
-
* @param {{ warn: Function }} [options.logger] - Logger for non-fatal warnings
|
|
102
|
+
* @param {{ persistence: import('../../ports/GraphPersistencePort.js').default, graphName: string, writerId: string, lamport: number, versionVector: import('../crdt/VersionVector.js').VersionVector, getCurrentState: () => import('../services/JoinReducer.js').WarpStateV5 | null, expectedParentSha?: string|null, onCommitSuccess?: ((result: {patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}) => void | Promise<void>)|null, onDeleteWithData?: 'reject'|'cascade'|'warn', codec?: import('../../ports/CodecPort.js').default, logger?: import('../../ports/LoggerPort.js').default }} options
|
|
89
103
|
*/
|
|
90
104
|
constructor({ persistence, graphName, writerId, lamport, versionVector, getCurrentState, expectedParentSha = null, onCommitSuccess = null, onDeleteWithData = 'warn', codec, logger }) {
|
|
91
105
|
/** @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} */
|
|
@@ -103,8 +117,8 @@ export class PatchBuilderV2 {
|
|
|
103
117
|
/** @type {import('../crdt/VersionVector.js').VersionVector} */
|
|
104
118
|
this._vv = vvClone(versionVector); // Clone to track local increments
|
|
105
119
|
|
|
106
|
-
/** @type {
|
|
107
|
-
this._getCurrentState = getCurrentState;
|
|
120
|
+
/** @type {() => import('../services/JoinReducer.js').WarpStateV5 | null} */
|
|
121
|
+
this._getCurrentState = getCurrentState;
|
|
108
122
|
|
|
109
123
|
/**
|
|
110
124
|
* Snapshot of state captured at construction time (C4).
|
|
@@ -133,7 +147,7 @@ export class PatchBuilderV2 {
|
|
|
133
147
|
/** @type {import('../../ports/CodecPort.js').default} */
|
|
134
148
|
this._codec = codec || defaultCodec;
|
|
135
149
|
|
|
136
|
-
/** @type {
|
|
150
|
+
/** @type {import('../../ports/LoggerPort.js').default} */
|
|
137
151
|
this._logger = logger || nullLogger;
|
|
138
152
|
|
|
139
153
|
/**
|
|
@@ -233,6 +247,7 @@ export class PatchBuilderV2 {
|
|
|
233
247
|
*/
|
|
234
248
|
addNode(nodeId) {
|
|
235
249
|
this._assertNotCommitted();
|
|
250
|
+
_assertNoReservedBytes(nodeId, 'nodeId');
|
|
236
251
|
const dot = vvIncrement(this._vv, this._writerId);
|
|
237
252
|
this._ops.push(createNodeAddV2(nodeId, dot));
|
|
238
253
|
// Provenance: NodeAdd writes the node
|
|
@@ -305,8 +320,7 @@ export class PatchBuilderV2 {
|
|
|
305
320
|
}
|
|
306
321
|
|
|
307
322
|
if (this._onDeleteWithData === 'warn') {
|
|
308
|
-
|
|
309
|
-
console.warn(
|
|
323
|
+
this._logger.warn(
|
|
310
324
|
`[warp] Deleting node '${nodeId}' which has attached data (${summary}). ` +
|
|
311
325
|
`Orphaned data will remain in state.`
|
|
312
326
|
);
|
|
@@ -349,6 +363,9 @@ export class PatchBuilderV2 {
|
|
|
349
363
|
*/
|
|
350
364
|
addEdge(from, to, label) {
|
|
351
365
|
this._assertNotCommitted();
|
|
366
|
+
_assertNoReservedBytes(from, 'from node ID');
|
|
367
|
+
_assertNoReservedBytes(to, 'to node ID');
|
|
368
|
+
_assertNoReservedBytes(label, 'edge label');
|
|
352
369
|
const dot = vvIncrement(this._vv, this._writerId);
|
|
353
370
|
this._ops.push(createEdgeAddV2(from, to, label, dot));
|
|
354
371
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
@@ -428,9 +445,11 @@ export class PatchBuilderV2 {
|
|
|
428
445
|
*/
|
|
429
446
|
setProperty(nodeId, key, value) {
|
|
430
447
|
this._assertNotCommitted();
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
448
|
+
_assertNoReservedBytes(nodeId, 'nodeId');
|
|
449
|
+
_assertNoReservedBytes(key, 'property key');
|
|
450
|
+
// Canonical NodePropSet — lowered to raw PropSet at commit time
|
|
451
|
+
this._ops.push(createNodePropSetV2(nodeId, key, value));
|
|
452
|
+
// Provenance: NodePropSet reads the node (implicit existence check) and writes the node
|
|
434
453
|
this._observedOperands.add(nodeId);
|
|
435
454
|
this._writes.add(nodeId);
|
|
436
455
|
return this;
|
|
@@ -475,6 +494,10 @@ export class PatchBuilderV2 {
|
|
|
475
494
|
*/
|
|
476
495
|
setEdgeProperty(from, to, label, key, value) {
|
|
477
496
|
this._assertNotCommitted();
|
|
497
|
+
_assertNoReservedBytes(from, 'from node ID');
|
|
498
|
+
_assertNoReservedBytes(to, 'to node ID');
|
|
499
|
+
_assertNoReservedBytes(label, 'edge label');
|
|
500
|
+
_assertNoReservedBytes(key, 'property key');
|
|
478
501
|
// Validate edge exists in this patch or in current state
|
|
479
502
|
const ek = encodeEdgeKey(from, to, label);
|
|
480
503
|
if (!this._edgesAdded.has(ek)) {
|
|
@@ -484,15 +507,10 @@ export class PatchBuilderV2 {
|
|
|
484
507
|
}
|
|
485
508
|
}
|
|
486
509
|
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
// = `\x01from\0to\0label` + `\0` + key
|
|
490
|
-
// = `\x01from\0to\0label\0key`
|
|
491
|
-
// = encodeEdgePropKey(from, to, label, key)
|
|
492
|
-
const edgeNode = `${EDGE_PROP_PREFIX}${from}\0${to}\0${label}`;
|
|
493
|
-
this._ops.push(createPropSetV2(edgeNode, key, value));
|
|
510
|
+
// Canonical EdgePropSet — lowered to legacy raw PropSet at commit time
|
|
511
|
+
this._ops.push(createEdgePropSetV2(from, to, label, key, value));
|
|
494
512
|
this._hasEdgeProps = true;
|
|
495
|
-
// Provenance:
|
|
513
|
+
// Provenance: EdgePropSet reads the edge (implicit existence check) and writes the edge
|
|
496
514
|
this._observedOperands.add(ek);
|
|
497
515
|
this._writes.add(ek);
|
|
498
516
|
return this;
|
|
@@ -510,11 +528,14 @@ export class PatchBuilderV2 {
|
|
|
510
528
|
* only sets the `_content` property — it does not create the node.
|
|
511
529
|
*
|
|
512
530
|
* @param {string} nodeId - The node ID to attach content to
|
|
513
|
-
* @param {
|
|
531
|
+
* @param {Uint8Array|string} content - The content to attach
|
|
514
532
|
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
515
533
|
*/
|
|
516
534
|
async attachContent(nodeId, content) {
|
|
517
535
|
this._assertNotCommitted();
|
|
536
|
+
// Validate identifiers before writing blob to avoid orphaned blobs
|
|
537
|
+
_assertNoReservedBytes(nodeId, 'nodeId');
|
|
538
|
+
_assertNoReservedBytes(CONTENT_PROPERTY_KEY, 'key');
|
|
518
539
|
const oid = await this._persistence.writeBlob(content);
|
|
519
540
|
this.setProperty(nodeId, CONTENT_PROPERTY_KEY, oid);
|
|
520
541
|
this._contentBlobs.push(oid);
|
|
@@ -528,11 +549,16 @@ export class PatchBuilderV2 {
|
|
|
528
549
|
* @param {string} from - Source node ID
|
|
529
550
|
* @param {string} to - Target node ID
|
|
530
551
|
* @param {string} label - Edge label
|
|
531
|
-
* @param {
|
|
552
|
+
* @param {Uint8Array|string} content - The content to attach
|
|
532
553
|
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
533
554
|
*/
|
|
534
555
|
async attachEdgeContent(from, to, label, content) {
|
|
535
556
|
this._assertNotCommitted();
|
|
557
|
+
// Validate identifiers before writing blob to avoid orphaned blobs
|
|
558
|
+
_assertNoReservedBytes(from, 'from');
|
|
559
|
+
_assertNoReservedBytes(to, 'to');
|
|
560
|
+
_assertNoReservedBytes(label, 'label');
|
|
561
|
+
_assertNoReservedBytes(CONTENT_PROPERTY_KEY, 'key');
|
|
536
562
|
const oid = await this._persistence.writeBlob(content);
|
|
537
563
|
this.setEdgeProperty(from, to, label, CONTENT_PROPERTY_KEY, oid);
|
|
538
564
|
this._contentBlobs.push(oid);
|
|
@@ -559,12 +585,14 @@ export class PatchBuilderV2 {
|
|
|
559
585
|
*/
|
|
560
586
|
build() {
|
|
561
587
|
const schema = this._hasEdgeProps ? 3 : 2;
|
|
588
|
+
// Lower canonical ops to raw form for the persisted patch
|
|
589
|
+
const rawOps = /** @type {import('../types/WarpTypesV2.js').RawOpV2[]} */ (this._ops.map(lowerCanonicalOp));
|
|
562
590
|
return createPatchV2({
|
|
563
591
|
schema,
|
|
564
592
|
writer: this._writerId,
|
|
565
593
|
lamport: this._lamport,
|
|
566
594
|
context: vvSerialize(this._vv),
|
|
567
|
-
ops:
|
|
595
|
+
ops: rawOps,
|
|
568
596
|
reads: [...this._observedOperands].sort(),
|
|
569
597
|
writes: [...this._writes].sort(),
|
|
570
598
|
});
|
|
@@ -678,20 +706,21 @@ export class PatchBuilderV2 {
|
|
|
678
706
|
// For now, we use the calculated lamport for the patch metadata.
|
|
679
707
|
// The dots themselves are independent of patch lamport (they use VV counters).
|
|
680
708
|
const schema = this._hasEdgeProps ? 3 : 2;
|
|
681
|
-
//
|
|
709
|
+
// Lower canonical ops to raw form for the persisted patch
|
|
710
|
+
const rawOps = /** @type {import('../types/WarpTypesV2.js').RawOpV2[]} */ (this._ops.map(lowerCanonicalOp));
|
|
682
711
|
const patch = createPatchV2({
|
|
683
712
|
schema,
|
|
684
713
|
writer: this._writerId,
|
|
685
714
|
lamport,
|
|
686
715
|
context: vvSerialize(this._vv),
|
|
687
|
-
ops:
|
|
716
|
+
ops: rawOps,
|
|
688
717
|
reads: [...this._observedOperands].sort(),
|
|
689
718
|
writes: [...this._writes].sort(),
|
|
690
719
|
});
|
|
691
720
|
|
|
692
721
|
// 6. Encode patch as CBOR and write as a Git blob
|
|
693
722
|
const patchCbor = this._codec.encode(patch);
|
|
694
|
-
const patchBlobOid = await this._persistence.writeBlob(
|
|
723
|
+
const patchBlobOid = await this._persistence.writeBlob(patchCbor);
|
|
695
724
|
|
|
696
725
|
// 7. Create tree with the patch blob + any content blobs (deduplicated)
|
|
697
726
|
// Format for mktree: "mode type oid\tpath"
|
|
@@ -726,7 +755,7 @@ export class PatchBuilderV2 {
|
|
|
726
755
|
await this._onCommitSuccess({ patch, sha: newCommitSha });
|
|
727
756
|
} catch (err) {
|
|
728
757
|
// Commit is already persisted — log but don't fail the caller.
|
|
729
|
-
this._logger.warn(`[warp] onCommitSuccess callback failed (sha=${newCommitSha}):`, err);
|
|
758
|
+
this._logger.warn(`[warp] onCommitSuccess callback failed (sha=${newCommitSha}):`, { error: err });
|
|
730
759
|
}
|
|
731
760
|
}
|
|
732
761
|
|
|
@@ -25,12 +25,7 @@ import {
|
|
|
25
25
|
/**
|
|
26
26
|
* Encodes a patch commit message.
|
|
27
27
|
*
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {string} options.graph - The graph name
|
|
30
|
-
* @param {string} options.writer - The writer ID
|
|
31
|
-
* @param {number} options.lamport - The Lamport timestamp (must be a positive integer)
|
|
32
|
-
* @param {string} options.patchOid - The OID of the patch blob
|
|
33
|
-
* @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
|
|
28
|
+
* @param {{ graph: string, writer: string, lamport: number, patchOid: string, schema?: number }} options - The patch message options
|
|
34
29
|
* @returns {string} The encoded commit message
|
|
35
30
|
* @throws {Error} If any validation fails
|
|
36
31
|
*
|