@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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemporalQuery - CTL*-style temporal operators over WARP graph history.
|
|
3
|
+
*
|
|
4
|
+
* Implements `always` and `eventually` temporal operators from Paper IV
|
|
5
|
+
* (Echo and the WARP Core). These operators evaluate predicates across
|
|
6
|
+
* the graph's history by replaying patches incrementally and checking
|
|
7
|
+
* the predicate at each tick boundary.
|
|
8
|
+
*
|
|
9
|
+
* ## Temporal Operators
|
|
10
|
+
*
|
|
11
|
+
* - **always(nodeId, predicate, { since })**: True iff the predicate holds
|
|
12
|
+
* at every tick since `since` where the node exists.
|
|
13
|
+
* - **eventually(nodeId, predicate, { since })**: True iff the predicate holds
|
|
14
|
+
* at some tick since `since`.
|
|
15
|
+
*
|
|
16
|
+
* ## Implementation
|
|
17
|
+
*
|
|
18
|
+
* Both operators collect all patches, sort them by causal order (same as
|
|
19
|
+
* materialization), then apply patches one at a time. After each patch
|
|
20
|
+
* application, a node snapshot is extracted and passed to the predicate.
|
|
21
|
+
*
|
|
22
|
+
* The "tick" corresponds to a patch's Lamport timestamp. The `since` option
|
|
23
|
+
* filters out patches with Lamport timestamps below the threshold.
|
|
24
|
+
*
|
|
25
|
+
* @module domain/services/TemporalQuery
|
|
26
|
+
* @see Paper IV - Echo and the WARP Core (CTL* temporal logic on histories)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createEmptyStateV5, join as joinPatch } from './JoinReducer.js';
|
|
30
|
+
import { decodePropKey } from './KeyCodec.js';
|
|
31
|
+
import { orsetContains } from '../crdt/ORSet.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Unwraps a property value from its CRDT envelope.
|
|
35
|
+
*
|
|
36
|
+
* InlineValue objects `{ type: 'inline', value: ... }` are unwrapped
|
|
37
|
+
* to their inner value. All other values pass through unchanged.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} value - Property value (potentially InlineValue-wrapped)
|
|
40
|
+
* @returns {*} The unwrapped value
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
function unwrapValue(value) {
|
|
44
|
+
if (value && typeof value === 'object' && value.type === 'inline') {
|
|
45
|
+
return value.value;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extracts a node snapshot from the current WARP state.
|
|
52
|
+
*
|
|
53
|
+
* Returns an object with `{ id, exists, props }` where props is a
|
|
54
|
+
* plain object mapping property keys to their unwrapped values.
|
|
55
|
+
* InlineValue wrappers are stripped so predicates can compare against
|
|
56
|
+
* raw values directly (e.g., `n.props.status === 'active'`).
|
|
57
|
+
*
|
|
58
|
+
* If the node does not exist in the state, `exists` is false and
|
|
59
|
+
* `props` is an empty object.
|
|
60
|
+
*
|
|
61
|
+
* @param {import('./JoinReducer.js').WarpStateV5} state - Current state
|
|
62
|
+
* @param {string} nodeId - Node ID to extract
|
|
63
|
+
* @returns {{ id: string, exists: boolean, props: Object<string, *> }}
|
|
64
|
+
*/
|
|
65
|
+
function extractNodeSnapshot(state, nodeId) {
|
|
66
|
+
const exists = orsetContains(state.nodeAlive, nodeId);
|
|
67
|
+
const props = {};
|
|
68
|
+
|
|
69
|
+
if (exists) {
|
|
70
|
+
const prefix = `${nodeId}\0`;
|
|
71
|
+
for (const [propKey, register] of state.prop) {
|
|
72
|
+
if (propKey.startsWith(prefix)) {
|
|
73
|
+
const decoded = decodePropKey(propKey);
|
|
74
|
+
props[decoded.propKey] = unwrapValue(register.value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { id: nodeId, exists, props };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* TemporalQuery provides temporal logic operators over graph history.
|
|
84
|
+
*
|
|
85
|
+
* Constructed by WarpGraph and exposed via `graph.temporal`.
|
|
86
|
+
* Both methods are async because they need to load patches from Git.
|
|
87
|
+
*/
|
|
88
|
+
export class TemporalQuery {
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object} options
|
|
91
|
+
* @param {Function} options.loadAllPatches - Async function that returns
|
|
92
|
+
* all patches as Array<{ patch, sha }> in causal order.
|
|
93
|
+
*/
|
|
94
|
+
constructor({ loadAllPatches }) {
|
|
95
|
+
/** @type {Function} */
|
|
96
|
+
this._loadAllPatches = loadAllPatches;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Tests whether a predicate holds at every tick since `since`.
|
|
101
|
+
*
|
|
102
|
+
* Replays patches from `since` to current. At each tick boundary,
|
|
103
|
+
* builds the node snapshot and tests the predicate. Returns true only
|
|
104
|
+
* if the predicate returned true at every tick where the node existed.
|
|
105
|
+
*
|
|
106
|
+
* Returns false if the node never existed in the range.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} nodeId - The node ID to evaluate
|
|
109
|
+
* @param {Function} predicate - Predicate receiving node snapshot
|
|
110
|
+
* `{ id, exists, props }`. Should return boolean.
|
|
111
|
+
* @param {{ since?: number }} [options={}] - Options
|
|
112
|
+
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
113
|
+
* Only patches with lamport >= since are considered.
|
|
114
|
+
* @returns {Promise<boolean>} True if predicate held at every tick
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const result = await graph.temporal.always(
|
|
118
|
+
* 'user:alice',
|
|
119
|
+
* n => n.props.status === 'active',
|
|
120
|
+
* { since: 0 }
|
|
121
|
+
* );
|
|
122
|
+
*/
|
|
123
|
+
async always(nodeId, predicate, options = {}) {
|
|
124
|
+
const since = options.since ?? 0;
|
|
125
|
+
const allPatches = await this._loadAllPatches();
|
|
126
|
+
|
|
127
|
+
const state = createEmptyStateV5();
|
|
128
|
+
let nodeEverExisted = false;
|
|
129
|
+
|
|
130
|
+
for (const { patch, sha } of allPatches) {
|
|
131
|
+
// Apply the patch to state
|
|
132
|
+
joinPatch(state, patch, sha);
|
|
133
|
+
|
|
134
|
+
// Skip patches before the `since` threshold
|
|
135
|
+
if (patch.lamport < since) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract node snapshot at this tick
|
|
140
|
+
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
141
|
+
|
|
142
|
+
if (snapshot.exists) {
|
|
143
|
+
nodeEverExisted = true;
|
|
144
|
+
if (!predicate(snapshot)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If the node never existed in the range, return false
|
|
151
|
+
return nodeEverExisted;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Tests whether a predicate holds at some tick since `since`.
|
|
156
|
+
*
|
|
157
|
+
* Replays patches from `since` to current. At each tick boundary,
|
|
158
|
+
* builds the node snapshot and tests the predicate. Returns true as
|
|
159
|
+
* soon as the predicate returns true at any tick.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} nodeId - The node ID to evaluate
|
|
162
|
+
* @param {Function} predicate - Predicate receiving node snapshot
|
|
163
|
+
* `{ id, exists, props }`. Should return boolean.
|
|
164
|
+
* @param {{ since?: number }} [options={}] - Options
|
|
165
|
+
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
166
|
+
* Only patches with lamport >= since are considered.
|
|
167
|
+
* @returns {Promise<boolean>} True if predicate held at any tick
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* const result = await graph.temporal.eventually(
|
|
171
|
+
* 'user:alice',
|
|
172
|
+
* n => n.props.status === 'merged',
|
|
173
|
+
* { since: 0 }
|
|
174
|
+
* );
|
|
175
|
+
*/
|
|
176
|
+
async eventually(nodeId, predicate, options = {}) {
|
|
177
|
+
const since = options.since ?? 0;
|
|
178
|
+
const allPatches = await this._loadAllPatches();
|
|
179
|
+
|
|
180
|
+
const state = createEmptyStateV5();
|
|
181
|
+
|
|
182
|
+
for (const { patch, sha } of allPatches) {
|
|
183
|
+
// Apply the patch to state
|
|
184
|
+
joinPatch(state, patch, sha);
|
|
185
|
+
|
|
186
|
+
// Skip patches before the `since` threshold
|
|
187
|
+
if (patch.lamport < since) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Extract node snapshot at this tick
|
|
192
|
+
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
193
|
+
|
|
194
|
+
if (snapshot.exists && predicate(snapshot)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TranslationCost - MDL-based cost estimation between observer views.
|
|
3
|
+
*
|
|
4
|
+
* Computes the directed cost of translating observer A's view into
|
|
5
|
+
* observer B's view, measuring information loss via Minimum Description
|
|
6
|
+
* Length (MDL) of the translation function.
|
|
7
|
+
*
|
|
8
|
+
* The cost is normalized to [0, 1]:
|
|
9
|
+
* 0 = identical views (no information lost)
|
|
10
|
+
* 1 = completely disjoint views (all information lost)
|
|
11
|
+
*
|
|
12
|
+
* @module domain/services/TranslationCost
|
|
13
|
+
* @see Paper IV, Section 4 -- Directed rulial cost
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { orsetElements, orsetContains } from '../crdt/ORSet.js';
|
|
17
|
+
import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tests whether a string matches a glob-style pattern.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} pattern - Glob pattern (e.g. 'user:*', '*:admin', '*')
|
|
23
|
+
* @param {string} str - The string to test
|
|
24
|
+
* @returns {boolean} True if the string matches the pattern
|
|
25
|
+
*/
|
|
26
|
+
function matchGlob(pattern, str) {
|
|
27
|
+
if (pattern === '*') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (!pattern.includes('*')) {
|
|
31
|
+
return pattern === str;
|
|
32
|
+
}
|
|
33
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
35
|
+
return regex.test(str);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Computes the set of property keys visible under an observer config.
|
|
40
|
+
*
|
|
41
|
+
* @param {Map<string, *>} allNodeProps - Map of propKey -> placeholder
|
|
42
|
+
* @param {string[]|undefined} expose - Whitelist of property keys
|
|
43
|
+
* @param {string[]|undefined} redact - Blacklist of property keys
|
|
44
|
+
* @returns {Set<string>} Visible property keys
|
|
45
|
+
*/
|
|
46
|
+
function visiblePropKeys(allNodeProps, expose, redact) {
|
|
47
|
+
const redactSet = redact && redact.length > 0 ? new Set(redact) : null;
|
|
48
|
+
const exposeSet = expose && expose.length > 0 ? new Set(expose) : null;
|
|
49
|
+
|
|
50
|
+
const keys = new Set();
|
|
51
|
+
for (const key of allNodeProps.keys()) {
|
|
52
|
+
if (redactSet && redactSet.has(key)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (exposeSet && !exposeSet.has(key)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
keys.add(key);
|
|
59
|
+
}
|
|
60
|
+
return keys;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Collects node property keys from state for a given node.
|
|
65
|
+
*
|
|
66
|
+
* @param {*} state - WarpStateV5 materialized state
|
|
67
|
+
* @param {string} nodeId - The node ID
|
|
68
|
+
* @returns {Map<string, boolean>} Map of propKey -> true
|
|
69
|
+
*/
|
|
70
|
+
function collectNodePropKeys(state, nodeId) {
|
|
71
|
+
const props = new Map();
|
|
72
|
+
for (const [propKey] of state.prop) {
|
|
73
|
+
if (isEdgePropKey(propKey)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const decoded = decodePropKey(propKey);
|
|
77
|
+
if (decoded.nodeId === nodeId) {
|
|
78
|
+
props.set(decoded.propKey, true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return props;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Weights for MDL cost components
|
|
85
|
+
const NODE_WEIGHT = 0.5;
|
|
86
|
+
const EDGE_WEIGHT = 0.3;
|
|
87
|
+
const PROP_WEIGHT = 0.2;
|
|
88
|
+
|
|
89
|
+
/** @returns {{ cost: 0, breakdown: { nodeLoss: 0, edgeLoss: 0, propLoss: 0 } }} */
|
|
90
|
+
function zeroCost() {
|
|
91
|
+
return { cost: 0, breakdown: { nodeLoss: 0, edgeLoss: 0, propLoss: 0 } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Counts how many items in `source` are absent from `targetSet`.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array|Set} source - Source collection
|
|
98
|
+
* @param {Set} targetSet - Target set to test against
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
function countMissing(source, targetSet) {
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (const item of source) {
|
|
104
|
+
if (!targetSet.has(item)) {
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Computes edge loss between two observer node sets.
|
|
113
|
+
*
|
|
114
|
+
* @param {*} state - WarpStateV5
|
|
115
|
+
* @param {Set<string>} nodesASet - Nodes visible to A
|
|
116
|
+
* @param {Set<string>} nodesBSet - Nodes visible to B
|
|
117
|
+
* @returns {number} edgeLoss fraction
|
|
118
|
+
*/
|
|
119
|
+
function computeEdgeLoss(state, nodesASet, nodesBSet) {
|
|
120
|
+
const allEdges = [...orsetElements(state.edgeAlive)];
|
|
121
|
+
const edgesA = [];
|
|
122
|
+
const edgesBSet = new Set();
|
|
123
|
+
|
|
124
|
+
for (const edgeKey of allEdges) {
|
|
125
|
+
const { from, to } = decodeEdgeKey(edgeKey);
|
|
126
|
+
if (!orsetContains(state.nodeAlive, from) || !orsetContains(state.nodeAlive, to)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (nodesASet.has(from) && nodesASet.has(to)) {
|
|
130
|
+
edgesA.push(edgeKey);
|
|
131
|
+
}
|
|
132
|
+
if (nodesBSet.has(from) && nodesBSet.has(to)) {
|
|
133
|
+
edgesBSet.add(edgeKey);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return countMissing(edgesA, edgesBSet) / Math.max(edgesA.length, 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Counts lost properties for a single node between two observer configs.
|
|
142
|
+
*
|
|
143
|
+
* @param {Map<string, boolean>} nodeProps - Property keys for the node
|
|
144
|
+
* @param {{ configA: Object, configB: Object, nodeInB: boolean }} opts
|
|
145
|
+
* @returns {{ propsInA: number, lostProps: number }}
|
|
146
|
+
*/
|
|
147
|
+
function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
|
|
148
|
+
const propsA = visiblePropKeys(nodeProps, configA.expose, configA.redact);
|
|
149
|
+
if (!nodeInB) {
|
|
150
|
+
return { propsInA: propsA.size, lostProps: propsA.size };
|
|
151
|
+
}
|
|
152
|
+
const propsB = visiblePropKeys(nodeProps, configB.expose, configB.redact);
|
|
153
|
+
return { propsInA: propsA.size, lostProps: countMissing(propsA, propsB) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Computes property loss across all A-visible nodes.
|
|
158
|
+
*
|
|
159
|
+
* @param {*} state - WarpStateV5
|
|
160
|
+
* @param {{ nodesA: string[], nodesBSet: Set<string>, configA: Object, configB: Object }} opts
|
|
161
|
+
* @returns {number} propLoss fraction
|
|
162
|
+
*/
|
|
163
|
+
function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
|
|
164
|
+
let totalPropsInA = 0;
|
|
165
|
+
let totalLostProps = 0;
|
|
166
|
+
|
|
167
|
+
for (const nodeId of nodesA) {
|
|
168
|
+
const nodeProps = collectNodePropKeys(state, nodeId);
|
|
169
|
+
if (nodeProps.size === 0) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const { propsInA, lostProps } = countNodePropLoss(
|
|
173
|
+
nodeProps, { configA, configB, nodeInB: nodesBSet.has(nodeId) }
|
|
174
|
+
);
|
|
175
|
+
totalPropsInA += propsInA;
|
|
176
|
+
totalLostProps += lostProps;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return totalLostProps / Math.max(totalPropsInA, 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Computes the directed MDL translation cost from observer A to observer B.
|
|
184
|
+
*
|
|
185
|
+
* The cost measures how much information is lost when translating from
|
|
186
|
+
* A's view to B's view. It is asymmetric: cost(A->B) != cost(B->A) in general.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} configA - Observer configuration for A
|
|
189
|
+
* @param {string} configA.match - Glob pattern for visible nodes
|
|
190
|
+
* @param {string[]} [configA.expose] - Property keys to include
|
|
191
|
+
* @param {string[]} [configA.redact] - Property keys to exclude
|
|
192
|
+
* @param {Object} configB - Observer configuration for B
|
|
193
|
+
* @param {string} configB.match - Glob pattern for visible nodes
|
|
194
|
+
* @param {string[]} [configB.expose] - Property keys to include
|
|
195
|
+
* @param {string[]} [configB.redact] - Property keys to exclude
|
|
196
|
+
* @param {*} state - WarpStateV5 materialized state
|
|
197
|
+
* @returns {{ cost: number, breakdown: { nodeLoss: number, edgeLoss: number, propLoss: number } }}
|
|
198
|
+
*/
|
|
199
|
+
export function computeTranslationCost(configA, configB, state) {
|
|
200
|
+
if (!configA || typeof configA.match !== 'string' ||
|
|
201
|
+
!configB || typeof configB.match !== 'string') {
|
|
202
|
+
throw new Error('configA.match and configB.match must be strings');
|
|
203
|
+
}
|
|
204
|
+
const allNodes = [...orsetElements(state.nodeAlive)];
|
|
205
|
+
const nodesA = allNodes.filter((id) => matchGlob(configA.match, id));
|
|
206
|
+
|
|
207
|
+
if (nodesA.length === 0) {
|
|
208
|
+
return zeroCost();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const nodesASet = new Set(nodesA);
|
|
212
|
+
const nodesBSet = new Set(allNodes.filter((id) => matchGlob(configB.match, id)));
|
|
213
|
+
|
|
214
|
+
const nodeLoss = countMissing(nodesA, nodesBSet) / Math.max(nodesA.length, 1);
|
|
215
|
+
const edgeLoss = computeEdgeLoss(state, nodesASet, nodesBSet);
|
|
216
|
+
const propLoss = computePropLoss(state, { nodesA, nodesBSet, configA, configB });
|
|
217
|
+
|
|
218
|
+
const cost = NODE_WEIGHT * nodeLoss + EDGE_WEIGHT * edgeLoss + PROP_WEIGHT * propLoss;
|
|
219
|
+
|
|
220
|
+
return { cost, breakdown: { nodeLoss, edgeLoss, propLoss } };
|
|
221
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WARP Message Codec — facade re-exporting all message encoding, decoding,
|
|
3
|
+
* and schema utilities.
|
|
4
|
+
*
|
|
5
|
+
* This module provides backward-compatible access to the three types of
|
|
6
|
+
* WARP (Write-Ahead Reference Protocol) commit messages:
|
|
7
|
+
* - Patch: Contains graph mutations from a single writer
|
|
8
|
+
* - Checkpoint: Contains a snapshot of materialized graph state
|
|
9
|
+
* - Anchor: Marks a merge point in the WARP DAG
|
|
10
|
+
*
|
|
11
|
+
* Implementation is split across focused sub-modules:
|
|
12
|
+
* - {@link module:domain/services/PatchMessageCodec}
|
|
13
|
+
* - {@link module:domain/services/CheckpointMessageCodec}
|
|
14
|
+
* - {@link module:domain/services/AnchorMessageCodec}
|
|
15
|
+
* - {@link module:domain/services/MessageSchemaDetector}
|
|
16
|
+
*
|
|
17
|
+
* @module domain/services/WarpMessageCodec
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { encodePatchMessage, decodePatchMessage } from './PatchMessageCodec.js';
|
|
21
|
+
export { encodeCheckpointMessage, decodeCheckpointMessage } from './CheckpointMessageCodec.js';
|
|
22
|
+
export { encodeAnchorMessage, decodeAnchorMessage } from './AnchorMessageCodec.js';
|
|
23
|
+
export {
|
|
24
|
+
detectSchemaVersion,
|
|
25
|
+
detectMessageKind,
|
|
26
|
+
assertOpsCompatible,
|
|
27
|
+
SCHEMA_V2,
|
|
28
|
+
SCHEMA_V3,
|
|
29
|
+
} from './MessageSchemaDetector.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WarpStateIndexBuilder - Builds bitmap index from materialized WARP state.
|
|
3
|
+
*
|
|
4
|
+
* This builder creates adjacency indexes from WarpStateV5.edgeAlive OR-Set,
|
|
5
|
+
* NOT from Git commit DAG topology. This is the correct WARP architecture
|
|
6
|
+
* as specified in TECH-SPEC-V7.md Task 6.
|
|
7
|
+
*
|
|
8
|
+
* The index supports O(1) neighbor lookups by node ID.
|
|
9
|
+
*
|
|
10
|
+
* @module domain/services/WarpStateIndexBuilder
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BitmapIndexBuilder from './BitmapIndexBuilder.js';
|
|
14
|
+
import { orsetContains, orsetElements } from '../crdt/ORSet.js';
|
|
15
|
+
import { decodeEdgeKey } from './KeyCodec.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds a bitmap index from materialized WARP state.
|
|
19
|
+
*
|
|
20
|
+
* This is the V7-compliant index builder that operates on logical graph edges
|
|
21
|
+
* from the edgeAlive OR-Set, not Git commit parents.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* import WarpStateIndexBuilder from './WarpStateIndexBuilder.js';
|
|
25
|
+
*
|
|
26
|
+
* const state = await graph.materialize();
|
|
27
|
+
* const builder = new WarpStateIndexBuilder();
|
|
28
|
+
* const indexData = builder.buildFromState(state);
|
|
29
|
+
*/
|
|
30
|
+
export default class WarpStateIndexBuilder {
|
|
31
|
+
/**
|
|
32
|
+
* Creates a new WarpStateIndexBuilder.
|
|
33
|
+
* @param {Object} [options] - Configuration
|
|
34
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
|
|
35
|
+
*/
|
|
36
|
+
constructor({ crypto } = {}) {
|
|
37
|
+
/** @type {BitmapIndexBuilder} */
|
|
38
|
+
this._builder = new BitmapIndexBuilder({ crypto });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds an index from materialized WARP state.
|
|
43
|
+
*
|
|
44
|
+
* Iterates over edgeAlive OR-Set and creates forward/reverse adjacency
|
|
45
|
+
* bitmaps for each node. Only includes edges where both endpoints are
|
|
46
|
+
* visible (exist in nodeAlive).
|
|
47
|
+
*
|
|
48
|
+
* @param {import('./JoinReducer.js').WarpStateV5} state - The materialized state
|
|
49
|
+
* @returns {{builder: BitmapIndexBuilder, stats: {nodes: number, edges: number}}} The populated builder and stats
|
|
50
|
+
* @throws {Error} If state is null or missing nodeAlive/edgeAlive fields
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const state = await graph.materialize();
|
|
54
|
+
* const { builder, stats } = new WarpStateIndexBuilder().buildFromState(state);
|
|
55
|
+
* console.log(`Indexed ${stats.nodes} nodes, ${stats.edges} edges`);
|
|
56
|
+
* const indexTree = await builder.serialize();
|
|
57
|
+
*/
|
|
58
|
+
buildFromState(state) {
|
|
59
|
+
if (!state || !state.nodeAlive || !state.edgeAlive) {
|
|
60
|
+
throw new Error('Invalid state: must be a valid WarpStateV5 object');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let nodeCount = 0;
|
|
64
|
+
let edgeCount = 0;
|
|
65
|
+
|
|
66
|
+
// Register all visible nodes
|
|
67
|
+
for (const nodeId of orsetElements(state.nodeAlive)) {
|
|
68
|
+
this._builder.registerNode(nodeId);
|
|
69
|
+
nodeCount++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add edges where both endpoints are visible
|
|
73
|
+
for (const edgeKey of orsetElements(state.edgeAlive)) {
|
|
74
|
+
const { from, to } = decodeEdgeKey(edgeKey);
|
|
75
|
+
|
|
76
|
+
// Only index edges where both endpoints exist in nodeAlive
|
|
77
|
+
if (orsetContains(state.nodeAlive, from) && orsetContains(state.nodeAlive, to)) {
|
|
78
|
+
this._builder.addEdge(from, to);
|
|
79
|
+
edgeCount++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
builder: this._builder,
|
|
85
|
+
stats: { nodes: nodeCount, edges: edgeCount },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serializes the index to a tree structure of buffers.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Promise<Record<string, Buffer>>} Map of path → serialized content
|
|
93
|
+
*/
|
|
94
|
+
async serialize() {
|
|
95
|
+
return await this._builder.serialize();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets the underlying BitmapIndexBuilder.
|
|
100
|
+
*
|
|
101
|
+
* @returns {BitmapIndexBuilder}
|
|
102
|
+
*/
|
|
103
|
+
get builder() {
|
|
104
|
+
return this._builder;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Convenience function to build and serialize a WARP state index.
|
|
110
|
+
*
|
|
111
|
+
* @param {import('./JoinReducer.js').WarpStateV5} state - The materialized state
|
|
112
|
+
* @param {Object} [options] - Configuration
|
|
113
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
|
|
114
|
+
* @returns {Promise<{tree: Record<string, Buffer>, stats: {nodes: number, edges: number}}>} Serialized index and stats
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* import { buildWarpStateIndex } from './WarpStateIndexBuilder.js';
|
|
118
|
+
*
|
|
119
|
+
* const state = await graph.materialize();
|
|
120
|
+
* const { tree, stats } = await buildWarpStateIndex(state);
|
|
121
|
+
*/
|
|
122
|
+
export async function buildWarpStateIndex(state, { crypto } = {}) {
|
|
123
|
+
const indexBuilder = new WarpStateIndexBuilder({ crypto });
|
|
124
|
+
const { stats } = indexBuilder.buildFromState(state);
|
|
125
|
+
const tree = await indexBuilder.serialize();
|
|
126
|
+
return { tree, stats };
|
|
127
|
+
}
|