@git-stunts/git-warp 11.2.1 → 11.5.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.
- package/README.md +24 -1
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +118 -22
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- 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/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- 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/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +22 -3
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +26 -11
- package/src/domain/services/KeyCodec.js +7 -0
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +96 -30
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +40 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +89 -6
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
* @typedef {Object} WarpStateV5
|
|
30
30
|
* @property {import('../crdt/ORSet.js').ORSet} nodeAlive - ORSet of alive nodes
|
|
31
31
|
* @property {import('../crdt/ORSet.js').ORSet} edgeAlive - ORSet of alive edges
|
|
32
|
-
* @property {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
32
|
+
* @property {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} prop - Properties with LWW
|
|
33
33
|
* @property {import('../crdt/VersionVector.js').VersionVector} observedFrontier - Observed version vector
|
|
34
34
|
* @property {Map<string, import('../utils/EventId.js').EventId>} edgeBirthEvent - EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility)
|
|
35
35
|
*/
|
|
@@ -81,7 +81,7 @@ export function createEmptyStateV5() {
|
|
|
81
81
|
* @param {string} [op.to] - Target node ID (for EdgeAdd, EdgeRemove)
|
|
82
82
|
* @param {string} [op.label] - Edge label (for EdgeAdd, EdgeRemove)
|
|
83
83
|
* @param {string} [op.key] - Property key (for PropSet)
|
|
84
|
-
* @param {
|
|
84
|
+
* @param {unknown} [op.value] - Property value (for PropSet)
|
|
85
85
|
* @param {import('../utils/EventId.js').EventId} eventId - Event ID for causality tracking
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
@@ -114,7 +114,7 @@ export function applyOpV2(state, op, eventId) {
|
|
|
114
114
|
// Uses EventId-based LWW, same as v4
|
|
115
115
|
const key = encodePropKey(/** @type {string} */ (op.node), /** @type {string} */ (op.key));
|
|
116
116
|
const current = state.prop.get(key);
|
|
117
|
-
state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister
|
|
117
|
+
state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(current, lwwSet(eventId, op.value))));
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
default:
|
|
@@ -290,11 +290,11 @@ function edgeRemoveOutcome(orset, op) {
|
|
|
290
290
|
* - `superseded`: An existing value with higher EventId wins
|
|
291
291
|
* - `redundant`: Exact same write (identical EventId)
|
|
292
292
|
*
|
|
293
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
293
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} propMap - The properties map keyed by encoded prop keys
|
|
294
294
|
* @param {Object} op - The PropSet operation
|
|
295
295
|
* @param {string} op.node - Node ID owning the property
|
|
296
296
|
* @param {string} op.key - Property key/name
|
|
297
|
-
* @param {
|
|
297
|
+
* @param {unknown} op.value - Property value to set
|
|
298
298
|
* @param {import('../utils/EventId.js').EventId} eventId - The event ID for this operation, used for LWW comparison
|
|
299
299
|
* @returns {{target: string, result: 'applied'|'superseded'|'redundant', reason?: string}}
|
|
300
300
|
* Outcome with encoded prop key as target; includes reason when superseded
|
|
@@ -328,6 +328,19 @@ function propSetOutcome(propMap, op, eventId) {
|
|
|
328
328
|
return { target, result: 'redundant' };
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Folds a patch's own dot into the observed frontier.
|
|
333
|
+
* @param {Map<string, number>} frontier
|
|
334
|
+
* @param {string} writer
|
|
335
|
+
* @param {number} lamport
|
|
336
|
+
*/
|
|
337
|
+
function foldPatchDot(frontier, writer, lamport) {
|
|
338
|
+
const current = frontier.get(writer) || 0;
|
|
339
|
+
if (lamport > current) {
|
|
340
|
+
frontier.set(writer, lamport);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
/**
|
|
332
345
|
* Joins a patch into state, applying all operations in order.
|
|
333
346
|
*
|
|
@@ -347,7 +360,7 @@ function propSetOutcome(propMap, op, eventId) {
|
|
|
347
360
|
* @param {Object} patch - The patch to apply
|
|
348
361
|
* @param {string} patch.writer - Writer ID who created this patch
|
|
349
362
|
* @param {number} patch.lamport - Lamport timestamp of this patch
|
|
350
|
-
* @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?:
|
|
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
|
|
351
364
|
* @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
|
|
352
365
|
* @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
|
|
353
366
|
* @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
|
|
@@ -366,6 +379,7 @@ export function join(state, patch, patchSha, collectReceipts) {
|
|
|
366
379
|
? patch.context
|
|
367
380
|
: vvDeserialize(patch.context);
|
|
368
381
|
state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
|
|
382
|
+
foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
|
|
369
383
|
return state;
|
|
370
384
|
}
|
|
371
385
|
|
|
@@ -423,6 +437,7 @@ export function join(state, patch, patchSha, collectReceipts) {
|
|
|
423
437
|
? patch.context
|
|
424
438
|
: vvDeserialize(patch.context);
|
|
425
439
|
state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
|
|
440
|
+
foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
|
|
426
441
|
|
|
427
442
|
const receipt = createTickReceipt({
|
|
428
443
|
patchSha,
|
|
@@ -470,16 +485,16 @@ export function joinStates(a, b) {
|
|
|
470
485
|
*
|
|
471
486
|
* This is a pure function that does not mutate its inputs.
|
|
472
487
|
*
|
|
473
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
474
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
475
|
-
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
488
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} a - First property map
|
|
489
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} b - Second property map
|
|
490
|
+
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} New map containing merged properties
|
|
476
491
|
*/
|
|
477
492
|
function mergeProps(a, b) {
|
|
478
493
|
const result = new Map(a);
|
|
479
494
|
|
|
480
495
|
for (const [key, regB] of b) {
|
|
481
496
|
const regA = result.get(key);
|
|
482
|
-
result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister
|
|
497
|
+
result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(regA, regB)));
|
|
483
498
|
}
|
|
484
499
|
|
|
485
500
|
return result;
|
|
@@ -530,7 +545,7 @@ function mergeEdgeBirthEvent(a, b) {
|
|
|
530
545
|
* - When `options.receipts` is true, returns a TickReceipt per patch for
|
|
531
546
|
* provenance tracking and debugging.
|
|
532
547
|
*
|
|
533
|
-
* @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?:
|
|
548
|
+
* @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
|
|
534
549
|
* @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
|
|
535
550
|
* @param {Object} [options] - Optional configuration
|
|
536
551
|
* @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
|
|
@@ -18,6 +18,13 @@ export const FIELD_SEPARATOR = '\0';
|
|
|
18
18
|
*/
|
|
19
19
|
export const EDGE_PROP_PREFIX = '\x01';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Well-known property key for content attachment.
|
|
23
|
+
* Stores a content-addressed blob OID as the property value.
|
|
24
|
+
* @const {string}
|
|
25
|
+
*/
|
|
26
|
+
export const CONTENT_PROPERTY_KEY = '_content';
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* Encodes an edge key to a string for Map storage.
|
|
23
30
|
*
|
|
@@ -152,7 +152,7 @@ export default class LogicalTraversal {
|
|
|
152
152
|
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
153
153
|
*/
|
|
154
154
|
async _prepare(start, { dir, labelFilter, maxDepth }) {
|
|
155
|
-
const materialized = await /** @type {
|
|
155
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
|
|
156
156
|
|
|
157
157
|
if (!(await this._graph.hasNode(start))) {
|
|
158
158
|
throw new TraversalError(`Start node not found: ${start}`, {
|
|
@@ -66,7 +66,7 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
|
66
66
|
// -----------------------------------------------------------------------------
|
|
67
67
|
|
|
68
68
|
// Lazy singleton codec instance
|
|
69
|
-
/** @type {
|
|
69
|
+
/** @type {TrailerCodec|null} */
|
|
70
70
|
let _codec = null;
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -16,7 +16,7 @@ import { createVersionVector, vvIncrement } from '../crdt/VersionVector.js';
|
|
|
16
16
|
* @param {Object} v4State - The V4 materialized state (visible projection)
|
|
17
17
|
* @param {Map<string, {value: boolean}>} v4State.nodeAlive - V4 node alive map
|
|
18
18
|
* @param {Map<string, {value: boolean}>} v4State.edgeAlive - V4 edge alive map
|
|
19
|
-
* @param {Map<string,
|
|
19
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} v4State.prop - V4 property map
|
|
20
20
|
* @param {string} migrationWriterId - Writer ID to use for synthetic dots
|
|
21
21
|
* @returns {import('./JoinReducer.js').WarpStateV5} The migrated V5 state
|
|
22
22
|
*/
|
|
@@ -43,10 +43,10 @@ function matchGlob(pattern, str) {
|
|
|
43
43
|
* - If `expose` is provided and non-empty, only keys in `expose` are included.
|
|
44
44
|
* - If `expose` is absent/empty, all non-redacted keys are included.
|
|
45
45
|
*
|
|
46
|
-
* @param {Map<string,
|
|
46
|
+
* @param {Map<string, unknown>} propsMap - The full properties Map
|
|
47
47
|
* @param {string[]|undefined} expose - Whitelist of property keys to include
|
|
48
48
|
* @param {string[]|undefined} redact - Blacklist of property keys to exclude
|
|
49
|
-
* @returns {Map<string,
|
|
49
|
+
* @returns {Map<string, unknown>} Filtered properties Map
|
|
50
50
|
*/
|
|
51
51
|
function filterProps(propsMap, expose, redact) {
|
|
52
52
|
const redactSet = redact && redact.length > 0 ? new Set(redact) : null;
|
|
@@ -102,7 +102,7 @@ export default class ObserverView {
|
|
|
102
102
|
this._graph = graph;
|
|
103
103
|
|
|
104
104
|
/** @type {LogicalTraversal} */
|
|
105
|
-
this.traverse = new LogicalTraversal(/** @type {
|
|
105
|
+
this.traverse = new LogicalTraversal(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
@@ -124,11 +124,11 @@ export default class ObserverView {
|
|
|
124
124
|
* Builds a filtered adjacency structure that only includes edges
|
|
125
125
|
* where both endpoints pass the match filter.
|
|
126
126
|
*
|
|
127
|
-
* @returns {Promise<{state:
|
|
127
|
+
* @returns {Promise<{state: unknown, stateHash: string, adjacency: {outgoing: Map<string, unknown[]>, incoming: Map<string, unknown[]>}}>}
|
|
128
128
|
* @private
|
|
129
129
|
*/
|
|
130
130
|
async _materializeGraph() {
|
|
131
|
-
const materialized = await /** @type {
|
|
131
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{state: import('./JoinReducer.js').WarpStateV5, stateHash: string, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
|
|
132
132
|
const { state, stateHash } = materialized;
|
|
133
133
|
|
|
134
134
|
// Build filtered adjacency: only edges where both endpoints match
|
|
@@ -212,7 +212,7 @@ export default class ObserverView {
|
|
|
212
212
|
* the observer pattern.
|
|
213
213
|
*
|
|
214
214
|
* @param {string} nodeId - The node ID to get properties for
|
|
215
|
-
* @returns {Promise<Map<string,
|
|
215
|
+
* @returns {Promise<Map<string, unknown>|null>} Filtered properties Map, or null
|
|
216
216
|
*/
|
|
217
217
|
async getNodeProps(nodeId) {
|
|
218
218
|
if (!matchGlob(this._matchPattern, nodeId)) {
|
|
@@ -234,7 +234,7 @@ export default class ObserverView {
|
|
|
234
234
|
*
|
|
235
235
|
* An edge is visible only when both endpoints match the observer pattern.
|
|
236
236
|
*
|
|
237
|
-
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string,
|
|
237
|
+
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, unknown>}>>}
|
|
238
238
|
*/
|
|
239
239
|
async getEdges() {
|
|
240
240
|
const allEdges = await this._graph.getEdges();
|
|
@@ -260,6 +260,6 @@ export default class ObserverView {
|
|
|
260
260
|
* @returns {QueryBuilder} A query builder scoped to this observer
|
|
261
261
|
*/
|
|
262
262
|
query() {
|
|
263
|
-
return new QueryBuilder(/** @type {
|
|
263
|
+
return new QueryBuilder(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
|
|
264
264
|
}
|
|
265
265
|
}
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
createPropSetV2,
|
|
24
24
|
createPatchV2,
|
|
25
25
|
} from '../types/WarpTypesV2.js';
|
|
26
|
-
import { encodeEdgeKey, EDGE_PROP_PREFIX } from './KeyCodec.js';
|
|
27
|
-
import { encodePatchMessage, decodePatchMessage } from './WarpMessageCodec.js';
|
|
26
|
+
import { encodeEdgeKey, EDGE_PROP_PREFIX, CONTENT_PROPERTY_KEY } from './KeyCodec.js';
|
|
27
|
+
import { encodePatchMessage, decodePatchMessage, detectMessageKind } from './WarpMessageCodec.js';
|
|
28
28
|
import { buildWriterRef } from '../utils/RefLayout.js';
|
|
29
29
|
import WriterError from '../errors/WriterError.js';
|
|
30
30
|
|
|
@@ -86,7 +86,7 @@ export class PatchBuilderV2 {
|
|
|
86
86
|
*/
|
|
87
87
|
constructor({ persistence, graphName, writerId, lamport, versionVector, getCurrentState, expectedParentSha = null, onCommitSuccess = null, onDeleteWithData = 'warn', codec, logger }) {
|
|
88
88
|
/** @type {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default} */
|
|
89
|
-
this._persistence = /** @type {
|
|
89
|
+
this._persistence = /** @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} */ (persistence);
|
|
90
90
|
|
|
91
91
|
/** @type {string} */
|
|
92
92
|
this._graphName = graphName;
|
|
@@ -124,6 +124,13 @@ export class PatchBuilderV2 {
|
|
|
124
124
|
/** @type {{ warn: Function }} */
|
|
125
125
|
this._logger = logger || nullLogger;
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Content blob OIDs written during this patch via attachContent/attachEdgeContent.
|
|
129
|
+
* These are embedded in the commit tree for GC protection.
|
|
130
|
+
* @type {string[]}
|
|
131
|
+
*/
|
|
132
|
+
this._contentBlobs = [];
|
|
133
|
+
|
|
127
134
|
/**
|
|
128
135
|
* Nodes/edges read by this patch (for provenance tracking).
|
|
129
136
|
*
|
|
@@ -346,7 +353,7 @@ export class PatchBuilderV2 {
|
|
|
346
353
|
*
|
|
347
354
|
* @param {string} nodeId - The node ID to set the property on
|
|
348
355
|
* @param {string} key - Property key (should not contain null bytes)
|
|
349
|
-
* @param {
|
|
356
|
+
* @param {unknown} value - Property value. Must be JSON-serializable (strings,
|
|
350
357
|
* numbers, booleans, arrays, plain objects, or null). Use `null` to
|
|
351
358
|
* effectively delete a property (LWW semantics).
|
|
352
359
|
* @returns {PatchBuilderV2} This builder instance for method chaining
|
|
@@ -389,7 +396,7 @@ export class PatchBuilderV2 {
|
|
|
389
396
|
* @param {string} to - Target node ID (edge destination)
|
|
390
397
|
* @param {string} label - Edge label/type identifying which edge to modify
|
|
391
398
|
* @param {string} key - Property key (should not contain null bytes)
|
|
392
|
-
* @param {
|
|
399
|
+
* @param {unknown} value - Property value. Must be JSON-serializable (strings,
|
|
393
400
|
* numbers, booleans, arrays, plain objects, or null). Use `null` to
|
|
394
401
|
* effectively delete a property (LWW semantics).
|
|
395
402
|
* @returns {PatchBuilderV2} This builder instance for method chaining
|
|
@@ -430,6 +437,45 @@ export class PatchBuilderV2 {
|
|
|
430
437
|
return this;
|
|
431
438
|
}
|
|
432
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Attaches content to a node by writing the blob to the Git object store
|
|
442
|
+
* and storing the blob OID as the `_content` property.
|
|
443
|
+
*
|
|
444
|
+
* The blob OID is also tracked for embedding in the commit tree, which
|
|
445
|
+
* ensures content blobs survive `git gc` (GC protection via reachability).
|
|
446
|
+
*
|
|
447
|
+
* Note: The node must exist in the materialized state (or be added in
|
|
448
|
+
* this patch) for `getContent()` to find it later. `attachContent()`
|
|
449
|
+
* only sets the `_content` property — it does not create the node.
|
|
450
|
+
*
|
|
451
|
+
* @param {string} nodeId - The node ID to attach content to
|
|
452
|
+
* @param {Buffer|string} content - The content to attach
|
|
453
|
+
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
454
|
+
*/
|
|
455
|
+
async attachContent(nodeId, content) {
|
|
456
|
+
const oid = await this._persistence.writeBlob(content);
|
|
457
|
+
this.setProperty(nodeId, CONTENT_PROPERTY_KEY, oid);
|
|
458
|
+
this._contentBlobs.push(oid);
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Attaches content to an edge by writing the blob to the Git object store
|
|
464
|
+
* and storing the blob OID as the `_content` edge property.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} from - Source node ID
|
|
467
|
+
* @param {string} to - Target node ID
|
|
468
|
+
* @param {string} label - Edge label
|
|
469
|
+
* @param {Buffer|string} content - The content to attach
|
|
470
|
+
* @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
|
|
471
|
+
*/
|
|
472
|
+
async attachEdgeContent(from, to, label, content) {
|
|
473
|
+
const oid = await this._persistence.writeBlob(content);
|
|
474
|
+
this.setEdgeProperty(from, to, label, CONTENT_PROPERTY_KEY, oid);
|
|
475
|
+
this._contentBlobs.push(oid);
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
433
479
|
/**
|
|
434
480
|
* Builds the PatchV2 object without committing.
|
|
435
481
|
*
|
|
@@ -454,7 +500,7 @@ export class PatchBuilderV2 {
|
|
|
454
500
|
schema,
|
|
455
501
|
writer: this._writerId,
|
|
456
502
|
lamport: this._lamport,
|
|
457
|
-
context:
|
|
503
|
+
context: vvSerialize(this._vv),
|
|
458
504
|
ops: this._ops,
|
|
459
505
|
reads: [...this._reads].sort(),
|
|
460
506
|
writes: [...this._writes].sort(),
|
|
@@ -468,11 +514,12 @@ export class PatchBuilderV2 {
|
|
|
468
514
|
* 1. Validates the patch is non-empty
|
|
469
515
|
* 2. Checks for concurrent modifications (compare-and-swap on writer ref)
|
|
470
516
|
* 3. Calculates the next lamport timestamp from the parent commit
|
|
471
|
-
* 4.
|
|
472
|
-
* 5.
|
|
473
|
-
* 6. Creates a
|
|
474
|
-
* 7.
|
|
475
|
-
* 8.
|
|
517
|
+
* 4. Builds the PatchV2 structure with the resolved lamport
|
|
518
|
+
* 5. Encodes the patch as CBOR and writes it as a Git blob
|
|
519
|
+
* 6. Creates a Git tree containing the patch blob
|
|
520
|
+
* 7. Creates a commit with proper trailers linking to the parent
|
|
521
|
+
* 8. Updates the writer ref to point to the new commit
|
|
522
|
+
* 9. Invokes the success callback if provided (for eager re-materialization)
|
|
476
523
|
*
|
|
477
524
|
* The commit is written to the writer's patch chain at:
|
|
478
525
|
* `refs/warp/<graphName>/writers/<writerId>`
|
|
@@ -524,19 +571,39 @@ export class PatchBuilderV2 {
|
|
|
524
571
|
throw err;
|
|
525
572
|
}
|
|
526
573
|
|
|
527
|
-
// 3. Calculate lamport and parent from current ref state
|
|
528
|
-
|
|
574
|
+
// 3. Calculate lamport and parent from current ref state.
|
|
575
|
+
// Start from this._lamport (set by _nextLamport() in createPatch()), which already
|
|
576
|
+
// incorporates the globally-observed max Lamport tick via _maxObservedLamport.
|
|
577
|
+
// This ensures a first-time writer whose own chain is empty still commits at a tick
|
|
578
|
+
// above any previously-observed writer, winning LWW tiebreakers correctly.
|
|
579
|
+
let lamport = this._lamport;
|
|
529
580
|
let parentCommit = null;
|
|
530
581
|
|
|
531
582
|
if (currentRefSha) {
|
|
532
|
-
// Read the current patch commit to get its lamport timestamp
|
|
533
|
-
const commitMessage = await this._persistence.showNode(currentRefSha);
|
|
534
|
-
const patchInfo = decodePatchMessage(commitMessage);
|
|
535
|
-
lamport = patchInfo.lamport + 1;
|
|
536
583
|
parentCommit = currentRefSha;
|
|
584
|
+
// Read the current patch commit to get its lamport timestamp and take the max,
|
|
585
|
+
// so the chain stays monotonic even if the ref advanced since createPatch().
|
|
586
|
+
const commitMessage = await this._persistence.showNode(currentRefSha);
|
|
587
|
+
const kind = detectMessageKind(commitMessage);
|
|
588
|
+
|
|
589
|
+
if (kind === 'patch') {
|
|
590
|
+
let patchInfo;
|
|
591
|
+
try {
|
|
592
|
+
patchInfo = decodePatchMessage(commitMessage);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Failed to parse lamport from writer ref ${writerRef}: ` +
|
|
596
|
+
`commit ${currentRefSha} has invalid patch message format`,
|
|
597
|
+
{ cause: err }
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
lamport = Math.max(this._lamport, patchInfo.lamport + 1);
|
|
601
|
+
}
|
|
602
|
+
// Non-patch ref (checkpoint, etc.): keep lamport from this._lamport
|
|
603
|
+
// (already incorporates _maxObservedLamport), matching _nextLamport() behavior.
|
|
537
604
|
}
|
|
538
605
|
|
|
539
|
-
//
|
|
606
|
+
// 4. Build PatchV2 structure with correct lamport
|
|
540
607
|
// Note: Dots were assigned using constructor lamport, but commit lamport may differ.
|
|
541
608
|
// For now, we use the calculated lamport for the patch metadata.
|
|
542
609
|
// The dots themselves are independent of patch lamport (they use VV counters).
|
|
@@ -552,18 +619,20 @@ export class PatchBuilderV2 {
|
|
|
552
619
|
writes: [...this._writes].sort(),
|
|
553
620
|
});
|
|
554
621
|
|
|
555
|
-
//
|
|
622
|
+
// 5. Encode patch as CBOR and write as a Git blob
|
|
556
623
|
const patchCbor = this._codec.encode(patch);
|
|
557
|
-
|
|
558
|
-
// 5. Write patch.cbor blob
|
|
559
624
|
const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
|
|
560
625
|
|
|
561
|
-
// 6. Create tree with the blob
|
|
626
|
+
// 6. Create tree with the patch blob + any content blobs (deduplicated)
|
|
562
627
|
// Format for mktree: "mode type oid\tpath"
|
|
563
|
-
const
|
|
564
|
-
const
|
|
628
|
+
const treeEntries = [`100644 blob ${patchBlobOid}\tpatch.cbor`];
|
|
629
|
+
const uniqueBlobs = [...new Set(this._contentBlobs)];
|
|
630
|
+
for (const blobOid of uniqueBlobs) {
|
|
631
|
+
treeEntries.push(`100644 blob ${blobOid}\t_content_${blobOid}`);
|
|
632
|
+
}
|
|
633
|
+
const treeOid = await this._persistence.writeTree(treeEntries);
|
|
565
634
|
|
|
566
|
-
// 7. Create
|
|
635
|
+
// 7. Create commit with proper trailers linking to the parent
|
|
567
636
|
const commitMessage = encodePatchMessage({
|
|
568
637
|
graph: this._graphName,
|
|
569
638
|
writer: this._writerId,
|
|
@@ -571,8 +640,6 @@ export class PatchBuilderV2 {
|
|
|
571
640
|
patchOid: patchBlobOid,
|
|
572
641
|
schema,
|
|
573
642
|
});
|
|
574
|
-
|
|
575
|
-
// 8. Create commit with tree, linking to previous patch as parent if exists
|
|
576
643
|
const parents = parentCommit ? [parentCommit] : [];
|
|
577
644
|
const newCommitSha = await this._persistence.commitNodeWithTree({
|
|
578
645
|
treeOid,
|
|
@@ -580,10 +647,10 @@ export class PatchBuilderV2 {
|
|
|
580
647
|
message: commitMessage,
|
|
581
648
|
});
|
|
582
649
|
|
|
583
|
-
//
|
|
650
|
+
// 8. Update writer ref to point to new commit
|
|
584
651
|
await this._persistence.updateRef(writerRef, newCommitSha);
|
|
585
652
|
|
|
586
|
-
//
|
|
653
|
+
// 9. Notify success callback (updates graph's version vector + eager re-materialize)
|
|
587
654
|
if (this._onCommitSuccess) {
|
|
588
655
|
try {
|
|
589
656
|
await this._onCommitSuccess({ patch, sha: newCommitSha });
|
|
@@ -593,7 +660,6 @@ export class PatchBuilderV2 {
|
|
|
593
660
|
}
|
|
594
661
|
}
|
|
595
662
|
|
|
596
|
-
// 11. Return the new commit SHA
|
|
597
663
|
return newCommitSha;
|
|
598
664
|
}
|
|
599
665
|
|
|
@@ -277,7 +277,7 @@ class ProvenanceIndex {
|
|
|
277
277
|
static deserialize(buffer, { codec } = {}) {
|
|
278
278
|
const c = codec || defaultCodec;
|
|
279
279
|
/** @type {{ version?: number, entries?: Array<[string, string[]]> }} */
|
|
280
|
-
const obj = /** @type {
|
|
280
|
+
const obj = /** @type {{ version?: number, entries?: Array<[string, string[]]> }} */ (c.decode(buffer));
|
|
281
281
|
|
|
282
282
|
if (obj.version !== 1) {
|
|
283
283
|
throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
|
|
@@ -172,7 +172,7 @@ class ProvenancePayload {
|
|
|
172
172
|
// Use JoinReducer's reduceV5 for deterministic materialization.
|
|
173
173
|
// Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
|
|
174
174
|
// but returns bare WarpStateV5 when no options passed (as here).
|
|
175
|
-
return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {
|
|
175
|
+
return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ ([...this.#patches]), initialState));
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -675,7 +675,7 @@ export default class QueryBuilder {
|
|
|
675
675
|
* @throws {QueryError} If an unknown select field is specified (code: E_QUERY_SELECT_FIELD)
|
|
676
676
|
*/
|
|
677
677
|
async run() {
|
|
678
|
-
const materialized = await /** @type {
|
|
678
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{adjacency: AdjacencyMaps, stateHash: string}> }} */ (this._graph)._materializeGraph();
|
|
679
679
|
const { adjacency, stateHash } = materialized;
|
|
680
680
|
const allNodes = sortIds(await this._graph.getNodes());
|
|
681
681
|
|
|
@@ -805,11 +805,11 @@ export default class QueryBuilder {
|
|
|
805
805
|
for (const nodeId of workingSet) {
|
|
806
806
|
const propsMap = (await this._graph.getNodeProps(nodeId)) || new Map();
|
|
807
807
|
for (const { segments, values } of propsByAgg.values()) {
|
|
808
|
-
/** @type {
|
|
808
|
+
/** @type {unknown} */
|
|
809
809
|
let value = propsMap.get(segments[0]);
|
|
810
810
|
for (let i = 1; i < segments.length; i++) {
|
|
811
811
|
if (value && typeof value === 'object') {
|
|
812
|
-
value = value[segments[i]];
|
|
812
|
+
value = /** @type {Record<string, unknown>} */ (value)[segments[i]];
|
|
813
813
|
} else {
|
|
814
814
|
value = undefined;
|
|
815
815
|
break;
|
|
@@ -24,8 +24,8 @@ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
|
|
|
24
24
|
* @property {string} key - Encoded property key
|
|
25
25
|
* @property {string} nodeId - Node ID (for node props)
|
|
26
26
|
* @property {string} propKey - Property name
|
|
27
|
-
* @property {
|
|
28
|
-
* @property {
|
|
27
|
+
* @property {unknown} oldValue - Previous value (undefined if new)
|
|
28
|
+
* @property {unknown} newValue - New value
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -33,7 +33,7 @@ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
|
|
|
33
33
|
* @property {string} key - Encoded property key
|
|
34
34
|
* @property {string} nodeId - Node ID (for node props)
|
|
35
35
|
* @property {string} propKey - Property name
|
|
36
|
-
* @property {
|
|
36
|
+
* @property {unknown} oldValue - Previous value
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -86,8 +86,8 @@ function compareProps(a, b) {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Checks if two arrays are deeply equal.
|
|
89
|
-
* @param {Array
|
|
90
|
-
* @param {Array
|
|
89
|
+
* @param {Array<unknown>} a
|
|
90
|
+
* @param {Array<unknown>} b
|
|
91
91
|
* @returns {boolean}
|
|
92
92
|
*/
|
|
93
93
|
function arraysEqual(a, b) {
|
|
@@ -104,8 +104,8 @@ function arraysEqual(a, b) {
|
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Checks if two objects are deeply equal.
|
|
107
|
-
* @param {Record<string,
|
|
108
|
-
* @param {Record<string,
|
|
107
|
+
* @param {Record<string, unknown>} a
|
|
108
|
+
* @param {Record<string, unknown>} b
|
|
109
109
|
* @returns {boolean}
|
|
110
110
|
*/
|
|
111
111
|
function objectsEqual(a, b) {
|
|
@@ -127,8 +127,8 @@ function objectsEqual(a, b) {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Checks if two values are deeply equal (for property comparison).
|
|
130
|
-
* @param {
|
|
131
|
-
* @param {
|
|
130
|
+
* @param {unknown} a
|
|
131
|
+
* @param {unknown} b
|
|
132
132
|
* @returns {boolean}
|
|
133
133
|
*/
|
|
134
134
|
function deepEqual(a, b) {
|
|
@@ -148,9 +148,12 @@ function deepEqual(a, b) {
|
|
|
148
148
|
return false;
|
|
149
149
|
}
|
|
150
150
|
if (Array.isArray(a)) {
|
|
151
|
-
return arraysEqual(a, b);
|
|
151
|
+
return arraysEqual(a, /** @type {unknown[]} */ (b));
|
|
152
152
|
}
|
|
153
|
-
return objectsEqual(
|
|
153
|
+
return objectsEqual(
|
|
154
|
+
/** @type {Record<string, unknown>} */ (a),
|
|
155
|
+
/** @type {Record<string, unknown>} */ (b),
|
|
156
|
+
);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
/**
|
|
@@ -139,11 +139,11 @@ export async function computeStateHashV5(state, { crypto, codec } = /** @type {{
|
|
|
139
139
|
* @param {Buffer} buffer
|
|
140
140
|
* @param {Object} [options]
|
|
141
141
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
142
|
-
* @returns {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value:
|
|
142
|
+
* @returns {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: unknown}>}}
|
|
143
143
|
*/
|
|
144
144
|
export function deserializeStateV5(buffer, { codec } = {}) {
|
|
145
145
|
const c = codec || defaultCodec;
|
|
146
|
-
return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value:
|
|
146
|
+
return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: unknown}>}} */ (c.decode(buffer));
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// ============================================================================
|