@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.
- package/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- package/src/visualization/utils/unicode.js +52 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Anchor Detector for v3 backward compatibility.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to detect legacy v3 JSON anchors
|
|
5
|
+
* ({"_type":"anchor"}) alongside v4 trailer-based anchors for
|
|
6
|
+
* backward compatibility in E-plane traversals.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/services/LegacyAnchorDetector
|
|
9
|
+
* @see WARP Spec Section 17 - Backward Compatibility
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detects if a commit message is a legacy v3 anchor.
|
|
14
|
+
* v3 anchors are JSON objects with _type: "anchor"
|
|
15
|
+
*
|
|
16
|
+
* @param {string} message - The commit message to check
|
|
17
|
+
* @returns {boolean} True if the message is a v3 JSON anchor
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* isLegacyAnchor('{"_type":"anchor"}'); // true
|
|
21
|
+
* isLegacyAnchor('{"_type":"node"}'); // false
|
|
22
|
+
* isLegacyAnchor('plain text'); // false
|
|
23
|
+
*/
|
|
24
|
+
export function isLegacyAnchor(message) {
|
|
25
|
+
if (typeof message !== 'string') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(message.trim());
|
|
30
|
+
return parsed && parsed._type === 'anchor';
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detects if a commit is any type of anchor (v3 JSON or v4 trailer).
|
|
38
|
+
*
|
|
39
|
+
* This function provides unified anchor detection that works across
|
|
40
|
+
* both protocol versions, ensuring anchors are correctly filtered
|
|
41
|
+
* from E-plane traversals regardless of format.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} message - The commit message to check
|
|
44
|
+
* @returns {boolean} True if the message is any type of anchor
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // v4 trailer anchor
|
|
48
|
+
* isAnyAnchor('warp:anchor\n\neg-kind: anchor\neg-graph: test'); // true
|
|
49
|
+
*
|
|
50
|
+
* // v3 JSON anchor
|
|
51
|
+
* isAnyAnchor('{"_type":"anchor"}'); // true
|
|
52
|
+
*
|
|
53
|
+
* // Regular message
|
|
54
|
+
* isAnyAnchor('Some node content'); // false
|
|
55
|
+
*/
|
|
56
|
+
export function isAnyAnchor(message) {
|
|
57
|
+
if (typeof message !== 'string') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check v4 trailer-based anchor
|
|
62
|
+
if (message.includes('eg-kind: anchor')) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Check v3 JSON anchor
|
|
66
|
+
return isLegacyAnchor(message);
|
|
67
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogicalTraversal - Traversal utilities for the logical WARP graph.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic BFS/DFS/shortestPath/connectedComponent over
|
|
5
|
+
* the materialized logical graph (node/edge OR-Sets), not the Git DAG.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import TraversalError from '../errors/TraversalError.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_DEPTH = 1000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validates and normalizes an edge direction parameter.
|
|
14
|
+
*
|
|
15
|
+
* @param {string|undefined} direction - The direction to validate ('out', 'in', or 'both')
|
|
16
|
+
* @returns {'out'|'in'|'both'} The validated direction, defaulting to 'out' if undefined
|
|
17
|
+
* @throws {TraversalError} If the direction is not one of the valid values
|
|
18
|
+
*/
|
|
19
|
+
function assertDirection(direction) {
|
|
20
|
+
if (direction === undefined) {
|
|
21
|
+
return 'out';
|
|
22
|
+
}
|
|
23
|
+
if (direction === 'out' || direction === 'in' || direction === 'both') {
|
|
24
|
+
return direction;
|
|
25
|
+
}
|
|
26
|
+
throw new TraversalError(`Invalid direction: ${direction}`, {
|
|
27
|
+
code: 'INVALID_DIRECTION',
|
|
28
|
+
context: { direction },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes a label filter into a Set for efficient lookup.
|
|
34
|
+
*
|
|
35
|
+
* Accepts a single label string, an array of labels, or undefined. Returns
|
|
36
|
+
* a Set containing the label(s) or null if no filter is specified.
|
|
37
|
+
*
|
|
38
|
+
* @param {string|string[]|undefined} labelFilter - The label filter to normalize
|
|
39
|
+
* @returns {Set<string>|null} A Set of labels for filtering, or null if no filter
|
|
40
|
+
* @throws {TraversalError} If labelFilter is neither a string, array, nor undefined
|
|
41
|
+
*/
|
|
42
|
+
function normalizeLabelFilter(labelFilter) {
|
|
43
|
+
if (labelFilter === undefined) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(labelFilter)) {
|
|
47
|
+
return new Set(labelFilter);
|
|
48
|
+
}
|
|
49
|
+
if (typeof labelFilter === 'string') {
|
|
50
|
+
return new Set([labelFilter]);
|
|
51
|
+
}
|
|
52
|
+
throw new TraversalError('labelFilter must be a string or array', {
|
|
53
|
+
code: 'INVALID_LABEL_FILTER',
|
|
54
|
+
context: { receivedType: typeof labelFilter },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Filters a list of neighbor edges by label.
|
|
60
|
+
*
|
|
61
|
+
* If no label set is provided (null), returns all neighbors unchanged.
|
|
62
|
+
* If an empty label set is provided, returns an empty array.
|
|
63
|
+
* Otherwise, returns only edges whose label is in the set.
|
|
64
|
+
*
|
|
65
|
+
* @param {Array<{neighborId: string, label: string}>} neighbors - The list of neighbor edges to filter
|
|
66
|
+
* @param {Set<string>|null} labelSet - The set of allowed labels, or null to allow all
|
|
67
|
+
* @returns {Array<{neighborId: string, label: string}>} The filtered list of neighbor edges
|
|
68
|
+
*/
|
|
69
|
+
function filterByLabel(neighbors, labelSet) {
|
|
70
|
+
if (!labelSet) {
|
|
71
|
+
return neighbors;
|
|
72
|
+
}
|
|
73
|
+
if (labelSet.size === 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return neighbors.filter((edge) => labelSet.has(edge.label));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Retrieves neighbors of a node based on direction and label filter.
|
|
81
|
+
*
|
|
82
|
+
* Returns outgoing neighbors for 'out', incoming neighbors for 'in', or
|
|
83
|
+
* a merged and sorted list of both for 'both'. Results are filtered by
|
|
84
|
+
* label if a label set is provided.
|
|
85
|
+
*
|
|
86
|
+
* For 'both' direction, neighbors are sorted first by neighborId, then by label,
|
|
87
|
+
* ensuring deterministic traversal order.
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} params - The neighbor lookup parameters
|
|
90
|
+
* @param {string} params.nodeId - The node ID to get neighbors for
|
|
91
|
+
* @param {'out'|'in'|'both'} params.direction - The edge direction to follow
|
|
92
|
+
* @param {Object} params.adjacency - The adjacency structure from materialized graph
|
|
93
|
+
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.adjacency.outgoing - Outgoing edge map
|
|
94
|
+
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.adjacency.incoming - Incoming edge map
|
|
95
|
+
* @param {Set<string>|null} params.labelSet - The set of allowed labels, or null to allow all
|
|
96
|
+
* @returns {Array<{neighborId: string, label: string}>} The list of neighbor edges
|
|
97
|
+
*/
|
|
98
|
+
function getNeighbors({ nodeId, direction, adjacency, labelSet }) {
|
|
99
|
+
const outgoing = filterByLabel(adjacency.outgoing.get(nodeId) || [], labelSet);
|
|
100
|
+
const incoming = filterByLabel(adjacency.incoming.get(nodeId) || [], labelSet);
|
|
101
|
+
|
|
102
|
+
if (direction === 'out') {
|
|
103
|
+
return outgoing;
|
|
104
|
+
}
|
|
105
|
+
if (direction === 'in') {
|
|
106
|
+
return incoming;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const merged = outgoing.concat(incoming);
|
|
110
|
+
merged.sort((a, b) => {
|
|
111
|
+
if (a.neighborId !== b.neighborId) {
|
|
112
|
+
return a.neighborId < b.neighborId ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
|
|
115
|
+
});
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic graph traversal engine for the materialized WARP graph.
|
|
121
|
+
*
|
|
122
|
+
* Provides BFS, DFS, shortest path (Dijkstra/A*), topological sort, and
|
|
123
|
+
* connected component algorithms over the logical node/edge OR-Sets.
|
|
124
|
+
* All traversals produce deterministic results via sorted neighbor ordering.
|
|
125
|
+
*/
|
|
126
|
+
export default class LogicalTraversal {
|
|
127
|
+
/**
|
|
128
|
+
* Creates a new LogicalTraversal.
|
|
129
|
+
*
|
|
130
|
+
* @param {import('../WarpGraph.js').default} graph - The WarpGraph instance to traverse
|
|
131
|
+
*/
|
|
132
|
+
constructor(graph) {
|
|
133
|
+
this._graph = graph;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Prepares common traversal state by materializing the graph and validating inputs.
|
|
138
|
+
*
|
|
139
|
+
* This private method is called by all traversal methods to ensure the graph is
|
|
140
|
+
* materialized, the start node exists, and options are normalized.
|
|
141
|
+
*
|
|
142
|
+
* @private
|
|
143
|
+
* @param {string} start - The starting node ID for traversal
|
|
144
|
+
* @param {Object} options - The traversal options to normalize
|
|
145
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
146
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
147
|
+
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
148
|
+
* @returns {Promise<{dir: 'out'|'in'|'both', labelSet: Set<string>|null, adjacency: Object, depthLimit: number}>}
|
|
149
|
+
* The normalized traversal parameters
|
|
150
|
+
* @throws {TraversalError} If the start node is not found (NODE_NOT_FOUND)
|
|
151
|
+
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
152
|
+
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
153
|
+
*/
|
|
154
|
+
async _prepare(start, { dir, labelFilter, maxDepth }) {
|
|
155
|
+
const materialized = await this._graph._materializeGraph();
|
|
156
|
+
|
|
157
|
+
if (!(await this._graph.hasNode(start))) {
|
|
158
|
+
throw new TraversalError(`Start node not found: ${start}`, {
|
|
159
|
+
code: 'NODE_NOT_FOUND',
|
|
160
|
+
context: { start },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const resolvedDir = assertDirection(dir);
|
|
165
|
+
const labelSet = normalizeLabelFilter(labelFilter);
|
|
166
|
+
const { adjacency } = materialized;
|
|
167
|
+
const depthLimit = maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
168
|
+
|
|
169
|
+
return { dir: resolvedDir, labelSet, adjacency, depthLimit };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Breadth-first traversal.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} start - Starting node ID
|
|
176
|
+
* @param {Object} [options] - Traversal options
|
|
177
|
+
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
178
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
179
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
180
|
+
* @returns {Promise<string[]>} Node IDs in visit order
|
|
181
|
+
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
182
|
+
*/
|
|
183
|
+
async bfs(start, options = {}) {
|
|
184
|
+
const { dir, labelSet, adjacency, depthLimit } = await this._prepare(start, options);
|
|
185
|
+
const visited = new Set();
|
|
186
|
+
const queue = [{ nodeId: start, depth: 0 }];
|
|
187
|
+
const result = [];
|
|
188
|
+
|
|
189
|
+
while (queue.length > 0) {
|
|
190
|
+
const current = queue.shift();
|
|
191
|
+
if (visited.has(current.nodeId)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (current.depth > depthLimit) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
visited.add(current.nodeId);
|
|
199
|
+
result.push(current.nodeId);
|
|
200
|
+
|
|
201
|
+
if (current.depth === depthLimit) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const neighbors = getNeighbors({
|
|
206
|
+
nodeId: current.nodeId,
|
|
207
|
+
direction: dir,
|
|
208
|
+
adjacency,
|
|
209
|
+
labelSet,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
for (const edge of neighbors) {
|
|
213
|
+
if (!visited.has(edge.neighborId)) {
|
|
214
|
+
queue.push({ nodeId: edge.neighborId, depth: current.depth + 1 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Depth-first traversal (pre-order).
|
|
224
|
+
*
|
|
225
|
+
* @param {string} start - Starting node ID
|
|
226
|
+
* @param {Object} [options] - Traversal options
|
|
227
|
+
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
228
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
229
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
230
|
+
* @returns {Promise<string[]>} Node IDs in visit order
|
|
231
|
+
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
232
|
+
*/
|
|
233
|
+
async dfs(start, options = {}) {
|
|
234
|
+
const { dir, labelSet, adjacency, depthLimit } = await this._prepare(start, options);
|
|
235
|
+
const visited = new Set();
|
|
236
|
+
const stack = [{ nodeId: start, depth: 0 }];
|
|
237
|
+
const result = [];
|
|
238
|
+
|
|
239
|
+
while (stack.length > 0) {
|
|
240
|
+
const current = stack.pop();
|
|
241
|
+
if (visited.has(current.nodeId)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (current.depth > depthLimit) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
visited.add(current.nodeId);
|
|
249
|
+
result.push(current.nodeId);
|
|
250
|
+
|
|
251
|
+
if (current.depth === depthLimit) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const neighbors = getNeighbors({
|
|
256
|
+
nodeId: current.nodeId,
|
|
257
|
+
direction: dir,
|
|
258
|
+
adjacency,
|
|
259
|
+
labelSet,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
for (let i = neighbors.length - 1; i >= 0; i -= 1) {
|
|
263
|
+
const edge = neighbors[i];
|
|
264
|
+
if (!visited.has(edge.neighborId)) {
|
|
265
|
+
stack.push({ nodeId: edge.neighborId, depth: current.depth + 1 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Shortest path (unweighted) using BFS.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} from - Source node ID
|
|
277
|
+
* @param {string} to - Target node ID
|
|
278
|
+
* @param {Object} [options] - Traversal options
|
|
279
|
+
* @param {number} [options.maxDepth] - Maximum search depth
|
|
280
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
281
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
282
|
+
* @returns {Promise<{found: boolean, path: string[], length: number}>}
|
|
283
|
+
* When `found` is true, `path` contains the node IDs from `from` to `to` and
|
|
284
|
+
* `length` is the hop count. When `found` is false, `path` is empty and `length` is -1.
|
|
285
|
+
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
286
|
+
*/
|
|
287
|
+
async shortestPath(from, to, options = {}) {
|
|
288
|
+
const { dir, labelSet, adjacency, depthLimit } = await this._prepare(from, options);
|
|
289
|
+
|
|
290
|
+
if (from === to) {
|
|
291
|
+
return { found: true, path: [from], length: 0 };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const visited = new Set();
|
|
295
|
+
const queue = [{ nodeId: from, depth: 0 }];
|
|
296
|
+
const parent = new Map();
|
|
297
|
+
|
|
298
|
+
visited.add(from);
|
|
299
|
+
|
|
300
|
+
while (queue.length > 0) {
|
|
301
|
+
const current = queue.shift();
|
|
302
|
+
if (current.depth >= depthLimit) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const neighbors = getNeighbors({
|
|
307
|
+
nodeId: current.nodeId,
|
|
308
|
+
direction: dir,
|
|
309
|
+
adjacency,
|
|
310
|
+
labelSet,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
for (const edge of neighbors) {
|
|
314
|
+
if (visited.has(edge.neighborId)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
visited.add(edge.neighborId);
|
|
318
|
+
parent.set(edge.neighborId, current.nodeId);
|
|
319
|
+
|
|
320
|
+
if (edge.neighborId === to) {
|
|
321
|
+
const path = [to];
|
|
322
|
+
let cursor = current.nodeId;
|
|
323
|
+
while (cursor) {
|
|
324
|
+
path.push(cursor);
|
|
325
|
+
cursor = parent.get(cursor) || null;
|
|
326
|
+
}
|
|
327
|
+
path.reverse();
|
|
328
|
+
return { found: true, path, length: path.length - 1 };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
queue.push({ nodeId: edge.neighborId, depth: current.depth + 1 });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { found: false, path: [], length: -1 };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Connected component (undirected by default).
|
|
340
|
+
*
|
|
341
|
+
* @param {string} start - Starting node ID
|
|
342
|
+
* @param {Object} [options] - Traversal options
|
|
343
|
+
* @param {number} [options.maxDepth] - Maximum depth to traverse (default: 1000)
|
|
344
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
345
|
+
* @returns {Promise<string[]>} Node IDs in visit order
|
|
346
|
+
* @throws {TraversalError} If the start node is not found
|
|
347
|
+
*/
|
|
348
|
+
async connectedComponent(start, options = {}) {
|
|
349
|
+
return await this.bfs(start, { ...options, dir: 'both' });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internals for WARP message codecs.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the lazy TrailerCodec singleton, constants, and
|
|
5
|
+
* validation helpers used by PatchMessageCodec, CheckpointMessageCodec,
|
|
6
|
+
* AnchorMessageCodec, and MessageSchemaDetector.
|
|
7
|
+
*
|
|
8
|
+
* Not part of the public API — consumers should import from
|
|
9
|
+
* WarpMessageCodec.js (the facade) or the individual sub-codecs.
|
|
10
|
+
*
|
|
11
|
+
* @module domain/services/MessageCodecInternal
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TrailerCodec, TrailerCodecService } from '@git-stunts/trailer-codec';
|
|
16
|
+
|
|
17
|
+
// -----------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// -----------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Message title prefixes for each WARP commit type.
|
|
23
|
+
* @type {Object<string, string>}
|
|
24
|
+
*/
|
|
25
|
+
export const MESSAGE_TITLES = {
|
|
26
|
+
patch: 'warp:patch',
|
|
27
|
+
checkpoint: 'warp:checkpoint',
|
|
28
|
+
anchor: 'warp:anchor',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Standard trailer keys used across WARP messages.
|
|
33
|
+
* @type {Object<string, string>}
|
|
34
|
+
*/
|
|
35
|
+
export const TRAILER_KEYS = {
|
|
36
|
+
kind: 'eg-kind',
|
|
37
|
+
graph: 'eg-graph',
|
|
38
|
+
writer: 'eg-writer',
|
|
39
|
+
lamport: 'eg-lamport',
|
|
40
|
+
patchOid: 'eg-patch-oid',
|
|
41
|
+
stateHash: 'eg-state-hash',
|
|
42
|
+
frontierOid: 'eg-frontier-oid',
|
|
43
|
+
indexOid: 'eg-index-oid',
|
|
44
|
+
schema: 'eg-schema',
|
|
45
|
+
checkpointVersion: 'eg-checkpoint',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pattern for valid Git OIDs (40-character hex for SHA-1 or 64-character for SHA-256).
|
|
50
|
+
* @type {RegExp}
|
|
51
|
+
*/
|
|
52
|
+
const OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pattern for valid SHA-256 state hashes (64-character hex).
|
|
56
|
+
* @type {RegExp}
|
|
57
|
+
*/
|
|
58
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
59
|
+
|
|
60
|
+
// -----------------------------------------------------------------------------
|
|
61
|
+
// Codec Instance
|
|
62
|
+
// -----------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
// Lazy singleton codec instance
|
|
65
|
+
let _codec = null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the lazy singleton TrailerCodec instance.
|
|
69
|
+
* @returns {TrailerCodec}
|
|
70
|
+
*/
|
|
71
|
+
export function getCodec() {
|
|
72
|
+
if (!_codec) {
|
|
73
|
+
const service = new TrailerCodecService();
|
|
74
|
+
_codec = new TrailerCodec({ service });
|
|
75
|
+
}
|
|
76
|
+
return _codec;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------------------------------------------
|
|
80
|
+
// Validation Helpers
|
|
81
|
+
// -----------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates that a value is a valid Git OID.
|
|
85
|
+
* @param {string} oid - The OID to validate
|
|
86
|
+
* @param {string} fieldName - Name of the field for error messages
|
|
87
|
+
* @throws {Error} If the OID is invalid
|
|
88
|
+
*/
|
|
89
|
+
export function validateOid(oid, fieldName) {
|
|
90
|
+
if (typeof oid !== 'string') {
|
|
91
|
+
throw new Error(`Invalid ${fieldName}: expected string, got ${typeof oid}`);
|
|
92
|
+
}
|
|
93
|
+
if (!OID_PATTERN.test(oid)) {
|
|
94
|
+
throw new Error(`Invalid ${fieldName}: must be a 40 or 64 character hex string, got '${oid}'`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates that a value is a valid SHA-256 hash.
|
|
100
|
+
* @param {string} hash - The hash to validate
|
|
101
|
+
* @param {string} fieldName - Name of the field for error messages
|
|
102
|
+
* @throws {Error} If the hash is invalid
|
|
103
|
+
*/
|
|
104
|
+
export function validateSha256(hash, fieldName) {
|
|
105
|
+
if (typeof hash !== 'string') {
|
|
106
|
+
throw new Error(`Invalid ${fieldName}: expected string, got ${typeof hash}`);
|
|
107
|
+
}
|
|
108
|
+
if (!SHA256_PATTERN.test(hash)) {
|
|
109
|
+
throw new Error(`Invalid ${fieldName}: must be a 64 character hex string, got '${hash}'`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validates that a value is a positive integer.
|
|
115
|
+
* @param {number} value - The value to validate
|
|
116
|
+
* @param {string} fieldName - Name of the field for error messages
|
|
117
|
+
* @throws {Error} If the value is not a positive integer
|
|
118
|
+
*/
|
|
119
|
+
export function validatePositiveInteger(value, fieldName) {
|
|
120
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
121
|
+
throw new Error(`Invalid ${fieldName}: must be a positive integer, got ${value}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validates that a schema version is valid.
|
|
127
|
+
* @param {number} schema - The schema version to validate
|
|
128
|
+
* @throws {Error} If the schema version is invalid
|
|
129
|
+
*/
|
|
130
|
+
export function validateSchema(schema) {
|
|
131
|
+
validatePositiveInteger(schema, 'schema');
|
|
132
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema version detection and compatibility validation for WARP messages.
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities to detect schema versions from patch operations,
|
|
5
|
+
* detect message kinds from raw commit messages, and validate operation
|
|
6
|
+
* compatibility with a reader's maximum supported schema version.
|
|
7
|
+
*
|
|
8
|
+
* See {@link module:domain/services/WarpMessageCodec} for the facade
|
|
9
|
+
* that re-exports all functions from this module.
|
|
10
|
+
*
|
|
11
|
+
* @module domain/services/MessageSchemaDetector
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EDGE_PROP_PREFIX } from './KeyCodec.js';
|
|
15
|
+
import SchemaUnsupportedError from '../errors/SchemaUnsupportedError.js';
|
|
16
|
+
import { getCodec, TRAILER_KEYS } from './MessageCodecInternal.js';
|
|
17
|
+
|
|
18
|
+
// -----------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Schema version for classic node-only patches (V5 format).
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
export const SCHEMA_V2 = 2;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Schema version for patches that may contain edge property PropSet ops.
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
export const SCHEMA_V3 = 3;
|
|
33
|
+
|
|
34
|
+
// -----------------------------------------------------------------------------
|
|
35
|
+
// Schema Version Detection
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detects the appropriate schema version for a set of patch operations.
|
|
40
|
+
*
|
|
41
|
+
* Returns schema 3 if ANY PropSet op has a `node` field starting with the
|
|
42
|
+
* edge property prefix (`\x01`), indicating edge property support is required.
|
|
43
|
+
* Otherwise returns schema 2 for backward compatibility.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array<{type: string, node?: string}>} ops - Array of patch operations
|
|
46
|
+
* @returns {number} The schema version (2 or 3)
|
|
47
|
+
*/
|
|
48
|
+
export function detectSchemaVersion(ops) {
|
|
49
|
+
if (!Array.isArray(ops)) {
|
|
50
|
+
return SCHEMA_V2;
|
|
51
|
+
}
|
|
52
|
+
for (const op of ops) {
|
|
53
|
+
if (op.type === 'PropSet' && typeof op.node === 'string' && op.node.startsWith(EDGE_PROP_PREFIX)) {
|
|
54
|
+
return SCHEMA_V3;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return SCHEMA_V2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// -----------------------------------------------------------------------------
|
|
61
|
+
// Schema Compatibility Validation
|
|
62
|
+
// -----------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Asserts that a set of decoded patch operations is compatible with a given
|
|
66
|
+
* maximum supported schema version. Throws {@link SchemaUnsupportedError} if
|
|
67
|
+
* any operation requires a higher schema version than `maxSchema`.
|
|
68
|
+
*
|
|
69
|
+
* Currently the only schema boundary is v2 -> v3:
|
|
70
|
+
* - Schema v3 introduces edge property PropSet ops (node starts with `\x01`).
|
|
71
|
+
* - A v2-only reader MUST reject patches containing such ops to prevent
|
|
72
|
+
* silent data loss.
|
|
73
|
+
* - A v3 patch that contains only classic node/edge ops is accepted by v2
|
|
74
|
+
* readers — the schema number alone is NOT a rejection criterion.
|
|
75
|
+
*
|
|
76
|
+
* @param {Array<{type: string, node?: string}>} ops - Decoded patch operations
|
|
77
|
+
* @param {number} maxSchema - Maximum schema version the reader supports
|
|
78
|
+
* @throws {SchemaUnsupportedError} If ops require a schema version > maxSchema
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* import { assertOpsCompatible, SCHEMA_V2 } from './MessageSchemaDetector.js';
|
|
82
|
+
* assertOpsCompatible(patch.ops, SCHEMA_V2); // throws if edge prop ops found
|
|
83
|
+
*/
|
|
84
|
+
export function assertOpsCompatible(ops, maxSchema) {
|
|
85
|
+
if (maxSchema >= SCHEMA_V3) {
|
|
86
|
+
return; // v3 readers understand everything up to v3
|
|
87
|
+
}
|
|
88
|
+
// For v2 readers: scan for edge property ops (the v3 feature)
|
|
89
|
+
if (!Array.isArray(ops)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const op of ops) {
|
|
93
|
+
if (
|
|
94
|
+
op.type === 'PropSet' &&
|
|
95
|
+
typeof op.node === 'string' &&
|
|
96
|
+
op.node.startsWith(EDGE_PROP_PREFIX)
|
|
97
|
+
) {
|
|
98
|
+
throw new SchemaUnsupportedError(
|
|
99
|
+
'Upgrade to >=7.3.0 (WEIGHTED) to sync edge properties.',
|
|
100
|
+
{
|
|
101
|
+
context: {
|
|
102
|
+
requiredSchema: SCHEMA_V3,
|
|
103
|
+
maxSupportedSchema: maxSchema,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// -----------------------------------------------------------------------------
|
|
112
|
+
// Detection Helper
|
|
113
|
+
// -----------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Detects the WARP message kind from a raw commit message.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} message - The raw commit message
|
|
119
|
+
* @returns {'patch'|'checkpoint'|'anchor'|null} The message kind, or null if not a WARP message
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const kind = detectMessageKind(message);
|
|
123
|
+
* if (kind === 'patch') {
|
|
124
|
+
* const data = decodePatchMessage(message);
|
|
125
|
+
* }
|
|
126
|
+
*/
|
|
127
|
+
export function detectMessageKind(message) {
|
|
128
|
+
if (typeof message !== 'string') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const codec = getCodec();
|
|
134
|
+
const decoded = codec.decode(message);
|
|
135
|
+
const kind = decoded.trailers[TRAILER_KEYS.kind];
|
|
136
|
+
|
|
137
|
+
if (kind === 'patch' || kind === 'checkpoint' || kind === 'anchor') {
|
|
138
|
+
return kind;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
} catch {
|
|
142
|
+
// Not a valid message format
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|