@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
package/README.md
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# @git-stunts/git-warp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/git-stunts/git-warp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
5
|
+
[](https://www.npmjs.com/package/@git-stunts/git-warp)
|
|
6
|
+
|
|
7
|
+
A multi-writer graph database that uses Git commits as its storage substrate. Graph state is stored as commits pointing to the empty tree (`4b825dc...`), making the data invisible to normal Git workflows while inheriting Git's content-addressing, cryptographic integrity, and distributed replication.
|
|
8
|
+
|
|
9
|
+
Writers collaborate without coordination using CRDTs (OR-Set for nodes/edges, LWW registers for properties). Every writer maintains an independent patch chain; materialization deterministically merges all writers into a single consistent view.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @git-stunts/git-warp @git-stunts/plumbing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For a comprehensive walkthrough — from setup to advanced features — see the [Guide](docs/GUIDE.md).
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import GitPlumbing from '@git-stunts/plumbing';
|
|
21
|
+
import WarpGraph, { GitGraphAdapter } from '@git-stunts/git-warp';
|
|
22
|
+
|
|
23
|
+
const plumbing = new GitPlumbing({ cwd: './my-repo' });
|
|
24
|
+
const persistence = new GitGraphAdapter({ plumbing });
|
|
25
|
+
|
|
26
|
+
const graph = await WarpGraph.open({
|
|
27
|
+
persistence,
|
|
28
|
+
graphName: 'demo',
|
|
29
|
+
writerId: 'writer-1',
|
|
30
|
+
autoMaterialize: true, // auto-materialize on query
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Write data using the patch builder
|
|
34
|
+
await (await graph.createPatch())
|
|
35
|
+
.addNode('user:alice')
|
|
36
|
+
.setProperty('user:alice', 'name', 'Alice')
|
|
37
|
+
.setProperty('user:alice', 'role', 'admin')
|
|
38
|
+
.addNode('user:bob')
|
|
39
|
+
.setProperty('user:bob', 'name', 'Bob')
|
|
40
|
+
.addEdge('user:alice', 'user:bob', 'manages')
|
|
41
|
+
.setEdgeProperty('user:alice', 'user:bob', 'manages', 'since', '2024')
|
|
42
|
+
.commit();
|
|
43
|
+
|
|
44
|
+
// Query the graph
|
|
45
|
+
const result = await graph.query()
|
|
46
|
+
.match('user:*')
|
|
47
|
+
.outgoing('manages')
|
|
48
|
+
.run();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
Each writer creates **patches**: atomic batches of graph operations (add/remove nodes, add/remove edges, set properties). Patches are serialized as CBOR-encoded Git commit messages pointing to the empty tree, forming a per-writer chain under `refs/warp/<graphName>/writers/<writerId>`.
|
|
54
|
+
|
|
55
|
+
**Materialization** replays all patches from all writers, applying CRDT merge semantics:
|
|
56
|
+
|
|
57
|
+
- **Nodes and edges** use an Observed-Remove Set (OR-Set). An add wins over a concurrent remove unless the remove has observed the specific add event.
|
|
58
|
+
- **Properties** use Last-Write-Wins (LWW) registers, ordered by Lamport timestamp, then writer ID, then patch SHA.
|
|
59
|
+
- **Version vectors** track causality across writers, ensuring deterministic convergence regardless of patch arrival order.
|
|
60
|
+
|
|
61
|
+
**Checkpoints** snapshot materialized state into a single commit for fast incremental recovery. Subsequent materializations only need to replay patches created after the checkpoint.
|
|
62
|
+
|
|
63
|
+
## Multi-Writer Collaboration
|
|
64
|
+
|
|
65
|
+
Writers operate independently on the same Git repository. Sync happens through standard Git transport (push/pull) or the built-in HTTP sync protocol.
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// Writer A (on machine A)
|
|
69
|
+
const graphA = await WarpGraph.open({
|
|
70
|
+
persistence: persistenceA,
|
|
71
|
+
graphName: 'shared',
|
|
72
|
+
writerId: 'alice',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await (await graphA.createPatch())
|
|
76
|
+
.addNode('doc:1')
|
|
77
|
+
.setProperty('doc:1', 'title', 'Draft')
|
|
78
|
+
.commit();
|
|
79
|
+
|
|
80
|
+
// Writer B (on machine B)
|
|
81
|
+
const graphB = await WarpGraph.open({
|
|
82
|
+
persistence: persistenceB,
|
|
83
|
+
graphName: 'shared',
|
|
84
|
+
writerId: 'bob',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await (await graphB.createPatch())
|
|
88
|
+
.addNode('doc:2')
|
|
89
|
+
.setProperty('doc:2', 'title', 'Notes')
|
|
90
|
+
.commit();
|
|
91
|
+
|
|
92
|
+
// After git push/pull, materialize merges both writers
|
|
93
|
+
const state = await graphA.materialize();
|
|
94
|
+
await graphA.hasNode('doc:1'); // true
|
|
95
|
+
await graphA.hasNode('doc:2'); // true
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### HTTP Sync
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// Start a sync server
|
|
102
|
+
const server = await graphB.serve({ port: 3000 });
|
|
103
|
+
|
|
104
|
+
// Sync from another instance
|
|
105
|
+
await graphA.syncWith('http://localhost:3000/sync');
|
|
106
|
+
|
|
107
|
+
await server.close();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Direct Sync
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// Sync two in-process instances directly
|
|
114
|
+
await graphA.syncWith(graphB);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Querying
|
|
118
|
+
|
|
119
|
+
Query methods require materialized state. Either call `materialize()` first, or pass `autoMaterialize: true` to `WarpGraph.open()` to handle this automatically.
|
|
120
|
+
|
|
121
|
+
### Simple Queries
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
await graph.materialize();
|
|
125
|
+
|
|
126
|
+
await graph.getNodes(); // ['user:alice', 'user:bob']
|
|
127
|
+
await graph.hasNode('user:alice'); // true
|
|
128
|
+
await graph.getNodeProps('user:alice'); // Map { 'name' => 'Alice', 'role' => 'admin' }
|
|
129
|
+
await graph.neighbors('user:alice', 'outgoing'); // [{ nodeId: 'user:bob', label: 'manages', direction: 'outgoing' }]
|
|
130
|
+
await graph.getEdges(); // [{ from: 'user:alice', to: 'user:bob', label: 'manages', props: {} }]
|
|
131
|
+
await graph.getEdgeProps('user:alice', 'user:bob', 'manages'); // { weight: 0.9 } or null
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Fluent Query Builder
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const result = await graph.query()
|
|
138
|
+
.match('user:*') // glob pattern matching
|
|
139
|
+
.outgoing('manages') // traverse outgoing edges with label
|
|
140
|
+
.select(['id', 'props']) // select fields
|
|
141
|
+
.run();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Object shorthand in `where()`
|
|
145
|
+
|
|
146
|
+
Filter nodes by property equality using plain objects. Multiple properties = AND semantics.
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// Object shorthand — strict equality on primitive values
|
|
150
|
+
const admins = await graph.query()
|
|
151
|
+
.match('user:*')
|
|
152
|
+
.where({ role: 'admin', active: true })
|
|
153
|
+
.run();
|
|
154
|
+
|
|
155
|
+
// Chain object and function filters
|
|
156
|
+
const seniorAdmins = await graph.query()
|
|
157
|
+
.match('user:*')
|
|
158
|
+
.where({ role: 'admin' })
|
|
159
|
+
.where(({ props }) => props.age >= 30)
|
|
160
|
+
.run();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Multi-hop traversal
|
|
164
|
+
|
|
165
|
+
Traverse multiple hops in a single call with `depth`. Default is `[1, 1]` (single hop).
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// Depth 2: return only hop-2 neighbors
|
|
169
|
+
const grandchildren = await graph.query()
|
|
170
|
+
.match('org:root')
|
|
171
|
+
.outgoing('child', { depth: 2 })
|
|
172
|
+
.run();
|
|
173
|
+
|
|
174
|
+
// Range [1, 3]: return neighbors at hops 1, 2, and 3
|
|
175
|
+
const reachable = await graph.query()
|
|
176
|
+
.match('node:a')
|
|
177
|
+
.outgoing('next', { depth: [1, 3] })
|
|
178
|
+
.run();
|
|
179
|
+
|
|
180
|
+
// Depth [0, 2]: include the start set (self) plus hops 1 and 2
|
|
181
|
+
const selfAndNeighbors = await graph.query()
|
|
182
|
+
.match('node:a')
|
|
183
|
+
.outgoing('next', { depth: [0, 2] })
|
|
184
|
+
.run();
|
|
185
|
+
|
|
186
|
+
// Incoming edges work the same way
|
|
187
|
+
const ancestors = await graph.query()
|
|
188
|
+
.match('node:leaf')
|
|
189
|
+
.incoming('child', { depth: [1, 5] })
|
|
190
|
+
.run();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Aggregation
|
|
194
|
+
|
|
195
|
+
Compute count, sum, avg, min, max over matched nodes. This is a terminal operation — `select()`, `outgoing()`, and `incoming()` cannot follow `aggregate()`.
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const stats = await graph.query()
|
|
199
|
+
.match('order:*')
|
|
200
|
+
.where({ status: 'paid' })
|
|
201
|
+
.aggregate({ count: true, sum: 'props.total', avg: 'props.total' })
|
|
202
|
+
.run();
|
|
203
|
+
|
|
204
|
+
// stats = { stateHash: '...', count: 12, sum: 1450, avg: 120.83 }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Non-numeric property values are silently skipped during aggregation.
|
|
208
|
+
|
|
209
|
+
### Path Finding
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const result = await graph.traverse.shortestPath('user:alice', 'user:bob', {
|
|
213
|
+
dir: 'outgoing',
|
|
214
|
+
labelFilter: 'manages',
|
|
215
|
+
maxDepth: 10,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (result.found) {
|
|
219
|
+
console.log(result.path); // ['user:alice', 'user:bob']
|
|
220
|
+
console.log(result.length); // 1
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Subscriptions & Reactivity
|
|
225
|
+
|
|
226
|
+
React to graph changes without polling. Handlers are called after `materialize()` when state has changed.
|
|
227
|
+
|
|
228
|
+
### Subscribe to All Changes
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const { unsubscribe } = graph.subscribe({
|
|
232
|
+
onChange: (diff) => {
|
|
233
|
+
console.log('Nodes added:', diff.nodes.added);
|
|
234
|
+
console.log('Nodes removed:', diff.nodes.removed);
|
|
235
|
+
console.log('Edges added:', diff.edges.added);
|
|
236
|
+
console.log('Props changed:', diff.props.set);
|
|
237
|
+
},
|
|
238
|
+
onError: (err) => console.error('Handler error:', err),
|
|
239
|
+
replay: true, // immediately fire with current state
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Make changes and materialize to trigger handlers
|
|
243
|
+
await (await graph.createPatch()).addNode('user:charlie').commit();
|
|
244
|
+
await graph.materialize(); // onChange fires with the diff
|
|
245
|
+
|
|
246
|
+
unsubscribe(); // stop receiving updates
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Watch with Pattern Filtering
|
|
250
|
+
|
|
251
|
+
Only receive changes for nodes matching a glob pattern:
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
const { unsubscribe } = graph.watch('user:*', {
|
|
255
|
+
onChange: (diff) => {
|
|
256
|
+
// Only includes user:* nodes, their edges, and their properties
|
|
257
|
+
console.log('User changes:', diff);
|
|
258
|
+
},
|
|
259
|
+
poll: 5000, // optional: check for remote changes every 5s
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When `poll` is set, the watcher periodically calls `hasFrontierChanged()` and auto-materializes if remote changes are detected.
|
|
264
|
+
|
|
265
|
+
## Observer Views
|
|
266
|
+
|
|
267
|
+
Project the graph through filtered lenses for access control, data minimization, or multi-tenant isolation (Paper IV).
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// Create an observer that only sees user:* nodes, with sensitive fields hidden
|
|
271
|
+
const view = await graph.observer('publicApi', {
|
|
272
|
+
match: 'user:*',
|
|
273
|
+
redact: ['ssn', 'password'],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const users = await view.getNodes(); // only user:* nodes
|
|
277
|
+
const props = await view.getNodeProps('user:alice'); // Map without ssn/password
|
|
278
|
+
const result = await view.query().match('user:*').where({ role: 'admin' }).run();
|
|
279
|
+
|
|
280
|
+
// Measure information loss between two observer perspectives
|
|
281
|
+
const { cost, breakdown } = await graph.translationCost(
|
|
282
|
+
{ match: '*' }, // full view
|
|
283
|
+
{ match: 'user:*', redact: ['ssn'] }, // restricted view
|
|
284
|
+
);
|
|
285
|
+
// cost ∈ [0, 1] — 0 = identical views, 1 = completely disjoint
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Temporal Queries
|
|
289
|
+
|
|
290
|
+
CTL*-style temporal operators over patch history (Paper IV).
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
// Was this node always in 'active' status?
|
|
294
|
+
const alwaysActive = await graph.temporal.always(
|
|
295
|
+
'user:alice',
|
|
296
|
+
(snapshot) => snapshot.props.status === 'active',
|
|
297
|
+
{ since: 0 },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Was this PR ever merged?
|
|
301
|
+
const wasMerged = await graph.temporal.eventually(
|
|
302
|
+
'pr:42',
|
|
303
|
+
(snapshot) => snapshot.props.status === 'merged',
|
|
304
|
+
);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Patch Operations
|
|
308
|
+
|
|
309
|
+
The patch builder supports seven operations:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
const sha = await (await graph.createPatch())
|
|
313
|
+
.addNode('n1') // create a node
|
|
314
|
+
.removeNode('n1') // tombstone a node
|
|
315
|
+
.addEdge('n1', 'n2', 'label') // create a directed edge
|
|
316
|
+
.removeEdge('n1', 'n2', 'label') // tombstone an edge
|
|
317
|
+
.setProperty('n1', 'key', 'value') // set a node property (LWW)
|
|
318
|
+
.setEdgeProperty('n1', 'n2', 'label', 'weight', 0.8) // set an edge property (LWW)
|
|
319
|
+
.commit(); // commit as a single atomic patch
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Each `commit()` creates one Git commit containing all the operations, advances the writer's Lamport clock, and updates the writer's ref via compare-and-swap.
|
|
323
|
+
|
|
324
|
+
### Writer API
|
|
325
|
+
|
|
326
|
+
For repeated writes, the Writer API is more convenient:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const writer = await graph.writer();
|
|
330
|
+
|
|
331
|
+
await writer.commitPatch(p => {
|
|
332
|
+
p.addNode('item:1');
|
|
333
|
+
p.setProperty('item:1', 'status', 'active');
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Checkpoints and Garbage Collection
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
// Checkpoint current state for fast future materialization
|
|
341
|
+
await graph.materialize();
|
|
342
|
+
await graph.createCheckpoint();
|
|
343
|
+
|
|
344
|
+
// GC removes tombstones when safe
|
|
345
|
+
const metrics = graph.getGCMetrics();
|
|
346
|
+
const { ran, result } = graph.maybeRunGC();
|
|
347
|
+
|
|
348
|
+
// Or configure automatic checkpointing
|
|
349
|
+
const graph = await WarpGraph.open({
|
|
350
|
+
persistence,
|
|
351
|
+
graphName: 'demo',
|
|
352
|
+
writerId: 'writer-1',
|
|
353
|
+
checkpointPolicy: { every: 500 }, // auto-checkpoint every 500 patches
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Observability
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
// Operational health snapshot (does not trigger materialization)
|
|
361
|
+
const status = await graph.status();
|
|
362
|
+
// {
|
|
363
|
+
// cachedState: 'fresh', // 'fresh' | 'stale' | 'none'
|
|
364
|
+
// patchesSinceCheckpoint: 12,
|
|
365
|
+
// tombstoneRatio: 0.03,
|
|
366
|
+
// writers: 2,
|
|
367
|
+
// frontier: { alice: 'abc...', bob: 'def...' },
|
|
368
|
+
// }
|
|
369
|
+
|
|
370
|
+
// Tick receipts: see exactly what happened during materialization
|
|
371
|
+
const { state, receipts } = await graph.materialize({ receipts: true });
|
|
372
|
+
for (const receipt of receipts) {
|
|
373
|
+
for (const op of receipt.ops) {
|
|
374
|
+
if (op.result === 'superseded') {
|
|
375
|
+
console.log(`${op.op} on ${op.target}: ${op.reason}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Core operations (`materialize()`, `syncWith()`, `createCheckpoint()`, `runGC()`) emit structured timing logs via `LoggerPort` when a logger is injected.
|
|
382
|
+
|
|
383
|
+
## CLI
|
|
384
|
+
|
|
385
|
+
The CLI is available as `warp-graph` or as a Git subcommand `git warp`.
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
# Install the git subcommand
|
|
389
|
+
npm run install:git-warp
|
|
390
|
+
|
|
391
|
+
# List graphs in a repo
|
|
392
|
+
git warp info
|
|
393
|
+
|
|
394
|
+
# Query nodes by pattern
|
|
395
|
+
git warp query --match 'user:*' --outgoing manages --json
|
|
396
|
+
|
|
397
|
+
# Find shortest path between nodes
|
|
398
|
+
git warp path --from user:alice --to user:bob --dir out
|
|
399
|
+
|
|
400
|
+
# Show patch history for a writer
|
|
401
|
+
git warp history --writer alice
|
|
402
|
+
|
|
403
|
+
# Check graph health, status, and GC metrics
|
|
404
|
+
git warp check
|
|
405
|
+
|
|
406
|
+
# Visualize query results (ascii output by default)
|
|
407
|
+
git warp query --match 'user:*' --outgoing manages --view
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
All commands accept `--repo <path>` to target a specific Git repository, `--json` for machine-readable output, and `--view [mode]` for visual output (ascii by default, or browser, svg:FILE, html:FILE).
|
|
411
|
+
|
|
412
|
+
## Architecture
|
|
413
|
+
|
|
414
|
+
The codebase follows hexagonal architecture with ports and adapters:
|
|
415
|
+
|
|
416
|
+
**Ports** define abstract interfaces for infrastructure:
|
|
417
|
+
- `GraphPersistencePort` -- Git operations (composite of CommitPort, BlobPort, TreePort, RefPort, ConfigPort)
|
|
418
|
+
- `CommitPort` / `BlobPort` / `TreePort` / `RefPort` / `ConfigPort` -- focused persistence interfaces
|
|
419
|
+
- `IndexStoragePort` -- bitmap index storage
|
|
420
|
+
- `CodecPort` -- encode/decode operations
|
|
421
|
+
- `CryptoPort` -- hash/HMAC operations
|
|
422
|
+
- `LoggerPort` -- structured logging
|
|
423
|
+
- `ClockPort` -- time measurement
|
|
424
|
+
|
|
425
|
+
**Adapters** implement the ports:
|
|
426
|
+
- `GitGraphAdapter` -- wraps `@git-stunts/plumbing` for Git operations
|
|
427
|
+
- `ClockAdapter` -- unified clock (factory: `ClockAdapter.node()`, `ClockAdapter.global()`)
|
|
428
|
+
- `NodeCryptoAdapter` -- cryptographic operations via `node:crypto`
|
|
429
|
+
- `WebCryptoAdapter` -- cryptographic operations via Web Crypto API (browsers, Deno, Bun, Node 20+)
|
|
430
|
+
- `NodeHttpAdapter` / `BunHttpAdapter` / `DenoHttpAdapter` -- HTTP server per runtime
|
|
431
|
+
- `ConsoleLogger` / `NoOpLogger` -- logging implementations
|
|
432
|
+
- `CborCodec` -- CBOR serialization for patches
|
|
433
|
+
|
|
434
|
+
**Domain** contains the core logic:
|
|
435
|
+
- `WarpGraph` -- public API facade
|
|
436
|
+
- `Writer` / `PatchSession` -- patch creation and commit
|
|
437
|
+
- `JoinReducer` -- CRDT-based state materialization
|
|
438
|
+
- `QueryBuilder` -- fluent query construction
|
|
439
|
+
- `LogicalTraversal` -- graph traversal over materialized state
|
|
440
|
+
- `SyncProtocol` -- multi-writer synchronization
|
|
441
|
+
- `CheckpointService` -- state snapshot creation and loading
|
|
442
|
+
- `ObserverView` -- read-only filtered graph projections
|
|
443
|
+
- `TemporalQuery` -- CTL* temporal operators over history
|
|
444
|
+
- `TranslationCost` -- MDL cost estimation between observer views
|
|
445
|
+
- `BitmapIndexBuilder` / `BitmapIndexReader` -- roaring bitmap indexes
|
|
446
|
+
- `VersionVector` / `ORSet` / `LWW` -- CRDT primitives
|
|
447
|
+
|
|
448
|
+
## Dependencies
|
|
449
|
+
|
|
450
|
+
| Package | Purpose |
|
|
451
|
+
|---------|---------|
|
|
452
|
+
| `@git-stunts/plumbing` | Low-level Git operations |
|
|
453
|
+
| `@git-stunts/alfred` | Retry with exponential backoff |
|
|
454
|
+
| `@git-stunts/trailer-codec` | Git trailer encoding |
|
|
455
|
+
| `cbor-x` | CBOR binary serialization |
|
|
456
|
+
| `roaring` | Roaring bitmap indexes (native C++ bindings) |
|
|
457
|
+
| `zod` | Schema validation |
|
|
458
|
+
|
|
459
|
+
## Testing
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm test # unit tests (vitest)
|
|
463
|
+
npm run lint # eslint
|
|
464
|
+
npm run test:bench # benchmarks
|
|
465
|
+
npm run test:bats # CLI integration tests (Docker + BATS)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## AIΩN Foundations Series
|
|
469
|
+
|
|
470
|
+
This package is the reference implementation of WARP (Worldline Algebra for Recursive Provenance) graphs as described in the AIΩN Foundations Series. The papers define WARP graphs as a minimal recursive state object ([Paper I](https://doi.org/10.5281/zenodo.17908005)), equip them with deterministic tick-based operational semantics ([Paper II](https://doi.org/10.5281/zenodo.17934512)), develop computational holography, provenance payloads, and prefix forks ([Paper III](https://doi.org/10.5281/zenodo.17963669)), and introduce observer geometry with rulial distance and temporal logic ([Paper IV](https://doi.org/10.5281/zenodo.18038297)). This codebase implements the core data structures and multi-writer collaboration protocol described in those papers.
|
|
471
|
+
|
|
472
|
+
## License
|
|
473
|
+
|
|
474
|
+
Apache-2.0
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
<p align="center">
|
|
479
|
+
<sub>Built by <a href="https://github.com/flyingrobots">FLYING ROBOTS</a></sub>
|
|
480
|
+
</p>
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
@git-stunts/git-warp is designed with security-by-default principles, treating the underlying Git binary as an untrusted subsystem through the `@git-stunts/plumbing` layer.
|
|
4
|
+
|
|
5
|
+
## 🛡️ Security Through Plumbing
|
|
6
|
+
|
|
7
|
+
This library inherits all security protections from `@git-stunts/plumbing`:
|
|
8
|
+
|
|
9
|
+
- **Command Sanitization**: All Git commands are validated through a strict whitelist
|
|
10
|
+
- **Argument Injection Prevention**: Refs are validated against strict patterns to prevent command injection
|
|
11
|
+
- **No Arbitrary Commands**: Only safe Git plumbing commands are permitted
|
|
12
|
+
- **Environment Isolation**: Git processes run in a clean environment with minimal variables
|
|
13
|
+
|
|
14
|
+
## 🚫 Ref Validation
|
|
15
|
+
|
|
16
|
+
The `GitGraphAdapter` validates all ref arguments to prevent injection attacks:
|
|
17
|
+
|
|
18
|
+
- Refs must match the pattern: `^[a-zA-Z0-9._/-]+((~\d*|\^\d*|\.\.[a-zA-Z0-9._/-]+)*)$`
|
|
19
|
+
- Refs cannot start with `-` or `--` to prevent option injection
|
|
20
|
+
- Invalid refs throw an error immediately
|
|
21
|
+
|
|
22
|
+
## 🌊 Resource Protection
|
|
23
|
+
|
|
24
|
+
- **Streaming-First**: Large graph traversals use async generators to prevent OOM
|
|
25
|
+
- **Bitmap Indexing**: Sharded Roaring Bitmap indexes enable O(1) lookups without loading entire graphs
|
|
26
|
+
- **Delimiter Safety**: Uses ASCII Record Separator (`\x1E`) to prevent message collision
|
|
27
|
+
|
|
28
|
+
## 🐞 Reporting a Vulnerability
|
|
29
|
+
|
|
30
|
+
If you discover a security vulnerability, please send an e-mail to [james@flyingrobots.dev](mailto:james@flyingrobots.dev).
|
package/bin/git-warp
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const cliPath = resolve(dir, 'warp-graph.js');
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
let result;
|
|
12
|
+
|
|
13
|
+
if (existsSync(cliPath)) {
|
|
14
|
+
result = spawnSync(process.execPath, [cliPath, ...args], { stdio: 'inherit' });
|
|
15
|
+
} else {
|
|
16
|
+
result = spawnSync('warp-graph', args, { stdio: 'inherit' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (result.error) {
|
|
20
|
+
process.stderr.write(result.error.message + '\n');
|
|
21
|
+
process.exit(result.error.code || 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process.exit(result.status ?? 1);
|