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.
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.
npm install @git-stunts/empty-graph @git-stunts/plumbingimport GitPlumbing from '@git-stunts/plumbing';
import WarpGraph, { GitGraphAdapter } from '@git-stunts/empty-graph';
const plumbing = new GitPlumbing({ cwd: './my-repo' });
const persistence = new GitGraphAdapter({ plumbing });
const graph = await WarpGraph.open({
persistence,
graphName: 'demo',
writerId: 'writer-1',
autoMaterialize: true, // auto-materialize on query
});
// Write data using the patch builder
await (await graph.createPatch())
.addNode('user:alice')
.setProperty('user:alice', 'name', 'Alice')
.setProperty('user:alice', 'role', 'admin')
.addNode('user:bob')
.setProperty('user:bob', 'name', 'Bob')
.addEdge('user:alice', 'user:bob', 'manages')
.commit();
// Query the graph
const result = await graph.query()
.match('user:*')
.outgoing('manages')
.run();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>.
Materialization replays all patches from all writers, applying CRDT merge semantics:
- 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.
- Properties use Last-Write-Wins (LWW) registers, ordered by Lamport timestamp, then writer ID, then patch SHA.
- Version vectors track causality across writers, ensuring deterministic convergence regardless of patch arrival order.
Checkpoints snapshot materialized state into a single commit for fast incremental recovery. Subsequent materializations only need to replay patches created after the checkpoint.
Writers operate independently on the same Git repository. Sync happens through standard Git transport (push/pull) or the built-in HTTP sync protocol.
// Writer A (on machine A)
const graphA = await WarpGraph.open({
persistence: persistenceA,
graphName: 'shared',
writerId: 'alice',
});
await (await graphA.createPatch())
.addNode('doc:1')
.setProperty('doc:1', 'title', 'Draft')
.commit();
// Writer B (on machine B)
const graphB = await WarpGraph.open({
persistence: persistenceB,
graphName: 'shared',
writerId: 'bob',
});
await (await graphB.createPatch())
.addNode('doc:2')
.setProperty('doc:2', 'title', 'Notes')
.commit();
// After git push/pull, materialize merges both writers
const state = await graphA.materialize();
await graphA.hasNode('doc:1'); // true
await graphA.hasNode('doc:2'); // true// Start a sync server
const server = await graphB.serve({ port: 3000 });
// Sync from another instance
await graphA.syncWith('http://localhost:3000/sync');
await server.close();// Sync two in-process instances directly
await graphA.syncWith(graphB);Query methods require materialized state. Either call materialize() first, or pass autoMaterialize: true to WarpGraph.open() to handle this automatically.
await graph.materialize();
await graph.getNodes(); // ['user:alice', 'user:bob']
await graph.hasNode('user:alice'); // true
await graph.getNodeProps('user:alice'); // Map { 'name' => 'Alice', 'role' => 'admin' }
await graph.neighbors('user:alice', 'outgoing'); // [{ nodeId: 'user:bob', label: 'manages', direction: 'outgoing' }]
await graph.getEdges(); // [{ from: 'user:alice', to: 'user:bob', label: 'manages' }]const result = await graph.query()
.match('user:*') // glob pattern matching
.outgoing('manages') // traverse outgoing edges with label
.select(['id', 'props']) // select fields
.run();const result = await graph.traverse.shortestPath('user:alice', 'user:bob', {
dir: 'outgoing',
labelFilter: 'manages',
maxDepth: 10,
});
if (result.found) {
console.log(result.path); // ['user:alice', 'user:bob']
console.log(result.length); // 1
}The patch builder supports six operations:
const sha = await (await graph.createPatch())
.addNode('n1') // create a node
.removeNode('n1') // tombstone a node
.addEdge('n1', 'n2', 'label') // create a directed edge
.removeEdge('n1', 'n2', 'label') // tombstone an edge
.setProperty('n1', 'key', 'value') // set a property (LWW)
.setProperty('n1', 'data', { nested: true }) // values can be any serializable type
.commit(); // commit as a single atomic patchEach 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.
For repeated writes, the Writer API is more convenient:
const writer = await graph.writer();
await writer.commitPatch(p => {
p.addNode('item:1');
p.setProperty('item:1', 'status', 'active');
});// Checkpoint current state for fast future materialization
await graph.materialize();
await graph.createCheckpoint();
// GC removes tombstones when safe
const metrics = graph.getGCMetrics();
const { ran, result } = graph.maybeRunGC();
// Or configure automatic checkpointing
const graph = await WarpGraph.open({
persistence,
graphName: 'demo',
writerId: 'writer-1',
checkpointPolicy: { every: 500 }, // auto-checkpoint every 500 patches
});The CLI is available as warp-graph or as a Git subcommand git warp.
# Install the git subcommand
npm run install:git-warp
# List graphs in a repo
git warp info
# Query nodes by pattern
git warp query --match 'user:*' --outgoing manages --json
# Find shortest path between nodes
git warp path --from user:alice --to user:bob --dir out
# Show patch history for a writer
git warp history --writer alice
# Check graph health and GC status
git warp checkAll commands accept --repo <path> to target a specific Git repository and --json for machine-readable output.
The codebase follows hexagonal architecture with ports and adapters:
Ports define abstract interfaces for infrastructure:
GraphPersistencePort-- Git operations (read/write commits, refs)IndexStoragePort-- bitmap index storageLoggerPort-- structured loggingClockPort-- time measurement
Adapters implement the ports:
GitGraphAdapter-- wraps@git-stunts/plumbingfor Git operationsConsoleLogger/NoOpLogger-- logging implementationsPerformanceClockAdapter/GlobalClockAdapter-- clock implementationsCborCodec-- CBOR serialization for patches
Domain contains the core logic:
WarpGraph-- public API facadeWriter/PatchSession-- patch creation and commitJoinReducer-- CRDT-based state materializationQueryBuilder-- fluent query constructionLogicalTraversal-- graph traversal over materialized stateSyncProtocol-- multi-writer synchronizationCheckpointService-- state snapshot creation and loadingBitmapIndexBuilder/BitmapIndexReader-- roaring bitmap indexesVersionVector/ORSet/LWW-- CRDT primitives
| Package | Purpose |
|---|---|
@git-stunts/plumbing |
Low-level Git operations |
@git-stunts/alfred |
Retry with exponential backoff |
@git-stunts/trailer-codec |
Git trailer encoding |
cbor-x |
CBOR binary serialization |
roaring |
Roaring bitmap indexes (native C++ bindings) |
zod |
Schema validation |
npm test # unit tests (vitest)
npm run lint # eslint
npm run test:bench # benchmarks
npm run test:bats # CLI integration tests (Docker + BATS)This package is the reference implementation of WARP (Worldline Algebra for Recursive Provenance) graphs as described in the AION Foundations Series. The papers define WARP graphs as a minimal recursive state object (Paper I), equip them with deterministic tick-based operational semantics (Paper II), and develop computational holography, provenance payloads, and prefix forks (Paper III). This codebase implements the core data structures and multi-writer collaboration protocol described in those papers.
Apache-2.0
Built by FLYING ROBOTS