@git-stunts/git-warp 10.1.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Patch message encoding and decoding for WARP commit messages.
3
+ *
4
+ * Handles the 'patch' message type which contains graph mutations from a
5
+ * single writer. See {@link module:domain/services/WarpMessageCodec} for the
6
+ * facade that re-exports all codec functions.
7
+ *
8
+ * @module domain/services/PatchMessageCodec
9
+ */
10
+
11
+ import { validateGraphName, validateWriterId } from '../utils/RefLayout.js';
12
+ import {
13
+ getCodec,
14
+ MESSAGE_TITLES,
15
+ TRAILER_KEYS,
16
+ validateOid,
17
+ validatePositiveInteger,
18
+ validateSchema,
19
+ } from './MessageCodecInternal.js';
20
+
21
+ // -----------------------------------------------------------------------------
22
+ // Encoder
23
+ // -----------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Encodes a patch commit message.
27
+ *
28
+ * @param {Object} options - The patch message options
29
+ * @param {string} options.graph - The graph name
30
+ * @param {string} options.writer - The writer ID
31
+ * @param {number} options.lamport - The Lamport timestamp (must be a positive integer)
32
+ * @param {string} options.patchOid - The OID of the patch blob
33
+ * @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
34
+ * @returns {string} The encoded commit message
35
+ * @throws {Error} If any validation fails
36
+ *
37
+ * @example
38
+ * const message = encodePatchMessage({
39
+ * graph: 'events',
40
+ * writer: 'node-1',
41
+ * lamport: 42,
42
+ * patchOid: 'abc123...' // 40-char hex
43
+ * });
44
+ */
45
+ export function encodePatchMessage({ graph, writer, lamport, patchOid, schema = 2 }) {
46
+ // Validate inputs
47
+ validateGraphName(graph);
48
+ validateWriterId(writer);
49
+ validatePositiveInteger(lamport, 'lamport');
50
+ validateOid(patchOid, 'patchOid');
51
+ validateSchema(schema);
52
+
53
+ const codec = getCodec();
54
+ return codec.encode({
55
+ title: MESSAGE_TITLES.patch,
56
+ trailers: {
57
+ [TRAILER_KEYS.kind]: 'patch',
58
+ [TRAILER_KEYS.graph]: graph,
59
+ [TRAILER_KEYS.writer]: writer,
60
+ [TRAILER_KEYS.lamport]: String(lamport),
61
+ [TRAILER_KEYS.patchOid]: patchOid,
62
+ [TRAILER_KEYS.schema]: String(schema),
63
+ },
64
+ });
65
+ }
66
+
67
+ // -----------------------------------------------------------------------------
68
+ // Decoder
69
+ // -----------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Decodes a patch commit message.
73
+ *
74
+ * @param {string} message - The raw commit message
75
+ * @returns {Object} The decoded patch message
76
+ * @returns {string} return.kind - Always 'patch'
77
+ * @returns {string} return.graph - The graph name
78
+ * @returns {string} return.writer - The writer ID
79
+ * @returns {number} return.lamport - The Lamport timestamp
80
+ * @returns {string} return.patchOid - The patch blob OID
81
+ * @returns {number} return.schema - The schema version
82
+ * @throws {Error} If the message is not a valid patch message
83
+ *
84
+ * @example
85
+ * const { kind, graph, writer, lamport, patchOid, schema } = decodePatchMessage(message);
86
+ */
87
+ export function decodePatchMessage(message) {
88
+ const codec = getCodec();
89
+ const decoded = codec.decode(message);
90
+ const { trailers } = decoded;
91
+
92
+ // Validate kind discriminator
93
+ const kind = trailers[TRAILER_KEYS.kind];
94
+ if (kind !== 'patch') {
95
+ throw new Error(`Invalid patch message: eg-kind must be 'patch', got '${kind}'`);
96
+ }
97
+
98
+ // Extract and validate required fields
99
+ const graph = trailers[TRAILER_KEYS.graph];
100
+ if (!graph) {
101
+ throw new Error('Invalid patch message: missing required trailer eg-graph');
102
+ }
103
+
104
+ const writer = trailers[TRAILER_KEYS.writer];
105
+ if (!writer) {
106
+ throw new Error('Invalid patch message: missing required trailer eg-writer');
107
+ }
108
+
109
+ const lamportStr = trailers[TRAILER_KEYS.lamport];
110
+ if (!lamportStr) {
111
+ throw new Error('Invalid patch message: missing required trailer eg-lamport');
112
+ }
113
+ const lamport = parseInt(lamportStr, 10);
114
+ if (!Number.isInteger(lamport) || lamport < 1) {
115
+ throw new Error(`Invalid patch message: eg-lamport must be a positive integer, got '${lamportStr}'`);
116
+ }
117
+
118
+ const patchOid = trailers[TRAILER_KEYS.patchOid];
119
+ if (!patchOid) {
120
+ throw new Error('Invalid patch message: missing required trailer eg-patch-oid');
121
+ }
122
+
123
+ const schemaStr = trailers[TRAILER_KEYS.schema];
124
+ if (!schemaStr) {
125
+ throw new Error('Invalid patch message: missing required trailer eg-schema');
126
+ }
127
+ const schema = parseInt(schemaStr, 10);
128
+ if (!Number.isInteger(schema) || schema < 1) {
129
+ throw new Error(`Invalid patch message: eg-schema must be a positive integer, got '${schemaStr}'`);
130
+ }
131
+
132
+ return {
133
+ kind: 'patch',
134
+ graph,
135
+ writer,
136
+ lamport,
137
+ patchOid,
138
+ schema,
139
+ };
140
+ }
@@ -0,0 +1,337 @@
1
+ import defaultCodec from '../utils/defaultCodec.js';
2
+
3
+ /**
4
+ * ProvenanceIndex - Node-to-Patch SHA Index
5
+ *
6
+ * Implements HG/IO/2: Build nodeId-to-patchSha index from I/O declarations.
7
+ * This enables quick answers to "which patches affected node X?" without
8
+ * replaying all patches.
9
+ *
10
+ * The index maps:
11
+ * - nodeId -> Set<patchSha> (patches that read or wrote this node)
12
+ *
13
+ * This supports the computational holography theorem from Paper III:
14
+ * given a target node, we can compute its backward causal cone D(v)
15
+ * by walking the index.
16
+ *
17
+ * @module domain/services/ProvenanceIndex
18
+ */
19
+
20
+ /**
21
+ * ProvenanceIndex - Maps node/edge IDs to contributing patch SHAs.
22
+ *
23
+ * This index is built incrementally during materialization by extracting
24
+ * the `reads` and `writes` arrays from each patch's I/O declarations
25
+ * (implemented by HG/IO/1).
26
+ *
27
+ * ## Usage
28
+ *
29
+ * ```javascript
30
+ * const index = new ProvenanceIndex();
31
+ *
32
+ * // During materialization, add each patch's I/O declarations
33
+ * index.addPatch(patchSha, patch.reads, patch.writes);
34
+ *
35
+ * // Query: which patches affected this node?
36
+ * const shas = index.patchesFor('user:alice');
37
+ * // Returns: ['abc123', 'def456', ...]
38
+ * ```
39
+ *
40
+ * ## Persistence
41
+ *
42
+ * The index can be serialized for checkpoint storage:
43
+ * ```javascript
44
+ * const buffer = index.serialize();
45
+ * // Store in checkpoint tree as provenanceIndex.cbor
46
+ *
47
+ * // Later, restore from checkpoint
48
+ * const index = ProvenanceIndex.deserialize(buffer);
49
+ * ```
50
+ */
51
+ class ProvenanceIndex {
52
+ /**
53
+ * Internal index mapping nodeId/edgeKey to Set of patch SHAs.
54
+ * @type {Map<string, Set<string>>}
55
+ * @private
56
+ */
57
+ #index;
58
+
59
+ /**
60
+ * Creates a new ProvenanceIndex.
61
+ *
62
+ * @param {Map<string, Set<string>>} [initialIndex] - Optional initial index data (defensively copied)
63
+ */
64
+ constructor(initialIndex) {
65
+ if (initialIndex) {
66
+ // Defensive copy to prevent external mutation
67
+ this.#index = new Map();
68
+ for (const [k, v] of initialIndex) {
69
+ this.#index.set(k, new Set(v));
70
+ }
71
+ } else {
72
+ this.#index = new Map();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Creates an empty ProvenanceIndex.
78
+ *
79
+ * @returns {ProvenanceIndex} A fresh, empty index
80
+ */
81
+ static empty() {
82
+ return new ProvenanceIndex();
83
+ }
84
+
85
+ /**
86
+ * Adds a patch's I/O declarations to the index.
87
+ *
88
+ * Both reads and writes are indexed because both indicate that
89
+ * the patch "affected" the entity:
90
+ * - Writes: the patch modified the entity
91
+ * - Reads: the patch's result depends on the entity's state
92
+ *
93
+ * This enables computing the full backward causal cone for slicing.
94
+ *
95
+ * @param {string} patchSha - The Git SHA of the patch commit
96
+ * @param {string[]|undefined} reads - Array of nodeIds/edgeKeys read by this patch
97
+ * @param {string[]|undefined} writes - Array of nodeIds/edgeKeys written by this patch
98
+ * @returns {ProvenanceIndex} This index for chaining
99
+ */
100
+ addPatch(patchSha, reads, writes) {
101
+ // Index all reads
102
+ if (reads && reads.length > 0) {
103
+ for (const entityId of reads) {
104
+ this.#addEntry(entityId, patchSha);
105
+ }
106
+ }
107
+
108
+ // Index all writes
109
+ if (writes && writes.length > 0) {
110
+ for (const entityId of writes) {
111
+ this.#addEntry(entityId, patchSha);
112
+ }
113
+ }
114
+
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Adds a single entry to the index.
120
+ *
121
+ * @param {string} entityId - The node ID or edge key
122
+ * @param {string} patchSha - The patch SHA
123
+ * @private
124
+ */
125
+ #addEntry(entityId, patchSha) {
126
+ let shas = this.#index.get(entityId);
127
+ if (!shas) {
128
+ shas = new Set();
129
+ this.#index.set(entityId, shas);
130
+ }
131
+ shas.add(patchSha);
132
+ }
133
+
134
+ /**
135
+ * Returns all patch SHAs that affected a given node or edge.
136
+ *
137
+ * "Affected" means the patch either read from or wrote to the entity.
138
+ * The returned array is sorted alphabetically for determinism.
139
+ *
140
+ * @param {string} entityId - The node ID or edge key to query
141
+ * @returns {string[]} Array of patch SHAs, sorted alphabetically
142
+ *
143
+ * @example
144
+ * const shas = index.patchesFor('user:alice');
145
+ * // Returns: ['abc123', 'def456', 'ghi789']
146
+ */
147
+ patchesFor(entityId) {
148
+ const shas = this.#index.get(entityId);
149
+ if (!shas || shas.size === 0) {
150
+ return [];
151
+ }
152
+ return [...shas].sort();
153
+ }
154
+
155
+ /**
156
+ * Returns whether the index has any entries for a given entity.
157
+ *
158
+ * @param {string} entityId - The node ID or edge key to check
159
+ * @returns {boolean} True if the entity has at least one contributing patch
160
+ */
161
+ has(entityId) {
162
+ const shas = this.#index.get(entityId);
163
+ return shas !== undefined && shas.size > 0;
164
+ }
165
+
166
+ /**
167
+ * Returns the number of entities in the index.
168
+ *
169
+ * @returns {number} Count of indexed entities
170
+ */
171
+ get size() {
172
+ return this.#index.size;
173
+ }
174
+
175
+ /**
176
+ * Returns all entity IDs in the index.
177
+ *
178
+ * @returns {string[]} Array of entity IDs, sorted alphabetically
179
+ */
180
+ entities() {
181
+ return [...this.#index.keys()].sort();
182
+ }
183
+
184
+ /**
185
+ * Clears all entries from the index.
186
+ *
187
+ * @returns {ProvenanceIndex} This index for chaining
188
+ */
189
+ clear() {
190
+ this.#index.clear();
191
+ return this;
192
+ }
193
+
194
+ /**
195
+ * Merges another index into this one.
196
+ *
197
+ * All entries from the other index are added to this index.
198
+ * This is useful for combining indexes from different sources
199
+ * (e.g., checkpoint index + incremental patches).
200
+ *
201
+ * @param {ProvenanceIndex} other - The index to merge in
202
+ * @returns {ProvenanceIndex} This index for chaining
203
+ */
204
+ merge(other) {
205
+ for (const [entityId, shas] of other.#index) {
206
+ for (const sha of shas) {
207
+ this.#addEntry(entityId, sha);
208
+ }
209
+ }
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Creates a clone of this index.
215
+ *
216
+ * @returns {ProvenanceIndex} A new index with the same data
217
+ */
218
+ clone() {
219
+ const clonedMap = new Map();
220
+ for (const [entityId, shas] of this.#index) {
221
+ clonedMap.set(entityId, new Set(shas));
222
+ }
223
+ return new ProvenanceIndex(clonedMap);
224
+ }
225
+
226
+ /**
227
+ * Returns sorted entries for deterministic output.
228
+ *
229
+ * @returns {Array<[string, string[]]>} Sorted array of [entityId, sortedShas[]] pairs
230
+ * @private
231
+ */
232
+ #sortedEntries() {
233
+ const entries = [];
234
+ for (const [entityId, shas] of this.#index) {
235
+ entries.push([entityId, [...shas].sort()]);
236
+ }
237
+ entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
238
+ return entries;
239
+ }
240
+
241
+ /**
242
+ * Serializes the index to CBOR format for checkpoint storage.
243
+ *
244
+ * The serialized format is a sorted array of [entityId, sortedShas[]] pairs
245
+ * for deterministic output.
246
+ *
247
+ * @param {Object} [options]
248
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
249
+ * @returns {Buffer} CBOR-encoded index
250
+ */
251
+ serialize({ codec } = {}) {
252
+ const c = codec || defaultCodec;
253
+ return c.encode({ version: 1, entries: this.#sortedEntries() });
254
+ }
255
+
256
+ /**
257
+ * Builds an index Map from an entries array.
258
+ *
259
+ * @param {Array<[string, string[]]>} entries - Array of [entityId, shas[]] pairs
260
+ * @returns {Map<string, Set<string>>} The built index
261
+ * @private
262
+ */
263
+ static #buildIndex(entries) {
264
+ const index = new Map();
265
+ for (const [entityId, shas] of entries) {
266
+ index.set(entityId, new Set(shas));
267
+ }
268
+ return index;
269
+ }
270
+
271
+ /**
272
+ * Deserializes an index from CBOR format.
273
+ *
274
+ * @param {Buffer} buffer - CBOR-encoded index
275
+ * @param {Object} [options]
276
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
277
+ * @returns {ProvenanceIndex} The deserialized index
278
+ * @throws {Error} If the buffer contains an unsupported version
279
+ */
280
+ static deserialize(buffer, { codec } = {}) {
281
+ const c = codec || defaultCodec;
282
+ const obj = c.decode(buffer);
283
+
284
+ if (obj.version !== 1) {
285
+ throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
286
+ }
287
+
288
+ if (!obj.entries || !Array.isArray(obj.entries)) {
289
+ throw new Error('Missing or invalid ProvenanceIndex entries');
290
+ }
291
+
292
+ return new ProvenanceIndex(ProvenanceIndex.#buildIndex(obj.entries));
293
+ }
294
+
295
+ /**
296
+ * Returns a JSON-serializable representation of this index.
297
+ *
298
+ * @returns {Object} Object with version and entries array
299
+ */
300
+ toJSON() {
301
+ return { version: 1, entries: this.#sortedEntries() };
302
+ }
303
+
304
+ /**
305
+ * Creates a ProvenanceIndex from a JSON representation.
306
+ *
307
+ * @param {Object} json - Object with version and entries array
308
+ * @returns {ProvenanceIndex} The deserialized index
309
+ * @throws {Error} If the JSON contains an unsupported version
310
+ */
311
+ static fromJSON(json) {
312
+ if (json.version !== 1) {
313
+ throw new Error(`Unsupported ProvenanceIndex version: ${json.version}`);
314
+ }
315
+
316
+ if (!json.entries || !Array.isArray(json.entries)) {
317
+ throw new Error('Missing or invalid ProvenanceIndex entries');
318
+ }
319
+
320
+ return new ProvenanceIndex(ProvenanceIndex.#buildIndex(json.entries || []));
321
+ }
322
+
323
+ /**
324
+ * Returns an iterator over [entityId, patchShas[]] pairs in deterministic order.
325
+ * Uses #sortedEntries() to ensure consistent ordering across iterations.
326
+ *
327
+ * @returns {Iterator<[string, string[]]>} Iterator over index entries
328
+ */
329
+ *[Symbol.iterator]() {
330
+ for (const entry of this.#sortedEntries()) {
331
+ yield entry;
332
+ }
333
+ }
334
+ }
335
+
336
+ export default ProvenanceIndex;
337
+ export { ProvenanceIndex };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * ProvenancePayload - Transferable Provenance as a Monoid
3
+ *
4
+ * Implements the provenance payload from Paper III (Computational Holography):
5
+ * P = (mu_0, ..., mu_{n-1}) - an ordered sequence of tick patches.
6
+ *
7
+ * The payload monoid (Payload, ., epsilon):
8
+ * - Composition is concatenation
9
+ * - Identity is empty sequence
10
+ *
11
+ * Monoid laws hold:
12
+ * - identity.concat(p) === p (left identity)
13
+ * - p.concat(identity) === p (right identity)
14
+ * - (a.concat(b)).concat(c) === a.concat(b.concat(c)) (associativity)
15
+ *
16
+ * @module domain/services/ProvenancePayload
17
+ */
18
+
19
+ import { reduceV5, createEmptyStateV5, cloneStateV5 } from './JoinReducer.js';
20
+
21
+ /**
22
+ * A single patch entry in the provenance payload.
23
+ *
24
+ * @typedef {Object} PatchEntry
25
+ * @property {Object} patch - The decoded patch object (writer, lamport, ops, context)
26
+ * @property {string} sha - The Git SHA of the patch commit
27
+ */
28
+
29
+ /**
30
+ * ProvenancePayload - Immutable sequence of patches forming a monoid.
31
+ *
32
+ * This class packages an ordered sequence of patches as a transferable
33
+ * provenance payload. Combined with an initial state (boundary encoding),
34
+ * it enables deterministic replay (computational holography).
35
+ *
36
+ * ## Monoid Structure
37
+ *
38
+ * ProvenancePayload forms a monoid under concatenation:
39
+ * - **Identity**: `ProvenancePayload.identity()` returns the empty payload
40
+ * - **Composition**: `a.concat(b)` concatenates two payloads
41
+ *
42
+ * ## Computational Holography
43
+ *
44
+ * Given a boundary encoding B = (U_0, P) where:
45
+ * - U_0 is the initial state
46
+ * - P is the provenance payload
47
+ *
48
+ * The `replay(U_0)` method uniquely determines the interior worldline,
49
+ * producing the final materialized state.
50
+ *
51
+ * @example
52
+ * ```javascript
53
+ * // Create payload from patches
54
+ * const payload = new ProvenancePayload([
55
+ * { patch: patch1, sha: 'abc123' },
56
+ * { patch: patch2, sha: 'def456' },
57
+ * ]);
58
+ *
59
+ * // Monoid operations
60
+ * const empty = ProvenancePayload.identity();
61
+ * const combined = payload1.concat(payload2);
62
+ *
63
+ * // Replay to materialize state
64
+ * const state = payload.replay();
65
+ * ```
66
+ */
67
+ class ProvenancePayload {
68
+ /**
69
+ * The internal array of patch entries. Frozen after construction.
70
+ * @type {ReadonlyArray<PatchEntry>}
71
+ * @private
72
+ */
73
+ #patches;
74
+
75
+ /**
76
+ * Creates a new ProvenancePayload from an ordered sequence of patches.
77
+ *
78
+ * The payload is immutable after construction - the patches array is
79
+ * frozen to prevent modification.
80
+ *
81
+ * @param {Array<PatchEntry>} patches - Ordered sequence of patch entries.
82
+ * Each entry must have { patch, sha } where patch is the decoded patch
83
+ * object and sha is the Git commit SHA.
84
+ * @throws {TypeError} If patches is not an array
85
+ */
86
+ constructor(patches = []) {
87
+ if (!Array.isArray(patches)) {
88
+ throw new TypeError('ProvenancePayload requires an array of patches');
89
+ }
90
+
91
+ // Shallow copy and freeze to ensure immutability
92
+ this.#patches = Object.freeze([...patches]);
93
+
94
+ // Freeze the instance itself
95
+ Object.freeze(this);
96
+ }
97
+
98
+ /**
99
+ * Returns the identity element of the payload monoid.
100
+ *
101
+ * The identity payload contains no patches. It satisfies:
102
+ * - `identity.concat(p)` equals `p` for any payload `p`
103
+ * - `p.concat(identity)` equals `p` for any payload `p`
104
+ *
105
+ * @returns {ProvenancePayload} The empty/identity payload
106
+ */
107
+ static identity() {
108
+ return new ProvenancePayload([]);
109
+ }
110
+
111
+ /**
112
+ * Returns the number of patches in this payload.
113
+ *
114
+ * @returns {number} The patch count
115
+ */
116
+ get length() {
117
+ return this.#patches.length;
118
+ }
119
+
120
+ /**
121
+ * Concatenates this payload with another, forming a new payload.
122
+ *
123
+ * This is the monoid composition operation. The resulting payload
124
+ * contains all patches from this payload followed by all patches
125
+ * from the other payload.
126
+ *
127
+ * Monoid laws:
128
+ * - `identity.concat(p) === p` (left identity)
129
+ * - `p.concat(identity) === p` (right identity)
130
+ * - `(a.concat(b)).concat(c) === a.concat(b.concat(c))` (associativity)
131
+ *
132
+ * @param {ProvenancePayload} other - The payload to append
133
+ * @returns {ProvenancePayload} A new payload with combined patches
134
+ * @throws {TypeError} If other is not a ProvenancePayload
135
+ */
136
+ concat(other) {
137
+ if (!(other instanceof ProvenancePayload)) {
138
+ throw new TypeError('concat requires a ProvenancePayload');
139
+ }
140
+
141
+ // Optimization: avoid array allocation for identity cases
142
+ if (this.#patches.length === 0) {
143
+ return other;
144
+ }
145
+ if (other.#patches.length === 0) {
146
+ return this;
147
+ }
148
+
149
+ return new ProvenancePayload([...this.#patches, ...other.#patches]);
150
+ }
151
+
152
+ /**
153
+ * Replays the payload to produce a materialized state.
154
+ *
155
+ * This implements the computational holography theorem (Paper III):
156
+ * Given a boundary encoding B = (U_0, P), Replay(B) uniquely
157
+ * determines the interior worldline.
158
+ *
159
+ * The replay applies patches in order using CRDT merge semantics:
160
+ * - Nodes/edges use OR-Set (add-wins)
161
+ * - Properties use LWW (Last-Write-Wins)
162
+ *
163
+ * @param {import('./JoinReducer.js').WarpStateV5} [initialState] - The initial
164
+ * state U_0 to replay from. If omitted, starts from empty state.
165
+ * @returns {import('./JoinReducer.js').WarpStateV5} The final materialized state
166
+ */
167
+ replay(initialState) {
168
+ // Handle empty payload - return clone of initial or fresh empty state
169
+ if (this.#patches.length === 0) {
170
+ return initialState ? cloneStateV5(initialState) : createEmptyStateV5();
171
+ }
172
+
173
+ // Use JoinReducer's reduceV5 for deterministic materialization.
174
+ // Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
175
+ // but returns bare WarpStateV5 when no options passed (as here).
176
+ return reduceV5(this.#patches, initialState);
177
+ }
178
+
179
+ /**
180
+ * Returns an iterator over the patch entries.
181
+ *
182
+ * This allows using the payload in for...of loops and spread syntax.
183
+ *
184
+ * @returns {Iterator<PatchEntry>} Iterator over patch entries
185
+ */
186
+ [Symbol.iterator]() {
187
+ return this.#patches[Symbol.iterator]();
188
+ }
189
+
190
+ /**
191
+ * Returns the patch entry at the given index.
192
+ *
193
+ * Supports negative indices like Array.prototype.at() (e.g., -1 for last element).
194
+ *
195
+ * @param {number} index - The index (negative indices count from end)
196
+ * @returns {PatchEntry|undefined} The patch entry, or undefined if out of bounds
197
+ */
198
+ at(index) {
199
+ return this.#patches.at(index);
200
+ }
201
+
202
+ /**
203
+ * Returns a new payload containing a slice of this payload's patches.
204
+ *
205
+ * This enables the slicing operation from Paper III: materializing
206
+ * only the causal cone for a target value.
207
+ *
208
+ * @param {number} [start=0] - Start index (inclusive)
209
+ * @param {number} [end] - End index (exclusive), defaults to length
210
+ * @returns {ProvenancePayload} A new payload with the sliced patches
211
+ */
212
+ slice(start = 0, end = this.#patches.length) {
213
+ const sliced = this.#patches.slice(start, end);
214
+ return new ProvenancePayload(sliced);
215
+ }
216
+
217
+ /**
218
+ * Returns a JSON-serializable representation of this payload.
219
+ *
220
+ * The serialized form is an array of patch entries, suitable for
221
+ * transmission as a Boundary Transition Record (BTR).
222
+ *
223
+ * @returns {Array<PatchEntry>} Array of patch entries
224
+ */
225
+ toJSON() {
226
+ return [...this.#patches];
227
+ }
228
+
229
+ /**
230
+ * Creates a ProvenancePayload from a JSON-serialized array.
231
+ *
232
+ * @param {Array<PatchEntry>} json - Array of patch entries
233
+ * @returns {ProvenancePayload} The deserialized payload
234
+ * @throws {TypeError} If json is not an array
235
+ */
236
+ static fromJSON(json) {
237
+ return new ProvenancePayload(json);
238
+ }
239
+ }
240
+
241
+ export default ProvenancePayload;
242
+ export { ProvenancePayload };