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