@git-stunts/git-warp 10.8.0 → 11.3.3
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 +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- 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 +3 -3
- 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 +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- 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 +2 -2
- 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 +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- 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 +71 -4
- 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/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- 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/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- 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 +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- 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
- package/src/hooks/post-merge.sh +0 -60
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance methods for WarpGraph — patch lookups, slice materialization,
|
|
3
|
+
* backward causal cone computation, and causal sorting.
|
|
4
|
+
*
|
|
5
|
+
* Every function uses `this` bound to a WarpGraph instance at runtime
|
|
6
|
+
* via wireWarpMethods().
|
|
7
|
+
*
|
|
8
|
+
* @module domain/warp/provenance.methods
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { QueryError } from './_internal.js';
|
|
12
|
+
import { createEmptyStateV5, reduceV5 } from '../services/JoinReducer.js';
|
|
13
|
+
import { ProvenancePayload } from '../services/ProvenancePayload.js';
|
|
14
|
+
import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
|
|
15
|
+
|
|
16
|
+
/** @typedef {import('../types/WarpTypesV2.js').PatchV2} PatchV2 */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns all patch SHAs that affected a given node or edge.
|
|
20
|
+
*
|
|
21
|
+
* "Affected" means the patch either read from or wrote to the entity
|
|
22
|
+
* (based on the patch's I/O declarations from HG/IO/1).
|
|
23
|
+
*
|
|
24
|
+
* If `autoMaterialize` is enabled, this will automatically materialize
|
|
25
|
+
* the state if dirty. Otherwise, call `materialize()` first.
|
|
26
|
+
*
|
|
27
|
+
* @this {import('../WarpGraph.js').default}
|
|
28
|
+
* @param {string} entityId - The node ID or edge key to query
|
|
29
|
+
* @returns {Promise<string[]>} Array of patch SHAs that affected the entity, sorted alphabetically
|
|
30
|
+
* @throws {QueryError} If no cached state exists and autoMaterialize is off (code: `E_NO_STATE`)
|
|
31
|
+
*/
|
|
32
|
+
export async function patchesFor(entityId) {
|
|
33
|
+
await this._ensureFreshState();
|
|
34
|
+
|
|
35
|
+
if (this._provenanceDegraded) {
|
|
36
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
37
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!this._provenanceIndex) {
|
|
42
|
+
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
43
|
+
code: 'E_NO_STATE',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return this._provenanceIndex.patchesFor(entityId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Materializes only the backward causal cone for a specific node.
|
|
51
|
+
*
|
|
52
|
+
* This implements the slicing theorem from Paper III (Computational Holography):
|
|
53
|
+
* Given a target node v, compute its backward causal cone D(v) - the set of
|
|
54
|
+
* all patches that contributed to v's current state - and replay only those.
|
|
55
|
+
*
|
|
56
|
+
* The algorithm:
|
|
57
|
+
* 1. Start with patches that directly wrote to the target node
|
|
58
|
+
* 2. For each patch, find entities it read from
|
|
59
|
+
* 3. Recursively gather all dependencies
|
|
60
|
+
* 4. Topologically sort by Lamport timestamp (causal order)
|
|
61
|
+
* 5. Replay the sorted patches against empty state
|
|
62
|
+
*
|
|
63
|
+
* **Requires a cached state.** Call materialize() first to build the provenance index.
|
|
64
|
+
*
|
|
65
|
+
* @this {import('../WarpGraph.js').default}
|
|
66
|
+
* @param {string} nodeId - The target node ID to materialize the cone for
|
|
67
|
+
* @param {{receipts?: boolean}} [options] - Optional configuration
|
|
68
|
+
* @returns {Promise<{state: import('../services/JoinReducer.js').WarpStateV5, patchCount: number, receipts?: import('../types/TickReceipt.js').TickReceipt[]}>}
|
|
69
|
+
* Returns the sliced state with the patch count (for comparison with full materialization)
|
|
70
|
+
* @throws {QueryError} If no provenance index exists (code: `E_NO_STATE`)
|
|
71
|
+
* @throws {Error} If patch loading fails
|
|
72
|
+
*/
|
|
73
|
+
export async function materializeSlice(nodeId, options) {
|
|
74
|
+
const t0 = this._clock.now();
|
|
75
|
+
const collectReceipts = options && options.receipts;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Ensure fresh state before accessing provenance index
|
|
79
|
+
await this._ensureFreshState();
|
|
80
|
+
|
|
81
|
+
if (this._provenanceDegraded) {
|
|
82
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
83
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this._provenanceIndex) {
|
|
88
|
+
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
89
|
+
code: 'E_NO_STATE',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 1. Compute backward causal cone using BFS over the provenance index
|
|
94
|
+
// Returns Map<sha, patch> with patches already loaded (avoids double I/O)
|
|
95
|
+
const conePatchMap = await this._computeBackwardCone(nodeId);
|
|
96
|
+
|
|
97
|
+
// 2. If no patches in cone, return empty state
|
|
98
|
+
if (conePatchMap.size === 0) {
|
|
99
|
+
const emptyState = createEmptyStateV5();
|
|
100
|
+
this._logTiming('materializeSlice', t0, { metrics: '0 patches (empty cone)' });
|
|
101
|
+
return {
|
|
102
|
+
state: emptyState,
|
|
103
|
+
patchCount: 0,
|
|
104
|
+
...(collectReceipts ? { receipts: [] } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Convert cached patches to entry format (patches already loaded by _computeBackwardCone)
|
|
109
|
+
const patchEntries = [];
|
|
110
|
+
for (const [sha, patch] of conePatchMap) {
|
|
111
|
+
patchEntries.push({ patch, sha });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Topologically sort by causal order (Lamport timestamp, then writer, then SHA)
|
|
115
|
+
const sortedPatches = this._sortPatchesCausally(patchEntries);
|
|
116
|
+
|
|
117
|
+
// 5. Replay: use reduceV5 directly when collecting receipts, otherwise use ProvenancePayload
|
|
118
|
+
this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
|
|
119
|
+
|
|
120
|
+
if (collectReceipts) {
|
|
121
|
+
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(sortedPatches, undefined, { receipts: true }));
|
|
122
|
+
return {
|
|
123
|
+
state: result.state,
|
|
124
|
+
patchCount: sortedPatches.length,
|
|
125
|
+
receipts: result.receipts,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const payload = new ProvenancePayload(sortedPatches);
|
|
130
|
+
return {
|
|
131
|
+
state: payload.replay(),
|
|
132
|
+
patchCount: sortedPatches.length,
|
|
133
|
+
};
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Computes the backward causal cone for a node.
|
|
142
|
+
*
|
|
143
|
+
* Uses BFS over the provenance index:
|
|
144
|
+
* 1. Find all patches that wrote to the target node
|
|
145
|
+
* 2. For each patch, find entities it read from
|
|
146
|
+
* 3. Find all patches that wrote to those entities
|
|
147
|
+
* 4. Repeat until no new patches are found
|
|
148
|
+
*
|
|
149
|
+
* Returns a Map of SHA -> patch to avoid double-loading (the cone
|
|
150
|
+
* computation needs to read patches for their read-dependencies,
|
|
151
|
+
* so we cache them for later replay).
|
|
152
|
+
*
|
|
153
|
+
* @this {import('../WarpGraph.js').default}
|
|
154
|
+
* @param {string} nodeId - The target node ID
|
|
155
|
+
* @returns {Promise<Map<string, Object>>} Map of patch SHA to loaded patch object
|
|
156
|
+
*/
|
|
157
|
+
export async function _computeBackwardCone(nodeId) {
|
|
158
|
+
if (!this._provenanceIndex) {
|
|
159
|
+
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
160
|
+
code: 'E_NO_STATE',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const cone = new Map(); // sha -> patch (cache loaded patches)
|
|
164
|
+
const visited = new Set(); // Visited entities
|
|
165
|
+
const queue = [nodeId]; // BFS queue of entities to process
|
|
166
|
+
let qi = 0;
|
|
167
|
+
|
|
168
|
+
while (qi < queue.length) {
|
|
169
|
+
const entityId = queue[qi++];
|
|
170
|
+
|
|
171
|
+
if (visited.has(entityId)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
visited.add(entityId);
|
|
175
|
+
|
|
176
|
+
// Get all patches that affected this entity
|
|
177
|
+
const patchShas = /** @type {import('../services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
|
|
178
|
+
|
|
179
|
+
for (const sha of patchShas) {
|
|
180
|
+
if (cone.has(sha)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Load the patch and cache it
|
|
185
|
+
const patch = await this._loadPatchBySha(sha);
|
|
186
|
+
cone.set(sha, patch);
|
|
187
|
+
|
|
188
|
+
// Add read dependencies to the queue
|
|
189
|
+
const patchReads = /** @type {{reads?: string[]}} */ (patch).reads;
|
|
190
|
+
if (patchReads) {
|
|
191
|
+
for (const readEntity of patchReads) {
|
|
192
|
+
if (!visited.has(readEntity)) {
|
|
193
|
+
queue.push(readEntity);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return cone;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Loads a single patch by its SHA.
|
|
205
|
+
*
|
|
206
|
+
* Thin wrapper around the internal `_loadPatchBySha` helper. Exposed for
|
|
207
|
+
* CLI/debug tooling (e.g. seek tick receipts) that needs to inspect patch
|
|
208
|
+
* operations without re-materializing intermediate states.
|
|
209
|
+
*
|
|
210
|
+
* @this {import('../WarpGraph.js').default}
|
|
211
|
+
* @param {string} sha - The patch commit SHA
|
|
212
|
+
* @returns {Promise<Object>} The decoded patch object
|
|
213
|
+
* @throws {Error} If the commit is not a patch or loading fails
|
|
214
|
+
*/
|
|
215
|
+
export async function loadPatchBySha(sha) {
|
|
216
|
+
return await this._loadPatchBySha(sha);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Loads a single patch by its SHA.
|
|
221
|
+
*
|
|
222
|
+
* @this {import('../WarpGraph.js').default}
|
|
223
|
+
* @param {string} sha - The patch commit SHA
|
|
224
|
+
* @returns {Promise<Object>} The decoded patch object
|
|
225
|
+
* @throws {Error} If the commit is not a patch or loading fails
|
|
226
|
+
*/
|
|
227
|
+
export async function _loadPatchBySha(sha) {
|
|
228
|
+
const nodeInfo = await this._persistence.getNodeInfo(sha);
|
|
229
|
+
const kind = detectMessageKind(nodeInfo.message);
|
|
230
|
+
|
|
231
|
+
if (kind !== 'patch') {
|
|
232
|
+
throw new Error(`Commit ${sha} is not a patch`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
236
|
+
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
237
|
+
return /** @type {Object} */ (this._codec.decode(patchBuffer));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Loads multiple patches by their SHAs.
|
|
242
|
+
*
|
|
243
|
+
* @this {import('../WarpGraph.js').default}
|
|
244
|
+
* @param {string[]} shas - Array of patch commit SHAs
|
|
245
|
+
* @returns {Promise<Array<{patch: Object, sha: string}>>} Array of patch entries
|
|
246
|
+
* @throws {Error} If any SHA is not a patch or loading fails
|
|
247
|
+
*/
|
|
248
|
+
export async function _loadPatchesBySha(shas) {
|
|
249
|
+
const entries = [];
|
|
250
|
+
|
|
251
|
+
for (const sha of shas) {
|
|
252
|
+
const patch = await this._loadPatchBySha(sha);
|
|
253
|
+
entries.push({ patch, sha });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return entries;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Sorts patches in causal order for deterministic replay.
|
|
261
|
+
*
|
|
262
|
+
* Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
|
|
263
|
+
* This ensures deterministic ordering regardless of discovery order.
|
|
264
|
+
*
|
|
265
|
+
* @this {import('../WarpGraph.js').default}
|
|
266
|
+
* @param {Array<{patch: PatchV2, sha: string}>} patches - Unsorted patch entries
|
|
267
|
+
* @returns {Array<{patch: PatchV2, sha: string}>} Sorted patch entries
|
|
268
|
+
*/
|
|
269
|
+
export function _sortPatchesCausally(patches) {
|
|
270
|
+
return [...patches].sort((a, b) => {
|
|
271
|
+
// Primary: Lamport timestamp (ascending - earlier patches first)
|
|
272
|
+
const lamportDiff = (a.patch.lamport || 0) - (b.patch.lamport || 0);
|
|
273
|
+
if (lamportDiff !== 0) {
|
|
274
|
+
return lamportDiff;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Secondary: Writer ID (lexicographic)
|
|
278
|
+
const writerCmp = (a.patch.writer || '').localeCompare(b.patch.writer || '');
|
|
279
|
+
if (writerCmp !== 0) {
|
|
280
|
+
return writerCmp;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Tertiary: SHA (lexicographic) for total ordering
|
|
284
|
+
return a.sha.localeCompare(b.sha);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query methods for WarpGraph — pure reads on materialized state.
|
|
3
|
+
*
|
|
4
|
+
* Every function uses `this` bound to a WarpGraph instance at runtime
|
|
5
|
+
* via wireWarpMethods().
|
|
6
|
+
*
|
|
7
|
+
* @module domain/warp/query.methods
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { orsetContains, orsetElements } from '../crdt/ORSet.js';
|
|
11
|
+
import { decodePropKey, isEdgePropKey, decodeEdgePropKey, encodeEdgeKey, decodeEdgeKey } from '../services/KeyCodec.js';
|
|
12
|
+
import { compareEventIds } from '../utils/EventId.js';
|
|
13
|
+
import { cloneStateV5 } from '../services/JoinReducer.js';
|
|
14
|
+
import QueryBuilder from '../services/QueryBuilder.js';
|
|
15
|
+
import ObserverView from '../services/ObserverView.js';
|
|
16
|
+
import { computeTranslationCost } from '../services/TranslationCost.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a node exists in the materialized graph state.
|
|
20
|
+
*
|
|
21
|
+
* **Requires a cached state.** Call materialize() first if not already cached.
|
|
22
|
+
*
|
|
23
|
+
* @this {import('../WarpGraph.js').default}
|
|
24
|
+
* @param {string} nodeId - The node ID to check
|
|
25
|
+
* @returns {Promise<boolean>} True if the node exists in the materialized state
|
|
26
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
27
|
+
* @throws {import('../errors/QueryError.js').default} If cached state is dirty (code: `E_STALE_STATE`)
|
|
28
|
+
*/
|
|
29
|
+
export async function hasNode(nodeId) {
|
|
30
|
+
await this._ensureFreshState();
|
|
31
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
32
|
+
return orsetContains(s.nodeAlive, nodeId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets all properties for a node from the materialized state.
|
|
37
|
+
*
|
|
38
|
+
* @this {import('../WarpGraph.js').default}
|
|
39
|
+
* @param {string} nodeId - The node ID to get properties for
|
|
40
|
+
* @returns {Promise<Map<string, unknown>|null>} Map of property key → value, or null if node doesn't exist
|
|
41
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
42
|
+
*/
|
|
43
|
+
export async function getNodeProps(nodeId) {
|
|
44
|
+
await this._ensureFreshState();
|
|
45
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
46
|
+
|
|
47
|
+
if (!orsetContains(s.nodeAlive, nodeId)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const props = new Map();
|
|
52
|
+
for (const [propKey, register] of s.prop) {
|
|
53
|
+
const decoded = decodePropKey(propKey);
|
|
54
|
+
if (decoded.nodeId === nodeId) {
|
|
55
|
+
props.set(decoded.propKey, register.value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return props;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets all properties for an edge from the materialized state.
|
|
64
|
+
*
|
|
65
|
+
* @this {import('../WarpGraph.js').default}
|
|
66
|
+
* @param {string} from - Source node ID
|
|
67
|
+
* @param {string} to - Target node ID
|
|
68
|
+
* @param {string} label - Edge label
|
|
69
|
+
* @returns {Promise<Record<string, unknown>|null>} Object of property key → value, or null if edge doesn't exist
|
|
70
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
71
|
+
*/
|
|
72
|
+
export async function getEdgeProps(from, to, label) {
|
|
73
|
+
await this._ensureFreshState();
|
|
74
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
75
|
+
|
|
76
|
+
const edgeKey = encodeEdgeKey(from, to, label);
|
|
77
|
+
if (!orsetContains(s.edgeAlive, edgeKey)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!orsetContains(s.nodeAlive, from) ||
|
|
82
|
+
!orsetContains(s.nodeAlive, to)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const birthEvent = s.edgeBirthEvent?.get(edgeKey);
|
|
87
|
+
|
|
88
|
+
/** @type {Record<string, unknown>} */
|
|
89
|
+
const props = {};
|
|
90
|
+
for (const [propKey, register] of s.prop) {
|
|
91
|
+
if (!isEdgePropKey(propKey)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const decoded = decodeEdgePropKey(propKey);
|
|
95
|
+
if (decoded.from === from && decoded.to === to && decoded.label === label) {
|
|
96
|
+
if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
props[decoded.propKey] = register.value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return props;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets neighbors of a node from the materialized state.
|
|
108
|
+
*
|
|
109
|
+
* @this {import('../WarpGraph.js').default}
|
|
110
|
+
* @param {string} nodeId - The node ID to get neighbors for
|
|
111
|
+
* @param {'outgoing' | 'incoming' | 'both'} [direction='both'] - Edge direction to follow
|
|
112
|
+
* @param {string} [edgeLabel] - Optional edge label filter
|
|
113
|
+
* @returns {Promise<Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>>} Array of neighbor info
|
|
114
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
115
|
+
*/
|
|
116
|
+
export async function neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
|
|
117
|
+
await this._ensureFreshState();
|
|
118
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
119
|
+
|
|
120
|
+
/** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
|
|
121
|
+
const result = [];
|
|
122
|
+
|
|
123
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
124
|
+
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
125
|
+
|
|
126
|
+
if (edgeLabel !== undefined && label !== edgeLabel) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
|
|
131
|
+
if (orsetContains(s.nodeAlive, to)) {
|
|
132
|
+
result.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
|
|
137
|
+
if (orsetContains(s.nodeAlive, from)) {
|
|
138
|
+
result.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns a defensive copy of the current materialized state.
|
|
148
|
+
*
|
|
149
|
+
* @this {import('../WarpGraph.js').default}
|
|
150
|
+
* @returns {Promise<import('../services/JoinReducer.js').WarpStateV5 | null>}
|
|
151
|
+
*/
|
|
152
|
+
export async function getStateSnapshot() {
|
|
153
|
+
if (!this._cachedState && !this._autoMaterialize) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
await this._ensureFreshState();
|
|
157
|
+
if (!this._cachedState) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return cloneStateV5(/** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Gets all visible nodes in the materialized state.
|
|
165
|
+
*
|
|
166
|
+
* @this {import('../WarpGraph.js').default}
|
|
167
|
+
* @returns {Promise<string[]>} Array of node IDs
|
|
168
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
169
|
+
*/
|
|
170
|
+
export async function getNodes() {
|
|
171
|
+
await this._ensureFreshState();
|
|
172
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
173
|
+
return [...orsetElements(s.nodeAlive)];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Gets all visible edges in the materialized state.
|
|
178
|
+
*
|
|
179
|
+
* @this {import('../WarpGraph.js').default}
|
|
180
|
+
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, unknown>}>>} Array of edge info
|
|
181
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
182
|
+
*/
|
|
183
|
+
export async function getEdges() {
|
|
184
|
+
await this._ensureFreshState();
|
|
185
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
186
|
+
|
|
187
|
+
const edgePropsByKey = new Map();
|
|
188
|
+
for (const [propKey, register] of s.prop) {
|
|
189
|
+
if (!isEdgePropKey(propKey)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const decoded = decodeEdgePropKey(propKey);
|
|
193
|
+
const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
|
|
194
|
+
|
|
195
|
+
const birthEvent = s.edgeBirthEvent?.get(ek);
|
|
196
|
+
if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let bag = edgePropsByKey.get(ek);
|
|
201
|
+
if (!bag) {
|
|
202
|
+
bag = {};
|
|
203
|
+
edgePropsByKey.set(ek, bag);
|
|
204
|
+
}
|
|
205
|
+
bag[decoded.propKey] = register.value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const edges = [];
|
|
209
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
210
|
+
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
211
|
+
if (orsetContains(s.nodeAlive, from) &&
|
|
212
|
+
orsetContains(s.nodeAlive, to)) {
|
|
213
|
+
const props = edgePropsByKey.get(edgeKey) || {};
|
|
214
|
+
edges.push({ from, to, label, props });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return edges;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns the number of property entries in the materialized state.
|
|
222
|
+
*
|
|
223
|
+
* @this {import('../WarpGraph.js').default}
|
|
224
|
+
* @returns {Promise<number>} Number of property entries
|
|
225
|
+
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
226
|
+
*/
|
|
227
|
+
export async function getPropertyCount() {
|
|
228
|
+
await this._ensureFreshState();
|
|
229
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
230
|
+
return s.prop.size;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates a fluent query builder for the logical graph.
|
|
235
|
+
*
|
|
236
|
+
* @this {import('../WarpGraph.js').default}
|
|
237
|
+
* @returns {import('../services/QueryBuilder.js').default} A fluent query builder
|
|
238
|
+
*/
|
|
239
|
+
export function query() {
|
|
240
|
+
return new QueryBuilder(this);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Creates a read-only observer view of the current materialized state.
|
|
245
|
+
*
|
|
246
|
+
* @this {import('../WarpGraph.js').default}
|
|
247
|
+
* @param {string} name - Observer name
|
|
248
|
+
* @param {Object} config - Observer configuration
|
|
249
|
+
* @param {string} config.match - Glob pattern for visible nodes
|
|
250
|
+
* @param {string[]} [config.expose] - Property keys to include
|
|
251
|
+
* @param {string[]} [config.redact] - Property keys to exclude
|
|
252
|
+
* @returns {Promise<import('../services/ObserverView.js').default>} A read-only observer view
|
|
253
|
+
*/
|
|
254
|
+
export async function observer(name, config) {
|
|
255
|
+
if (!config || typeof config.match !== 'string') {
|
|
256
|
+
throw new Error('observer config.match must be a string');
|
|
257
|
+
}
|
|
258
|
+
await this._ensureFreshState();
|
|
259
|
+
return new ObserverView({ name, config, graph: this });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Computes the directed MDL translation cost from observer A to observer B.
|
|
264
|
+
*
|
|
265
|
+
* @this {import('../WarpGraph.js').default}
|
|
266
|
+
* @param {Object} configA - Observer configuration for A
|
|
267
|
+
* @param {string} configA.match - Glob pattern for visible nodes
|
|
268
|
+
* @param {string[]} [configA.expose] - Property keys to include
|
|
269
|
+
* @param {string[]} [configA.redact] - Property keys to exclude
|
|
270
|
+
* @param {Object} configB - Observer configuration for B
|
|
271
|
+
* @param {string} configB.match - Glob pattern for visible nodes
|
|
272
|
+
* @param {string[]} [configB.expose] - Property keys to include
|
|
273
|
+
* @param {string[]} [configB.redact] - Property keys to exclude
|
|
274
|
+
* @returns {Promise<{cost: number, breakdown: {nodeLoss: number, edgeLoss: number, propLoss: number}}>}
|
|
275
|
+
*/
|
|
276
|
+
export async function translationCost(configA, configB) {
|
|
277
|
+
await this._ensureFreshState();
|
|
278
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
279
|
+
return computeTranslationCost(configA, configB, s);
|
|
280
|
+
}
|