@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,300 @@
1
+ /**
2
+ * StateDiff - Deterministic state diff engine for PULSE subscriptions.
3
+ *
4
+ * Computes what changed between two materialized WarpStateV5 states.
5
+ * Used by the subscription system to notify handlers of graph changes.
6
+ *
7
+ * @module domain/services/StateDiff
8
+ * @see ROADMAP.md PL/DIFF/1
9
+ */
10
+
11
+ import { orsetElements } from '../crdt/ORSet.js';
12
+ import { lwwValue } from '../crdt/LWW.js';
13
+ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
14
+
15
+ /**
16
+ * @typedef {Object} EdgeChange
17
+ * @property {string} from - Source node ID
18
+ * @property {string} to - Target node ID
19
+ * @property {string} label - Edge label
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} PropSet
24
+ * @property {string} key - Encoded property key
25
+ * @property {string} nodeId - Node ID (for node props)
26
+ * @property {string} propKey - Property name
27
+ * @property {*} oldValue - Previous value (undefined if new)
28
+ * @property {*} newValue - New value
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} PropRemoved
33
+ * @property {string} key - Encoded property key
34
+ * @property {string} nodeId - Node ID (for node props)
35
+ * @property {string} propKey - Property name
36
+ * @property {*} oldValue - Previous value
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} StateDiffResult
41
+ * @property {Object} nodes - Node changes
42
+ * @property {string[]} nodes.added - Added node IDs (sorted)
43
+ * @property {string[]} nodes.removed - Removed node IDs (sorted)
44
+ * @property {Object} edges - Edge changes
45
+ * @property {EdgeChange[]} edges.added - Added edges (sorted)
46
+ * @property {EdgeChange[]} edges.removed - Removed edges (sorted)
47
+ * @property {Object} props - Property changes
48
+ * @property {PropSet[]} props.set - Set/changed properties (sorted)
49
+ * @property {PropRemoved[]} props.removed - Removed properties (sorted)
50
+ */
51
+
52
+ /**
53
+ * Compares two edge changes for deterministic ordering.
54
+ * @param {EdgeChange} a
55
+ * @param {EdgeChange} b
56
+ * @returns {number}
57
+ */
58
+ function compareEdges(a, b) {
59
+ if (a.from !== b.from) {
60
+ return a.from < b.from ? -1 : 1;
61
+ }
62
+ if (a.to !== b.to) {
63
+ return a.to < b.to ? -1 : 1;
64
+ }
65
+ if (a.label !== b.label) {
66
+ return a.label < b.label ? -1 : 1;
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ /**
72
+ * Compares two property changes for deterministic ordering.
73
+ * @param {{key: string}} a
74
+ * @param {{key: string}} b
75
+ * @returns {number}
76
+ */
77
+ function compareProps(a, b) {
78
+ if (a.key < b.key) {
79
+ return -1;
80
+ }
81
+ if (a.key > b.key) {
82
+ return 1;
83
+ }
84
+ return 0;
85
+ }
86
+
87
+ /**
88
+ * Checks if two arrays are deeply equal.
89
+ * @param {Array} a
90
+ * @param {Array} b
91
+ * @returns {boolean}
92
+ */
93
+ function arraysEqual(a, b) {
94
+ if (a.length !== b.length) {
95
+ return false;
96
+ }
97
+ for (let i = 0; i < a.length; i++) {
98
+ if (!deepEqual(a[i], b[i])) {
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Checks if two objects are deeply equal.
107
+ * @param {Object} a
108
+ * @param {Object} b
109
+ * @returns {boolean}
110
+ */
111
+ function objectsEqual(a, b) {
112
+ const keysA = Object.keys(a);
113
+ const keysB = Object.keys(b);
114
+ if (keysA.length !== keysB.length) {
115
+ return false;
116
+ }
117
+ for (const key of keysA) {
118
+ if (!Object.prototype.hasOwnProperty.call(b, key)) {
119
+ return false;
120
+ }
121
+ if (!deepEqual(a[key], b[key])) {
122
+ return false;
123
+ }
124
+ }
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Checks if two values are deeply equal (for property comparison).
130
+ * @param {*} a
131
+ * @param {*} b
132
+ * @returns {boolean}
133
+ */
134
+ function deepEqual(a, b) {
135
+ if (a === b) {
136
+ return true;
137
+ }
138
+ if (a === null || b === null) {
139
+ return false;
140
+ }
141
+ if (typeof a !== typeof b) {
142
+ return false;
143
+ }
144
+ if (typeof a !== 'object') {
145
+ return false;
146
+ }
147
+ if (Array.isArray(a) !== Array.isArray(b)) {
148
+ return false;
149
+ }
150
+ if (Array.isArray(a)) {
151
+ return arraysEqual(a, b);
152
+ }
153
+ return objectsEqual(a, b);
154
+ }
155
+
156
+ /**
157
+ * Computes set difference: elements in `after` not in `before`.
158
+ * @param {Set} before
159
+ * @param {Set} after
160
+ * @returns {Array}
161
+ */
162
+ function setAdded(before, after) {
163
+ const result = [];
164
+ for (const item of after) {
165
+ if (!before.has(item)) {
166
+ result.push(item);
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Computes node and edge diffs between two states.
174
+ * @param {import('./JoinReducer.js').WarpStateV5 | null} before
175
+ * @param {import('./JoinReducer.js').WarpStateV5} after
176
+ * @returns {{nodesAdded: string[], nodesRemoved: string[], edgesAdded: EdgeChange[], edgesRemoved: EdgeChange[]}}
177
+ */
178
+ function diffNodesAndEdges(before, after) {
179
+ const beforeNodes = before ? new Set(orsetElements(before.nodeAlive)) : new Set();
180
+ const afterNodes = new Set(orsetElements(after.nodeAlive));
181
+
182
+ // Filter edges to only include those with visible endpoints (both nodes must be alive).
183
+ // This ensures diffs respect node visibility rules - edges with tombstoned endpoints
184
+ // are treated as invisible.
185
+ const beforeEdges = before
186
+ ? new Set(
187
+ orsetElements(before.edgeAlive).filter((edgeKey) => {
188
+ const { from, to } = decodeEdgeKey(edgeKey);
189
+ return beforeNodes.has(from) && beforeNodes.has(to);
190
+ })
191
+ )
192
+ : new Set();
193
+
194
+ const afterEdges = new Set(
195
+ orsetElements(after.edgeAlive).filter((edgeKey) => {
196
+ const { from, to } = decodeEdgeKey(edgeKey);
197
+ return afterNodes.has(from) && afterNodes.has(to);
198
+ })
199
+ );
200
+
201
+ const nodesAdded = setAdded(beforeNodes, afterNodes);
202
+ const nodesRemoved = setAdded(afterNodes, beforeNodes);
203
+ const edgesAdded = setAdded(beforeEdges, afterEdges).map(decodeEdgeKey);
204
+ const edgesRemoved = setAdded(afterEdges, beforeEdges).map(decodeEdgeKey);
205
+
206
+ return { nodesAdded, nodesRemoved, edgesAdded, edgesRemoved };
207
+ }
208
+
209
+ /**
210
+ * Computes property diffs between two states.
211
+ * @param {import('./JoinReducer.js').WarpStateV5 | null} before
212
+ * @param {import('./JoinReducer.js').WarpStateV5} after
213
+ * @returns {{propsSet: PropSet[], propsRemoved: PropRemoved[]}}
214
+ */
215
+ function diffProps(before, after) {
216
+ const propsSet = [];
217
+ const propsRemoved = [];
218
+ const beforeProps = before ? before.prop : new Map();
219
+ const afterProps = after.prop;
220
+ const allPropKeys = new Set([...beforeProps.keys(), ...afterProps.keys()]);
221
+
222
+ for (const key of allPropKeys) {
223
+ // Skip edge properties (out of scope per spec)
224
+ if (isEdgePropKey(key)) {
225
+ continue;
226
+ }
227
+
228
+ const beforeReg = beforeProps.get(key);
229
+ const afterReg = afterProps.get(key);
230
+ const beforeValue = lwwValue(beforeReg);
231
+ const afterValue = lwwValue(afterReg);
232
+ const { nodeId, propKey } = decodePropKey(key);
233
+
234
+ if (afterReg !== undefined && beforeReg === undefined) {
235
+ propsSet.push({ key, nodeId, propKey, oldValue: undefined, newValue: afterValue });
236
+ } else if (afterReg === undefined && beforeReg !== undefined) {
237
+ propsRemoved.push({ key, nodeId, propKey, oldValue: beforeValue });
238
+ } else if (afterReg !== undefined && !deepEqual(beforeValue, afterValue)) {
239
+ propsSet.push({ key, nodeId, propKey, oldValue: beforeValue, newValue: afterValue });
240
+ }
241
+ }
242
+
243
+ return { propsSet, propsRemoved };
244
+ }
245
+
246
+ /**
247
+ * Computes a deterministic diff between two materialized states.
248
+ *
249
+ * @param {import('./JoinReducer.js').WarpStateV5 | null} before - Previous state (null for initial)
250
+ * @param {import('./JoinReducer.js').WarpStateV5} after - Current state
251
+ * @returns {StateDiffResult} The diff between states
252
+ */
253
+ export function diffStates(before, after) {
254
+ const { nodesAdded, nodesRemoved, edgesAdded, edgesRemoved } = diffNodesAndEdges(before, after);
255
+ const { propsSet, propsRemoved } = diffProps(before, after);
256
+
257
+ // Sort for deterministic output
258
+ nodesAdded.sort();
259
+ nodesRemoved.sort();
260
+ edgesAdded.sort(compareEdges);
261
+ edgesRemoved.sort(compareEdges);
262
+ propsSet.sort(compareProps);
263
+ propsRemoved.sort(compareProps);
264
+
265
+ return {
266
+ nodes: { added: nodesAdded, removed: nodesRemoved },
267
+ edges: { added: edgesAdded, removed: edgesRemoved },
268
+ props: { set: propsSet, removed: propsRemoved },
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Returns true if the diff represents no changes.
274
+ *
275
+ * @param {StateDiffResult} diff
276
+ * @returns {boolean}
277
+ */
278
+ export function isEmptyDiff(diff) {
279
+ return (
280
+ diff.nodes.added.length === 0 &&
281
+ diff.nodes.removed.length === 0 &&
282
+ diff.edges.added.length === 0 &&
283
+ diff.edges.removed.length === 0 &&
284
+ diff.props.set.length === 0 &&
285
+ diff.props.removed.length === 0
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Creates an empty diff result.
291
+ *
292
+ * @returns {StateDiffResult}
293
+ */
294
+ export function createEmptyDiff() {
295
+ return {
296
+ nodes: { added: [], removed: [] },
297
+ edges: { added: [], removed: [] },
298
+ props: { set: [], removed: [] },
299
+ };
300
+ }
@@ -0,0 +1,156 @@
1
+ import defaultCodec from '../utils/defaultCodec.js';
2
+ import { orsetContains, orsetElements } from '../crdt/ORSet.js';
3
+ import { decodeEdgeKey, decodePropKey } from './KeyCodec.js';
4
+
5
+ /**
6
+ * State Serialization and Hashing for WARP v5
7
+ *
8
+ * Provides visibility predicates for determining what is visible in the graph,
9
+ * canonical state serialization for deterministic hashing, and state hash computation.
10
+ *
11
+ * V5 uses ORSet-based state (rather than LWW registers for nodeAlive/edgeAlive).
12
+ *
13
+ * @module StateSerializerV5
14
+ * @see WARP Spec Section 8.3 (Visibility)
15
+ * @see WARP Spec Section 10.3 (Canonical Serialization)
16
+ */
17
+
18
+ // ============================================================================
19
+ // Visibility Predicates (WARP spec Section 8.3)
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Checks if a node is visible (present in the ORSet).
24
+ * @param {import('./JoinReducer.js').WarpStateV5} state
25
+ * @param {string} nodeId
26
+ * @returns {boolean}
27
+ */
28
+ export function nodeVisibleV5(state, nodeId) {
29
+ return orsetContains(state.nodeAlive, nodeId);
30
+ }
31
+
32
+ /**
33
+ * Checks if an edge is visible.
34
+ * Edge is visible if: edge is in ORSet AND both endpoints are visible.
35
+ * @param {import('./JoinReducer.js').WarpStateV5} state
36
+ * @param {string} edgeKey - Encoded edge key
37
+ * @returns {boolean}
38
+ */
39
+ export function edgeVisibleV5(state, edgeKey) {
40
+ if (!orsetContains(state.edgeAlive, edgeKey)) {
41
+ return false;
42
+ }
43
+ const { from, to } = decodeEdgeKey(edgeKey);
44
+ return nodeVisibleV5(state, from) && nodeVisibleV5(state, to);
45
+ }
46
+
47
+ /**
48
+ * Checks if a property is visible.
49
+ * Property is visible if: node is visible AND prop exists.
50
+ * @param {import('./JoinReducer.js').WarpStateV5} state
51
+ * @param {string} propKey - Encoded prop key
52
+ * @returns {boolean}
53
+ */
54
+ export function propVisibleV5(state, propKey) {
55
+ const { nodeId } = decodePropKey(propKey);
56
+ if (!nodeVisibleV5(state, nodeId)) {
57
+ return false;
58
+ }
59
+ return state.prop.has(propKey);
60
+ }
61
+
62
+ // ============================================================================
63
+ // Canonical State Serialization (WARP spec Section 10.3)
64
+ // ============================================================================
65
+
66
+ /**
67
+ * Serializes state to canonical CBOR bytes.
68
+ * Only includes VISIBLE projection with stable ordering:
69
+ * 1. Nodes sorted by NodeId
70
+ * 2. Edges sorted by (from, to, label)
71
+ * 3. Props sorted by (node, key)
72
+ *
73
+ * Same canonical ordering as v4 for visible projection.
74
+ *
75
+ * @param {import('./JoinReducer.js').WarpStateV5} state
76
+ * @param {Object} [options]
77
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
78
+ * @returns {Buffer}
79
+ */
80
+ export function serializeStateV5(state, { codec } = {}) {
81
+ const c = codec || defaultCodec;
82
+ // 1. Collect visible nodes, sorted
83
+ const nodes = [...orsetElements(state.nodeAlive)].sort();
84
+
85
+ // 2. Collect visible edges (both endpoints visible), sorted by (from, to, label)
86
+ const visibleEdges = [];
87
+ for (const edgeKey of orsetElements(state.edgeAlive)) {
88
+ if (edgeVisibleV5(state, edgeKey)) {
89
+ visibleEdges.push(decodeEdgeKey(edgeKey));
90
+ }
91
+ }
92
+ visibleEdges.sort((a, b) => {
93
+ if (a.from !== b.from) {
94
+ return a.from < b.from ? -1 : 1;
95
+ }
96
+ if (a.to !== b.to) {
97
+ return a.to < b.to ? -1 : 1;
98
+ }
99
+ return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
100
+ });
101
+
102
+ // 3. Collect visible props (node visible), sorted by (node, key)
103
+ const visibleProps = [];
104
+ for (const [propKey, register] of state.prop) {
105
+ const { nodeId, propKey: key } = decodePropKey(propKey);
106
+ if (nodeVisibleV5(state, nodeId)) {
107
+ visibleProps.push({ node: nodeId, key, value: register.value });
108
+ }
109
+ }
110
+ visibleProps.sort((a, b) => {
111
+ if (a.node !== b.node) {
112
+ return a.node < b.node ? -1 : 1;
113
+ }
114
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
115
+ });
116
+
117
+ // Encode as canonical CBOR
118
+ return c.encode({ nodes, edges: visibleEdges, props: visibleProps });
119
+ }
120
+
121
+ /**
122
+ * Computes SHA-256 hash of canonical state bytes.
123
+ * @param {import('./JoinReducer.js').WarpStateV5} state
124
+ * @param {Object} [options] - Options
125
+ * @param {import('../../ports/CryptoPort.js').default} options.crypto - CryptoPort instance
126
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
127
+ * @returns {Promise<string|null>} Hex-encoded SHA-256 hash, or null if no crypto
128
+ */
129
+ export async function computeStateHashV5(state, { crypto, codec } = {}) {
130
+ const serialized = serializeStateV5(state, { codec });
131
+ return crypto ? await crypto.hash('sha256', serialized) : null;
132
+ }
133
+
134
+ /**
135
+ * Deserializes state from CBOR bytes.
136
+ * Note: This reconstructs the visible projection only.
137
+ * @param {Buffer} buffer
138
+ * @param {Object} [options]
139
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
140
+ * @returns {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: *}>}}
141
+ */
142
+ export function deserializeStateV5(buffer, { codec } = {}) {
143
+ const c = codec || defaultCodec;
144
+ return c.decode(buffer);
145
+ }
146
+
147
+ // ============================================================================
148
+ // Full State Serialization (for BTR replay)
149
+ // ============================================================================
150
+
151
+ // Re-export from CheckpointSerializerV5 for compatibility.
152
+ // Both BTR and Checkpoint use the same canonical full-state format.
153
+ export {
154
+ serializeFullStateV5,
155
+ deserializeFullStateV5,
156
+ } from './CheckpointSerializerV5.js';