@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.
Files changed (136) 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 +80 -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/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. 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
+ }