@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,554 @@
1
+ /**
2
+ * Patch/writer methods for WarpGraph — state mutation, writer lifecycle,
3
+ * discovery, and CRDT join.
4
+ *
5
+ * Every function uses `this` bound to a WarpGraph instance at runtime
6
+ * via wireWarpMethods().
7
+ *
8
+ * @module domain/warp/patch.methods
9
+ */
10
+
11
+ import { QueryError, E_NO_STATE_MSG, E_STALE_STATE_MSG } from './_internal.js';
12
+ import { PatchBuilderV2 } from '../services/PatchBuilderV2.js';
13
+ import { joinStates, join as joinPatch } from '../services/JoinReducer.js';
14
+ import { orsetElements } from '../crdt/ORSet.js';
15
+ import { vvIncrement } from '../crdt/VersionVector.js';
16
+ import { buildWriterRef, buildWritersPrefix, parseWriterIdFromRef } from '../utils/RefLayout.js';
17
+ import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
18
+ import { Writer } from './Writer.js';
19
+ import { generateWriterId, resolveWriterId } from '../utils/WriterId.js';
20
+
21
+ /** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
22
+
23
+ /**
24
+ * Creates a new PatchBuilderV2 for this graph.
25
+ *
26
+ * In multi-writer scenarios, call `materialize()` (or a query method that
27
+ * auto-materializes) before creating a patch so that `_maxObservedLamport`
28
+ * reflects all known writers. Without this, `_nextLamport()` still produces
29
+ * locally-monotonic ticks (`Math.max(ownTick, _maxObservedLamport) + 1`),
30
+ * and `PatchBuilderV2.commit()` re-reads the writer's own ref at commit
31
+ * time, so correctness is preserved — but the tick may be lower than
32
+ * necessary, losing LWW tiebreakers against other writers.
33
+ *
34
+ * @this {import('../WarpGraph.js').default}
35
+ * @returns {Promise<PatchBuilderV2>} A new patch builder
36
+ */
37
+ export async function createPatch() {
38
+ const { lamport, parentSha } = await this._nextLamport();
39
+ return new PatchBuilderV2({
40
+ persistence: this._persistence,
41
+ graphName: this._graphName,
42
+ writerId: this._writerId,
43
+ lamport,
44
+ versionVector: this._versionVector,
45
+ getCurrentState: () => this._cachedState,
46
+ expectedParentSha: parentSha,
47
+ onDeleteWithData: this._onDeleteWithData,
48
+ onCommitSuccess: (/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
49
+ codec: this._codec,
50
+ logger: this._logger || undefined,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Convenience wrapper: creates a patch, runs the callback, and commits.
56
+ *
57
+ * The callback receives a `PatchBuilderV2` and may be synchronous or
58
+ * asynchronous. The commit happens only after the callback resolves
59
+ * successfully. If the callback throws or rejects, no commit is attempted
60
+ * and the error propagates untouched.
61
+ *
62
+ * Not reentrant: calling `graph.patch()` inside a callback throws.
63
+ * Use `createPatch()` directly for advanced multi-patch workflows.
64
+ *
65
+ * **Multi-writer note:** call `materialize()` before `patch()` so that
66
+ * `_maxObservedLamport` is up-to-date. See `createPatch()` for details.
67
+ *
68
+ * @this {import('../WarpGraph.js').default}
69
+ * @param {(p: PatchBuilderV2) => void | Promise<void>} build - Callback that adds operations to the patch
70
+ * @returns {Promise<string>} The commit SHA of the new patch
71
+ *
72
+ * @example
73
+ * const sha = await graph.patch(p => {
74
+ * p.addNode('user:alice');
75
+ * p.setProperty('user:alice', 'name', 'Alice');
76
+ * });
77
+ */
78
+ export async function patch(build) {
79
+ if (this._patchInProgress) {
80
+ throw new Error(
81
+ 'graph.patch() is not reentrant. Use createPatch() for nested or concurrent patches.',
82
+ );
83
+ }
84
+ this._patchInProgress = true;
85
+ try {
86
+ const p = await this.createPatch();
87
+ await build(p);
88
+ return await p.commit();
89
+ } finally {
90
+ this._patchInProgress = false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Gets the next lamport timestamp and current parent SHA for this writer.
96
+ * Reads from the current ref chain to determine values.
97
+ *
98
+ * @this {import('../WarpGraph.js').default}
99
+ * @returns {Promise<{lamport: number, parentSha: string|null}>} The next lamport and current parent
100
+ */
101
+ export async function _nextLamport() {
102
+ const writerRef = buildWriterRef(this._graphName, this._writerId);
103
+ const currentRefSha = await this._persistence.readRef(writerRef);
104
+
105
+ let ownTick = 0;
106
+
107
+ if (currentRefSha) {
108
+ // Read the current patch commit to get its lamport timestamp
109
+ const commitMessage = await this._persistence.showNode(currentRefSha);
110
+ const kind = detectMessageKind(commitMessage);
111
+
112
+ if (kind === 'patch') {
113
+ try {
114
+ const patchInfo = decodePatchMessage(commitMessage);
115
+ ownTick = patchInfo.lamport;
116
+ } catch (err) {
117
+ throw new Error(
118
+ `Failed to parse lamport from writer ref ${writerRef}: ` +
119
+ `commit ${currentRefSha} has invalid patch message format`,
120
+ { cause: err }
121
+ );
122
+ }
123
+ }
124
+ // Non-patch ref: ownTick stays 0 (fresh start), falls through to standard return.
125
+ }
126
+
127
+ // Standard Lamport clock rule: next tick = max(own chain, globally observed max) + 1.
128
+ // _maxObservedLamport is updated during materialize() and after each commit, so this
129
+ // is O(1) — no additional git reads required at commit time.
130
+ return {
131
+ lamport: Math.max(ownTick, this._maxObservedLamport) + 1,
132
+ parentSha: currentRefSha ?? null,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Loads all patches from a writer's ref chain.
138
+ *
139
+ * Walks commits from the tip SHA back to the first patch commit,
140
+ * collecting all patches along the way.
141
+ *
142
+ * @this {import('../WarpGraph.js').default}
143
+ * @param {string} writerId - The writer ID to load patches for
144
+ * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
145
+ * @returns {Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
146
+ */
147
+ export async function _loadWriterPatches(writerId, stopAtSha = null) {
148
+ const writerRef = buildWriterRef(this._graphName, writerId);
149
+ const tipSha = await this._persistence.readRef(writerRef);
150
+
151
+ if (!tipSha) {
152
+ return [];
153
+ }
154
+
155
+ const patches = [];
156
+ let currentSha = tipSha;
157
+
158
+ while (currentSha && currentSha !== stopAtSha) {
159
+ // Get commit info and message
160
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
161
+ const {message} = nodeInfo;
162
+
163
+ // Check if this is a patch commit
164
+ const kind = detectMessageKind(message);
165
+ if (kind !== 'patch') {
166
+ // Not a patch commit, stop walking
167
+ break;
168
+ }
169
+
170
+ // Decode the patch message to get patchOid
171
+ const patchMeta = decodePatchMessage(message);
172
+
173
+ // Read the patch blob
174
+ const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
175
+ const decoded = /** @type {import('../types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
176
+
177
+ patches.push({ patch: decoded, sha: currentSha });
178
+
179
+ // Move to parent commit
180
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
181
+ currentSha = nodeInfo.parents[0];
182
+ } else {
183
+ break;
184
+ }
185
+ }
186
+
187
+ // Patches are collected in reverse order (newest first), reverse them
188
+ return patches.reverse();
189
+ }
190
+
191
+ /**
192
+ * Returns patches from a writer's ref chain.
193
+ *
194
+ * @this {import('../WarpGraph.js').default}
195
+ * @param {string} writerId - The writer ID to load patches for
196
+ * @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
197
+ * @returns {Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
198
+ */
199
+ export async function getWriterPatches(writerId, stopAtSha = null) {
200
+ return await this._loadWriterPatches(writerId, stopAtSha);
201
+ }
202
+
203
+ /**
204
+ * Post-commit hook: updates version vector, eager re-materialize,
205
+ * provenance index, frontier, and audit service.
206
+ *
207
+ * @this {import('../WarpGraph.js').default}
208
+ * @param {string} writerId - The writer that committed
209
+ * @param {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} [opts]
210
+ * @returns {Promise<void>}
211
+ */
212
+ export async function _onPatchCommitted(writerId, { patch: committed, sha } = {}) {
213
+ vvIncrement(this._versionVector, writerId);
214
+ // Keep _maxObservedLamport up to date so _nextLamport() issues globally-monotonic ticks.
215
+ if (committed?.lamport !== undefined && committed.lamport > this._maxObservedLamport) {
216
+ this._maxObservedLamport = committed.lamport;
217
+ }
218
+ this._patchesSinceCheckpoint++;
219
+ // Eager re-materialize: apply the just-committed patch to cached state
220
+ // Only when the cache is clean — applying a patch to stale state would be incorrect
221
+ if (this._cachedState && !this._stateDirty && committed && sha) {
222
+ let tickReceipt = null;
223
+ if (this._auditService) {
224
+ const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}} */ (
225
+ joinPatch(this._cachedState, /** @type {Parameters<typeof joinPatch>[1]} */ (committed), sha, true)
226
+ );
227
+ tickReceipt = result.receipt;
228
+ } else {
229
+ joinPatch(this._cachedState, /** @type {Parameters<typeof joinPatch>[1]} */ (committed), sha);
230
+ }
231
+ await this._setMaterializedState(this._cachedState);
232
+ // Update provenance index with new patch
233
+ if (this._provenanceIndex) {
234
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (committed.reads), /** @type {string[]|undefined} */ (committed.writes));
235
+ }
236
+ // Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
237
+ if (this._lastFrontier) {
238
+ this._lastFrontier.set(writerId, sha);
239
+ }
240
+ // Audit receipt — AFTER all state updates succeed
241
+ if (this._auditService && tickReceipt) {
242
+ try {
243
+ await this._auditService.commit(tickReceipt);
244
+ } catch {
245
+ // Data commit already succeeded. Logged inside service.
246
+ }
247
+ }
248
+ } else {
249
+ this._stateDirty = true;
250
+ if (this._auditService) {
251
+ this._auditSkipCount++;
252
+ this._logger?.warn('[warp:audit]', {
253
+ code: 'AUDIT_SKIPPED_DIRTY_STATE',
254
+ sha,
255
+ skipCount: this._auditSkipCount,
256
+ });
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Creates a Writer bound to an existing (or resolved) writer ID.
263
+ *
264
+ * @this {import('../WarpGraph.js').default}
265
+ * @param {string} writerId - The writer ID to resolve
266
+ * @returns {Promise<Writer>} A Writer instance
267
+ */
268
+ export async function writer(writerId) {
269
+ // Build config adapters for resolveWriterId
270
+ const configGet = async (/** @type {string} */ key) => await this._persistence.configGet(key);
271
+ const configSet = async (/** @type {string} */ key, /** @type {string} */ value) => await this._persistence.configSet(key, value);
272
+
273
+ // Resolve the writer ID
274
+ const resolvedWriterId = await resolveWriterId({
275
+ graphName: this._graphName,
276
+ explicitWriterId: writerId,
277
+ configGet,
278
+ configSet,
279
+ });
280
+
281
+ /** @type {CorePersistence} */
282
+ const persistence = this._persistence;
283
+ return new Writer({
284
+ persistence,
285
+ graphName: this._graphName,
286
+ writerId: resolvedWriterId,
287
+ versionVector: this._versionVector,
288
+ getCurrentState: /** @type {() => Promise<import('../services/JoinReducer.js').WarpStateV5>} */ (/** @type {unknown} */ (() => this._cachedState)),
289
+ onDeleteWithData: this._onDeleteWithData,
290
+ onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(resolvedWriterId, opts))),
291
+ codec: this._codec,
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Creates a new Writer with a fresh canonical ID.
297
+ *
298
+ * This always generates a new unique writer ID, regardless of any
299
+ * existing configuration. Use this when you need a guaranteed fresh
300
+ * identity (e.g., spawning a new writer process).
301
+ *
302
+ * @deprecated Use `writer()` to resolve a stable ID from git config, or `writer(id)` with an explicit ID.
303
+ * @this {import('../WarpGraph.js').default}
304
+ * @param {Object} [opts]
305
+ * @param {'config'|'none'} [opts.persist='none'] - Whether to persist the new ID to git config
306
+ * @param {string} [opts.alias] - Optional alias for config key (used with persist:'config')
307
+ * @returns {Promise<Writer>} A Writer instance with new canonical ID
308
+ * @throws {Error} If config operations fail (when persist:'config')
309
+ *
310
+ * @example
311
+ * // Create ephemeral writer (not persisted)
312
+ * const writer = await graph.createWriter();
313
+ *
314
+ * @example
315
+ * // Create and persist to git config
316
+ * const writer = await graph.createWriter({ persist: 'config' });
317
+ */
318
+ export async function createWriter(opts = {}) {
319
+ if (this._logger) {
320
+ this._logger.warn('[warp] createWriter() is deprecated. Use writer() or writer(id) instead.');
321
+ } else {
322
+ // eslint-disable-next-line no-console
323
+ console.warn('[warp] createWriter() is deprecated. Use writer() or writer(id) instead.');
324
+ }
325
+
326
+ const { persist = 'none', alias } = opts;
327
+
328
+ // Generate new canonical writerId
329
+ const freshWriterId = generateWriterId();
330
+
331
+ // Optionally persist to git config
332
+ if (persist === 'config') {
333
+ const configKey = alias
334
+ ? `warp.writerId.${alias}`
335
+ : `warp.writerId.${this._graphName}`;
336
+ await this._persistence.configSet(configKey, freshWriterId);
337
+ }
338
+
339
+ /** @type {CorePersistence} */
340
+ const writerPersistence = this._persistence;
341
+ return new Writer({
342
+ persistence: writerPersistence,
343
+ graphName: this._graphName,
344
+ writerId: freshWriterId,
345
+ versionVector: this._versionVector,
346
+ getCurrentState: /** @type {() => Promise<import('../services/JoinReducer.js').WarpStateV5>} */ (/** @type {unknown} */ (() => this._cachedState)),
347
+ onDeleteWithData: this._onDeleteWithData,
348
+ onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts))),
349
+ codec: this._codec,
350
+ });
351
+ }
352
+
353
+ /**
354
+ * Ensures cached state is fresh. When autoMaterialize is enabled,
355
+ * materializes if state is null or dirty. Otherwise throws.
356
+ *
357
+ * @this {import('../WarpGraph.js').default}
358
+ * @returns {Promise<void>}
359
+ * @throws {QueryError} If no cached state and autoMaterialize is off (code: `E_NO_STATE`)
360
+ * @throws {QueryError} If cached state is dirty and autoMaterialize is off (code: `E_STALE_STATE`)
361
+ */
362
+ export async function _ensureFreshState() {
363
+ if (this._autoMaterialize && (!this._cachedState || this._stateDirty)) {
364
+ await this.materialize();
365
+ return;
366
+ }
367
+ if (!this._cachedState) {
368
+ throw new QueryError(
369
+ E_NO_STATE_MSG,
370
+ { code: 'E_NO_STATE' },
371
+ );
372
+ }
373
+ if (this._stateDirty) {
374
+ throw new QueryError(
375
+ E_STALE_STATE_MSG,
376
+ { code: 'E_STALE_STATE' },
377
+ );
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Discovers all writers that have written to this graph.
383
+ *
384
+ * @this {import('../WarpGraph.js').default}
385
+ * @returns {Promise<string[]>} Sorted array of writer IDs
386
+ */
387
+ export async function discoverWriters() {
388
+ const prefix = buildWritersPrefix(this._graphName);
389
+ const refs = await this._persistence.listRefs(prefix);
390
+
391
+ const writerIds = [];
392
+ for (const refPath of refs) {
393
+ const writerId = parseWriterIdFromRef(refPath);
394
+ if (writerId) {
395
+ writerIds.push(writerId);
396
+ }
397
+ }
398
+
399
+ return writerIds.sort();
400
+ }
401
+
402
+ /**
403
+ * Discovers all distinct Lamport ticks across all writers.
404
+ *
405
+ * Walks each writer's patch chain from tip to root, reading commit
406
+ * messages (no CBOR blob deserialization) to extract Lamport timestamps.
407
+ * Stops when a non-patch commit (e.g. checkpoint) is encountered.
408
+ * Logs a warning for any non-monotonic lamport sequence within a single
409
+ * writer's chain.
410
+ *
411
+ * @this {import('../WarpGraph.js').default}
412
+ * @returns {Promise<{
413
+ * ticks: number[],
414
+ * maxTick: number,
415
+ * perWriter: Map<string, {ticks: number[], tipSha: string|null, tickShas: Record<number, string>}>
416
+ * }>} `ticks` is the sorted (ascending) deduplicated union of all
417
+ * Lamport values; `maxTick` is the largest value (0 if none);
418
+ * `perWriter` maps each writer ID to its ticks in ascending order
419
+ * and its current tip SHA (or `null` if the writer ref is missing)
420
+ * @throws {Error} If reading refs or commit metadata fails
421
+ */
422
+ export async function discoverTicks() {
423
+ const writerIds = await this.discoverWriters();
424
+ /** @type {Set<number>} */
425
+ const globalTickSet = new Set();
426
+ const perWriter = new Map();
427
+
428
+ for (const writerId of writerIds) {
429
+ const writerRef = buildWriterRef(this._graphName, writerId);
430
+ const tipSha = await this._persistence.readRef(writerRef);
431
+ const writerTicks = [];
432
+ /** @type {Record<number, string>} */
433
+ const tickShas = {};
434
+
435
+ if (tipSha) {
436
+ let currentSha = tipSha;
437
+ let lastLamport = Infinity;
438
+
439
+ while (currentSha) {
440
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
441
+ const kind = detectMessageKind(nodeInfo.message);
442
+ if (kind !== 'patch') {
443
+ break;
444
+ }
445
+
446
+ const patchMeta = decodePatchMessage(nodeInfo.message);
447
+ globalTickSet.add(patchMeta.lamport);
448
+ writerTicks.push(patchMeta.lamport);
449
+ tickShas[patchMeta.lamport] = currentSha;
450
+
451
+ // Check monotonic invariant (walking newest->oldest, lamport should decrease)
452
+ if (patchMeta.lamport > lastLamport && this._logger) {
453
+ this._logger.warn(`[warp] non-monotonic lamport for writer ${writerId}: ${patchMeta.lamport} > ${lastLamport}`);
454
+ }
455
+ lastLamport = patchMeta.lamport;
456
+
457
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
458
+ currentSha = nodeInfo.parents[0];
459
+ } else {
460
+ break;
461
+ }
462
+ }
463
+ }
464
+
465
+ perWriter.set(writerId, {
466
+ ticks: writerTicks.reverse(),
467
+ tipSha: tipSha || null,
468
+ tickShas,
469
+ });
470
+ }
471
+
472
+ const ticks = [...globalTickSet].sort((a, b) => a - b);
473
+ const maxTick = ticks.length > 0 ? ticks[ticks.length - 1] : 0;
474
+
475
+ return { ticks, maxTick, perWriter };
476
+ }
477
+
478
+ /**
479
+ * Joins an external WarpStateV5 into the cached state using CRDT merge.
480
+ *
481
+ * @this {import('../WarpGraph.js').default}
482
+ * @param {import('../services/JoinReducer.js').WarpStateV5} otherState - The state to merge in
483
+ * @returns {{state: import('../services/JoinReducer.js').WarpStateV5, receipt: Object}} Merged state and receipt
484
+ * @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
485
+ * @throws {Error} If otherState is invalid
486
+ */
487
+ export function join(otherState) {
488
+ if (!this._cachedState) {
489
+ throw new QueryError(E_NO_STATE_MSG, {
490
+ code: 'E_NO_STATE',
491
+ });
492
+ }
493
+
494
+ if (!otherState || !otherState.nodeAlive || !otherState.edgeAlive) {
495
+ throw new Error('Invalid state: must be a valid WarpStateV5 object');
496
+ }
497
+
498
+ // Capture pre-merge counts for receipt
499
+ const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
500
+ const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
501
+ const beforeFrontierSize = this._cachedState.observedFrontier.size;
502
+
503
+ // Perform the join
504
+ const mergedState = joinStates(this._cachedState, otherState);
505
+
506
+ // Calculate receipt
507
+ const afterNodes = orsetElements(mergedState.nodeAlive).length;
508
+ const afterEdges = orsetElements(mergedState.edgeAlive).length;
509
+ const afterFrontierSize = mergedState.observedFrontier.size;
510
+
511
+ // Count property changes (keys that existed in both but have different values)
512
+ let propsChanged = 0;
513
+ for (const [key, reg] of mergedState.prop) {
514
+ const oldReg = this._cachedState.prop.get(key);
515
+ if (!oldReg || oldReg.value !== reg.value) {
516
+ propsChanged++;
517
+ }
518
+ }
519
+
520
+ const receipt = {
521
+ nodesAdded: Math.max(0, afterNodes - beforeNodes),
522
+ nodesRemoved: Math.max(0, beforeNodes - afterNodes),
523
+ edgesAdded: Math.max(0, afterEdges - beforeEdges),
524
+ edgesRemoved: Math.max(0, beforeEdges - afterEdges),
525
+ propsChanged,
526
+ frontierMerged: afterFrontierSize !== beforeFrontierSize ||
527
+ !this._frontierEquals(this._cachedState.observedFrontier, mergedState.observedFrontier),
528
+ };
529
+
530
+ // Update cached state
531
+ this._cachedState = mergedState;
532
+
533
+ return { state: mergedState, receipt };
534
+ }
535
+
536
+ /**
537
+ * Compares two version vectors for equality.
538
+ *
539
+ * @this {import('../WarpGraph.js').default}
540
+ * @param {import('../crdt/VersionVector.js').VersionVector} a
541
+ * @param {import('../crdt/VersionVector.js').VersionVector} b
542
+ * @returns {boolean}
543
+ */
544
+ export function _frontierEquals(a, b) {
545
+ if (a.size !== b.size) {
546
+ return false;
547
+ }
548
+ for (const [key, val] of a) {
549
+ if (b.get(key) !== val) {
550
+ return false;
551
+ }
552
+ }
553
+ return true;
554
+ }