@git-stunts/git-warp 11.5.0 → 12.0.0
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/README.md +145 -1
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/reindex.js +41 -0
- package/bin/cli/commands/verify-index.js +59 -0
- package/bin/cli/infrastructure.js +7 -2
- package/bin/cli/schemas.js +19 -0
- package/bin/cli/types.js +2 -0
- package/index.d.ts +49 -12
- package/package.json +2 -2
- package/src/domain/WarpGraph.js +62 -2
- package/src/domain/errors/ShardIdOverflowError.js +28 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
- package/src/domain/services/BitmapIndexReader.js +32 -10
- package/src/domain/services/BitmapNeighborProvider.js +178 -0
- package/src/domain/services/CheckpointMessageCodec.js +3 -3
- package/src/domain/services/CheckpointService.js +77 -12
- package/src/domain/services/GraphTraversal.js +1239 -0
- package/src/domain/services/IncrementalIndexUpdater.js +765 -0
- package/src/domain/services/JoinReducer.js +310 -46
- package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
- package/src/domain/services/LogicalIndexBuildService.js +108 -0
- package/src/domain/services/LogicalIndexReader.js +315 -0
- package/src/domain/services/LogicalTraversal.js +321 -202
- package/src/domain/services/MaterializedViewService.js +379 -0
- package/src/domain/services/ObserverView.js +138 -47
- package/src/domain/services/PatchBuilderV2.js +3 -3
- package/src/domain/services/PropertyIndexBuilder.js +64 -0
- package/src/domain/services/PropertyIndexReader.js +111 -0
- package/src/domain/services/SyncController.js +576 -0
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/types/PatchDiff.js +90 -0
- package/src/domain/types/WarpTypesV2.js +4 -4
- package/src/domain/utils/MinHeap.js +45 -17
- package/src/domain/utils/canonicalCbor.js +36 -0
- package/src/domain/utils/fnv1a.js +20 -0
- package/src/domain/utils/roaring.js +14 -3
- package/src/domain/utils/shardKey.js +40 -0
- package/src/domain/utils/toBytes.js +17 -0
- package/src/domain/utils/validateShardOid.js +13 -0
- package/src/domain/warp/_internal.js +0 -9
- package/src/domain/warp/_wiredMethods.d.ts +8 -2
- package/src/domain/warp/checkpoint.methods.js +21 -5
- package/src/domain/warp/materialize.methods.js +17 -5
- package/src/domain/warp/materializeAdvanced.methods.js +142 -3
- package/src/domain/warp/query.methods.js +78 -12
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
- package/src/ports/BlobPort.js +1 -1
- package/src/ports/NeighborProviderPort.js +59 -0
- package/src/ports/SeekCachePort.js +4 -3
- package/src/domain/warp/sync.methods.js +0 -554
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
<img src="docs/images/hero.gif" alt="git-warp CLI demo" width="600">
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
## What's New in v12.0.0
|
|
12
|
+
|
|
13
|
+
- **MaterializedViewService** — unified service orchestrating build, persist, and load of bitmap indexes and property readers as a single coherent materialized view. Checkpoints now embed the index (schema:4) for instant hydration on open.
|
|
14
|
+
- **GraphTraversal engine (11 algorithms)** — BFS, DFS, shortest path, Dijkstra, A\*, bidirectional A\*, topological sort, longest path, connected component, reachability, and common ancestors. All accessible via `graph.traverse.*`.
|
|
15
|
+
- **NeighborProviderPort abstraction** — decouples traversal algorithms from storage. Two implementations: `AdjacencyNeighborProvider` (in-memory) and `BitmapNeighborProvider` (O(1) bitmap lookups).
|
|
16
|
+
- **Logical bitmap index** — CBOR-sharded Roaring bitmap index with labeled edges, stable numeric IDs, and property indexes. `IncrementalIndexUpdater` enables O(diff) updates.
|
|
17
|
+
- **`nodeWeightFn`** — node-weighted graph algorithms (Dijkstra, A\*, longest path) as an alternative to edge-weight functions.
|
|
18
|
+
- **CLI: `verify-index` and `reindex`** — new commands for index integrity checks and forced rebuilds.
|
|
19
|
+
- **Cross-runtime hardening** — eliminated bare `Buffer` usage across the index subsystem; bitmap indexes now work on Node, Bun, and Deno.
|
|
20
|
+
|
|
21
|
+
See the [full changelog](CHANGELOG.md) for details.
|
|
22
|
+
|
|
11
23
|
## The Core Idea
|
|
12
24
|
|
|
13
25
|
**git-warp** is a graph database that doesn't need a database server. It stores all its data inside a Git repository by abusing a clever trick: every piece of data is a Git commit that points to the **empty tree** — a special object that exists in every Git repo. Because the commits don't reference any actual files, they're completely invisible to normal Git operations like `git log`, `git diff`, or `git status`. Your codebase stays untouched, but there's a full graph database living alongside it.
|
|
@@ -55,6 +67,33 @@ const result = await graph.query()
|
|
|
55
67
|
|
|
56
68
|
## How It Works
|
|
57
69
|
|
|
70
|
+
```mermaid
|
|
71
|
+
flowchart TB
|
|
72
|
+
subgraph normal["Normal Git Objects"]
|
|
73
|
+
h["HEAD"] -.-> m["refs/heads/main"]
|
|
74
|
+
m -.-> c3["C3 · a1b2c3d"]
|
|
75
|
+
c3 -->|parent| c2["C2 · e4f5a6b"]
|
|
76
|
+
c2 -->|parent| c1["C1 · 7c8d9e0"]
|
|
77
|
+
c3 -->|tree| t3["tree"]
|
|
78
|
+
t3 --> src["src/index.js"]
|
|
79
|
+
t3 --> pkg["package.json"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
subgraph warp["WARP Patch Objects"]
|
|
83
|
+
wr["refs/warp/myGraph/<br/>writers/alice"] -.-> p3["P3 lamport=3<br/>f1a2b3c"]
|
|
84
|
+
p3 -->|parent| p2["P2 lamport=2<br/>d4e5f6a"]
|
|
85
|
+
p2 -->|parent| p1["P1 lamport=1<br/>b7c8d9e"]
|
|
86
|
+
p3 -->|tree| wt["empty tree"]
|
|
87
|
+
wt --> pc["patch.cbor"]
|
|
88
|
+
wt --> cc["_content_*"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
vis1["✔ visible to git log"]
|
|
92
|
+
vis2["✘ invisible to git log<br/>(lives under refs/warp/)"]
|
|
93
|
+
vis1 ~~~ normal
|
|
94
|
+
vis2 ~~~ warp
|
|
95
|
+
```
|
|
96
|
+
|
|
58
97
|
### The Multi-Writer Problem (and How It's Solved)
|
|
59
98
|
|
|
60
99
|
Multiple people (or machines, or processes) can write to the same graph **simultaneously, without any coordination**. There's no central server, no locking, no "wait your turn."
|
|
@@ -73,6 +112,21 @@ Every operation gets a unique **EventId** — `(lamport, writerId, patchSha, opI
|
|
|
73
112
|
|
|
74
113
|
## Multi-Writer Collaboration
|
|
75
114
|
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart TB
|
|
117
|
+
subgraph alice["Alice"]
|
|
118
|
+
pa1["Pa1 · L=1"] --> pa2["Pa2 · L=2"] --> pa3["Pa3 · L=4"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
subgraph bob["Bob"]
|
|
122
|
+
pb1["Pb1 · L=1"] --> pb2["Pb2 · L=3"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
pa3 & pb2 --> sort["Sort by Lamport"]
|
|
126
|
+
sort --> reducer["JoinReducer<br/>OR-Set merge · LWW merge"]
|
|
127
|
+
reducer --> state["WarpStateV5<br/>nodeAlive · edgeAlive · prop · frontier"]
|
|
128
|
+
```
|
|
129
|
+
|
|
76
130
|
Writers operate independently on the same Git repository. Sync happens through standard Git transport (push/pull) or the built-in HTTP sync protocol.
|
|
77
131
|
|
|
78
132
|
```javascript
|
|
@@ -228,6 +282,45 @@ if (result.found) {
|
|
|
228
282
|
}
|
|
229
283
|
```
|
|
230
284
|
|
|
285
|
+
```javascript
|
|
286
|
+
// Weighted shortest path (Dijkstra) with edge weight function
|
|
287
|
+
const weighted = await graph.traverse.weightedShortestPath('city:a', 'city:z', {
|
|
288
|
+
dir: 'outgoing',
|
|
289
|
+
weightFn: (from, to, label) => edgeWeights.get(`${from}-${to}`) ?? 1,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Weighted shortest path with per-node weight function
|
|
293
|
+
const nodeWeighted = await graph.traverse.weightedShortestPath('city:a', 'city:z', {
|
|
294
|
+
dir: 'outgoing',
|
|
295
|
+
nodeWeightFn: (nodeId) => nodeDelays.get(nodeId) ?? 0,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// A* search with heuristic
|
|
299
|
+
const astar = await graph.traverse.aStarSearch('city:a', 'city:z', {
|
|
300
|
+
dir: 'outgoing',
|
|
301
|
+
heuristic: (nodeId) => euclideanDistance(coords[nodeId], coords['city:z']),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Topological sort (Kahn's algorithm with cycle detection)
|
|
305
|
+
const sorted = await graph.traverse.topologicalSort('task:root', {
|
|
306
|
+
dir: 'outgoing',
|
|
307
|
+
labelFilter: 'depends-on',
|
|
308
|
+
});
|
|
309
|
+
// sorted = ['task:root', 'task:auth', 'task:caching', ...]
|
|
310
|
+
|
|
311
|
+
// Longest path on DAGs (critical path)
|
|
312
|
+
const critical = await graph.traverse.weightedLongestPath('task:start', 'task:end', {
|
|
313
|
+
dir: 'outgoing',
|
|
314
|
+
weightFn: (from, to, label) => taskDurations.get(to) ?? 1,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Reachability check (fast, no path reconstruction)
|
|
318
|
+
const canReach = await graph.traverse.isReachable('user:alice', 'user:bob', {
|
|
319
|
+
dir: 'outgoing',
|
|
320
|
+
});
|
|
321
|
+
// canReach = true | false
|
|
322
|
+
```
|
|
323
|
+
|
|
231
324
|
## Subscriptions & Reactivity
|
|
232
325
|
|
|
233
326
|
React to graph changes without polling. Handlers are called after `materialize()` when state has changed.
|
|
@@ -457,6 +550,51 @@ When a seek cursor is active, `query`, `info`, `materialize`, and `history` auto
|
|
|
457
550
|
|
|
458
551
|
## Architecture
|
|
459
552
|
|
|
553
|
+
```mermaid
|
|
554
|
+
flowchart TB
|
|
555
|
+
subgraph adapters["Adapters — infrastructure implementations"]
|
|
556
|
+
git["GitGraphAdapter"]
|
|
557
|
+
cbor["CborCodec"]
|
|
558
|
+
webcrypto["WebCryptoAdapter"]
|
|
559
|
+
clocka["ClockAdapter"]
|
|
560
|
+
consolel["ConsoleLogger"]
|
|
561
|
+
cascache["CasSeekCacheAdapter"]
|
|
562
|
+
|
|
563
|
+
subgraph ports["Ports — abstract interfaces"]
|
|
564
|
+
pp["GraphPersistencePort"]
|
|
565
|
+
cp["CodecPort"]
|
|
566
|
+
crp["CryptoPort"]
|
|
567
|
+
clp["ClockPort"]
|
|
568
|
+
lp["LoggerPort"]
|
|
569
|
+
ip["IndexStoragePort"]
|
|
570
|
+
sp["SeekCachePort"]
|
|
571
|
+
np["NeighborProviderPort"]
|
|
572
|
+
|
|
573
|
+
subgraph domain["Domain Core"]
|
|
574
|
+
wg["WarpGraph — main API facade"]
|
|
575
|
+
jr["JoinReducer"]
|
|
576
|
+
pb["PatchBuilderV2"]
|
|
577
|
+
cs["CheckpointService"]
|
|
578
|
+
qb["QueryBuilder"]
|
|
579
|
+
lt["LogicalTraversal"]
|
|
580
|
+
gt["GraphTraversal"]
|
|
581
|
+
mvs["MaterializedViewService"]
|
|
582
|
+
crdts["CRDTs: VersionVector · ORSet · LWW"]
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
pp -.->|implements| git
|
|
588
|
+
cp -.->|implements| cbor
|
|
589
|
+
crp -.->|implements| webcrypto
|
|
590
|
+
clp -.->|implements| clocka
|
|
591
|
+
lp -.->|implements| consolel
|
|
592
|
+
sp -.->|implements| cascache
|
|
593
|
+
ip -.->|via| git
|
|
594
|
+
|
|
595
|
+
wg --> pp & cp & crp & clp & lp & ip & sp
|
|
596
|
+
```
|
|
597
|
+
|
|
460
598
|
The codebase follows hexagonal architecture with ports and adapters:
|
|
461
599
|
|
|
462
600
|
**Ports** define abstract interfaces for infrastructure:
|
|
@@ -468,6 +606,7 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
468
606
|
- `LoggerPort` -- structured logging
|
|
469
607
|
- `ClockPort` -- time measurement
|
|
470
608
|
- `SeekCachePort` -- persistent seek materialization cache
|
|
609
|
+
- `NeighborProviderPort` -- abstract neighbor lookup interface
|
|
471
610
|
|
|
472
611
|
**Adapters** implement the ports:
|
|
473
612
|
- `GitGraphAdapter` -- wraps `@git-stunts/plumbing` for Git operations
|
|
@@ -484,7 +623,12 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
484
623
|
- `Writer` / `PatchSession` -- patch creation and commit
|
|
485
624
|
- `JoinReducer` -- CRDT-based state materialization
|
|
486
625
|
- `QueryBuilder` -- fluent query construction
|
|
487
|
-
- `LogicalTraversal` --
|
|
626
|
+
- `LogicalTraversal` -- deprecated facade, delegates to GraphTraversal
|
|
627
|
+
- `GraphTraversal` -- unified traversal engine (11 algorithms, `nodeWeightFn`)
|
|
628
|
+
- `MaterializedViewService` -- orchestrate build/persist/load of materialized views
|
|
629
|
+
- `IncrementalIndexUpdater` -- O(diff) bitmap index updates
|
|
630
|
+
- `LogicalIndexBuildService` / `LogicalIndexReader` -- logical bitmap indexes
|
|
631
|
+
- `AdjacencyNeighborProvider` / `BitmapNeighborProvider` -- neighbor provider implementations
|
|
488
632
|
- `SyncProtocol` -- multi-writer synchronization
|
|
489
633
|
- `CheckpointService` -- state snapshot creation and loading
|
|
490
634
|
- `ObserverView` -- read-only filtered graph projections
|
|
@@ -7,6 +7,8 @@ import handleDoctor from './doctor/index.js';
|
|
|
7
7
|
import handleMaterialize from './materialize.js';
|
|
8
8
|
import handleSeek from './seek.js';
|
|
9
9
|
import handleVerifyAudit from './verify-audit.js';
|
|
10
|
+
import handleVerifyIndex from './verify-index.js';
|
|
11
|
+
import handleReindex from './reindex.js';
|
|
10
12
|
import handleView from './view.js';
|
|
11
13
|
import handleInstallHooks from './install-hooks.js';
|
|
12
14
|
import handleTrust from './trust.js';
|
|
@@ -24,6 +26,8 @@ export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
|
|
|
24
26
|
['materialize', handleMaterialize],
|
|
25
27
|
['seek', handleSeek],
|
|
26
28
|
['verify-audit', handleVerifyAudit],
|
|
29
|
+
['verify-index', handleVerifyIndex],
|
|
30
|
+
['reindex', handleReindex],
|
|
27
31
|
['trust', handleTrust],
|
|
28
32
|
['patch', handlePatch],
|
|
29
33
|
['tree', handleTree],
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EXIT_CODES, parseCommandArgs } from '../infrastructure.js';
|
|
2
|
+
import { reindexSchema } from '../schemas.js';
|
|
3
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Handles the `reindex` command: forces a full bitmap index rebuild
|
|
9
|
+
* by clearing cached index state and re-materializing.
|
|
10
|
+
*
|
|
11
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
12
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
13
|
+
*/
|
|
14
|
+
export default async function handleReindex({ options, args }) {
|
|
15
|
+
parseCommandArgs(args, {}, reindexSchema);
|
|
16
|
+
|
|
17
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
18
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
19
|
+
emitCursorWarning(cursorInfo, null);
|
|
20
|
+
|
|
21
|
+
// Clear cached index to force full rebuild
|
|
22
|
+
graph.invalidateIndex();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await graph.materialize();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return {
|
|
28
|
+
payload: { error: /** @type {Error} */ (err).message },
|
|
29
|
+
exitCode: EXIT_CODES.INTERNAL,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
payload: {
|
|
35
|
+
graph: graphName,
|
|
36
|
+
status: 'ok',
|
|
37
|
+
message: 'Index rebuilt successfully',
|
|
38
|
+
},
|
|
39
|
+
exitCode: EXIT_CODES.OK,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EXIT_CODES, parseCommandArgs } from '../infrastructure.js';
|
|
2
|
+
import { verifyIndexSchema } from '../schemas.js';
|
|
3
|
+
import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('../types.js').CliOptions} CliOptions */
|
|
6
|
+
|
|
7
|
+
const VERIFY_INDEX_OPTIONS = {
|
|
8
|
+
seed: { type: 'string' },
|
|
9
|
+
'sample-rate': { type: 'string' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles the `verify-index` command: samples alive nodes and cross-checks
|
|
14
|
+
* bitmap index neighbors against adjacency ground truth.
|
|
15
|
+
*
|
|
16
|
+
* @param {{options: CliOptions, args: string[]}} params
|
|
17
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
18
|
+
*/
|
|
19
|
+
export default async function handleVerifyIndex({ options, args }) {
|
|
20
|
+
const { values } = parseCommandArgs(
|
|
21
|
+
args,
|
|
22
|
+
VERIFY_INDEX_OPTIONS,
|
|
23
|
+
verifyIndexSchema,
|
|
24
|
+
);
|
|
25
|
+
const { graph, graphName, persistence } = await openGraph(options);
|
|
26
|
+
const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
|
|
27
|
+
emitCursorWarning(cursorInfo, null);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await graph.materialize();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return {
|
|
34
|
+
payload: { error: message },
|
|
35
|
+
exitCode: EXIT_CODES.INTERNAL,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = await graph.verifyIndex({ seed: values.seed, sampleRate: values.sampleRate });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
const noIndex = /no bitmap index|cannot verify index|index not built/i.test(message);
|
|
45
|
+
return {
|
|
46
|
+
payload: { error: noIndex ? 'No bitmap index available after materialization' : message },
|
|
47
|
+
exitCode: EXIT_CODES.INTERNAL,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
payload: {
|
|
53
|
+
graph: graphName,
|
|
54
|
+
...result,
|
|
55
|
+
totalChecks: result.passed + result.failed,
|
|
56
|
+
},
|
|
57
|
+
exitCode: result.failed > 0 ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -42,6 +42,8 @@ Commands:
|
|
|
42
42
|
check Report graph health/GC status
|
|
43
43
|
doctor Diagnose structural issues and suggest fixes
|
|
44
44
|
verify-audit Verify audit receipt chain integrity
|
|
45
|
+
verify-index Verify bitmap index integrity by sampling
|
|
46
|
+
reindex Force full index rebuild
|
|
45
47
|
trust Evaluate writer trust from signed evidence
|
|
46
48
|
materialize Materialize and checkpoint all graphs
|
|
47
49
|
seek Time-travel: step through graph history by Lamport tick
|
|
@@ -88,6 +90,10 @@ Verify-audit options:
|
|
|
88
90
|
--trust-mode <mode> Trust evaluation mode (warn, enforce)
|
|
89
91
|
--trust-pin <sha> Pin trust evaluation to a specific record chain commit
|
|
90
92
|
|
|
93
|
+
Verify-index options:
|
|
94
|
+
--seed <n> PRNG seed for reproducible sampling
|
|
95
|
+
--sample-rate <rate> Fraction of nodes to verify (>0 and <=1, default 0.1)
|
|
96
|
+
|
|
91
97
|
Trust options:
|
|
92
98
|
--mode <warn|enforce> Override trust evaluation mode
|
|
93
99
|
--trust-pin <sha> Pin trust evaluation to a specific record chain commit
|
|
@@ -144,7 +150,7 @@ export function notFoundError(message) {
|
|
|
144
150
|
return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'patch', 'tree', 'install-hooks', 'view'];
|
|
153
|
+
export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'verify-index', 'reindex', 'trust', 'patch', 'tree', 'install-hooks', 'view'];
|
|
148
154
|
|
|
149
155
|
const BASE_OPTIONS = {
|
|
150
156
|
repo: { type: 'string', short: 'r' },
|
|
@@ -347,4 +353,3 @@ export function parseCommandArgs(args, config, schema, { allowPositionals = fals
|
|
|
347
353
|
|
|
348
354
|
return { values: result.data, positionals: parsed.positionals || [] };
|
|
349
355
|
}
|
|
350
|
-
|
package/bin/cli/schemas.js
CHANGED
|
@@ -175,3 +175,22 @@ export const seekSchema = z.object({
|
|
|
175
175
|
diffLimit: val['diff-limit'],
|
|
176
176
|
};
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Verify-index
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
export const verifyIndexSchema = z.object({
|
|
184
|
+
seed: z.coerce.number().int().min(-2147483648).max(2147483647).optional(),
|
|
185
|
+
'sample-rate': z.coerce.number().gt(0, '--sample-rate must be greater than 0').max(1).optional().default(0.1),
|
|
186
|
+
}).strict().transform((val) => ({
|
|
187
|
+
seed: val.seed,
|
|
188
|
+
sampleRate: val['sample-rate'],
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Reindex
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
// No command-level options; schema exists for forward compatibility
|
|
196
|
+
export const reindexSchema = z.object({}).strict();
|
package/bin/cli/types.js
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* @property {{clear: () => Promise<void>} | null} seekCache
|
|
35
35
|
* @property {number} [_seekCeiling]
|
|
36
36
|
* @property {boolean} [_provenanceDegraded]
|
|
37
|
+
* @property {(options?: {seed?: number, sampleRate?: number}) => Promise<{passed: number, failed: number, errors: Array<{nodeId: string, direction: string, error: string}>}>} verifyIndex
|
|
38
|
+
* @property {() => void} invalidateIndex
|
|
37
39
|
*/
|
|
38
40
|
|
|
39
41
|
/**
|
package/index.d.ts
CHANGED
|
@@ -238,26 +238,63 @@ export class QueryBuilder {
|
|
|
238
238
|
/**
|
|
239
239
|
* Logical graph traversal module.
|
|
240
240
|
*/
|
|
241
|
+
export interface TraverseFacadeOptions {
|
|
242
|
+
maxDepth?: number;
|
|
243
|
+
dir?: 'out' | 'in' | 'both';
|
|
244
|
+
labelFilter?: string | string[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type EdgeWeightFn = (from: string, to: string, label: string) => number | Promise<number>;
|
|
248
|
+
export type NodeWeightFn = (nodeId: string) => number | Promise<number>;
|
|
249
|
+
export type WeightedCostSelector =
|
|
250
|
+
| { weightFn?: EdgeWeightFn; nodeWeightFn?: never }
|
|
251
|
+
| { nodeWeightFn?: NodeWeightFn; weightFn?: never };
|
|
252
|
+
|
|
241
253
|
export interface LogicalTraversal {
|
|
242
|
-
bfs(start: string, options?:
|
|
254
|
+
bfs(start: string, options?: TraverseFacadeOptions): Promise<string[]>;
|
|
255
|
+
dfs(start: string, options?: TraverseFacadeOptions): Promise<string[]>;
|
|
256
|
+
shortestPath(from: string, to: string, options?: TraverseFacadeOptions): Promise<{ found: boolean; path: string[]; length: number }>;
|
|
257
|
+
connectedComponent(start: string, options?: {
|
|
243
258
|
maxDepth?: number;
|
|
244
|
-
dir?: 'out' | 'in' | 'both';
|
|
245
259
|
labelFilter?: string | string[];
|
|
246
260
|
}): Promise<string[]>;
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
isReachable(from: string, to: string, options?: TraverseFacadeOptions & {
|
|
262
|
+
signal?: AbortSignal;
|
|
263
|
+
}): Promise<{ reachable: boolean }>;
|
|
264
|
+
weightedShortestPath(from: string, to: string, options?: WeightedCostSelector & {
|
|
249
265
|
dir?: 'out' | 'in' | 'both';
|
|
250
266
|
labelFilter?: string | string[];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
signal?: AbortSignal;
|
|
268
|
+
}): Promise<{ path: string[]; totalCost: number }>;
|
|
269
|
+
aStarSearch(from: string, to: string, options?: WeightedCostSelector & {
|
|
254
270
|
dir?: 'out' | 'in' | 'both';
|
|
255
271
|
labelFilter?: string | string[];
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
heuristicFn?: (nodeId: string, goalId: string) => number;
|
|
273
|
+
signal?: AbortSignal;
|
|
274
|
+
}): Promise<{ path: string[]; totalCost: number; nodesExplored: number }>;
|
|
275
|
+
bidirectionalAStar(from: string, to: string, options?: WeightedCostSelector & {
|
|
276
|
+
labelFilter?: string | string[];
|
|
277
|
+
forwardHeuristic?: (nodeId: string, goalId: string) => number;
|
|
278
|
+
backwardHeuristic?: (nodeId: string, goalId: string) => number;
|
|
279
|
+
signal?: AbortSignal;
|
|
280
|
+
}): Promise<{ path: string[]; totalCost: number; nodesExplored: number }>;
|
|
281
|
+
topologicalSort(start: string | string[], options?: {
|
|
282
|
+
dir?: 'out' | 'in' | 'both';
|
|
283
|
+
labelFilter?: string | string[];
|
|
284
|
+
throwOnCycle?: boolean;
|
|
285
|
+
signal?: AbortSignal;
|
|
286
|
+
}): Promise<{ sorted: string[]; hasCycle: boolean }>;
|
|
287
|
+
commonAncestors(nodes: string[], options?: {
|
|
258
288
|
maxDepth?: number;
|
|
259
289
|
labelFilter?: string | string[];
|
|
260
|
-
|
|
290
|
+
maxResults?: number;
|
|
291
|
+
signal?: AbortSignal;
|
|
292
|
+
}): Promise<{ ancestors: string[] }>;
|
|
293
|
+
weightedLongestPath(from: string, to: string, options?: WeightedCostSelector & {
|
|
294
|
+
dir?: 'out' | 'in' | 'both';
|
|
295
|
+
labelFilter?: string | string[];
|
|
296
|
+
signal?: AbortSignal;
|
|
297
|
+
}): Promise<{ path: string[]; totalCost: number }>;
|
|
261
298
|
}
|
|
262
299
|
|
|
263
300
|
/**
|
|
@@ -486,9 +523,9 @@ export class GlobalClockAdapter extends ClockPort {
|
|
|
486
523
|
*/
|
|
487
524
|
export abstract class SeekCachePort {
|
|
488
525
|
/** Retrieves a cached state buffer by key, or null on miss. */
|
|
489
|
-
abstract get(key: string): Promise<Buffer | null>;
|
|
526
|
+
abstract get(key: string): Promise<{ buffer: Buffer | Uint8Array; indexTreeOid?: string } | null>;
|
|
490
527
|
/** Stores a state buffer under the given key. */
|
|
491
|
-
abstract set(key: string, buffer: Buffer): Promise<void>;
|
|
528
|
+
abstract set(key: string, buffer: Buffer | Uint8Array, options?: { indexTreeOid?: string }): Promise<void>;
|
|
492
529
|
/** Checks whether a key exists in the cache index. */
|
|
493
530
|
abstract has(key: string): Promise<boolean>;
|
|
494
531
|
/** Lists all keys currently in the cache index. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@git-stunts/git-warp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"description": "Deterministic WARP graph over Git: graph-native storage, traversal, and tooling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"roaring": "^2.7.0",
|
|
108
108
|
"string-width": "^7.1.0",
|
|
109
109
|
"wrap-ansi": "^9.0.0",
|
|
110
|
-
"zod": "
|
|
110
|
+
"zod": "3.24.1"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@eslint/js": "^9.17.0",
|
package/src/domain/WarpGraph.js
CHANGED
|
@@ -18,12 +18,13 @@ import defaultCrypto from './utils/defaultCrypto.js';
|
|
|
18
18
|
import defaultClock from './utils/defaultClock.js';
|
|
19
19
|
import LogicalTraversal from './services/LogicalTraversal.js';
|
|
20
20
|
import LRUCache from './utils/LRUCache.js';
|
|
21
|
+
import SyncController from './services/SyncController.js';
|
|
22
|
+
import MaterializedViewService from './services/MaterializedViewService.js';
|
|
21
23
|
import { wireWarpMethods } from './warp/_wire.js';
|
|
22
24
|
import * as queryMethods from './warp/query.methods.js';
|
|
23
25
|
import * as subscribeMethods from './warp/subscribe.methods.js';
|
|
24
26
|
import * as provenanceMethods from './warp/provenance.methods.js';
|
|
25
27
|
import * as forkMethods from './warp/fork.methods.js';
|
|
26
|
-
import * as syncMethods from './warp/sync.methods.js';
|
|
27
28
|
import * as checkpointMethods from './warp/checkpoint.methods.js';
|
|
28
29
|
import * as patchMethods from './warp/patch.methods.js';
|
|
29
30
|
import * as materializeMethods from './warp/materialize.methods.js';
|
|
@@ -40,6 +41,7 @@ const DEFAULT_ADJACENCY_CACHE_SIZE = 3;
|
|
|
40
41
|
* @property {import('./services/JoinReducer.js').WarpStateV5} state
|
|
41
42
|
* @property {string} stateHash
|
|
42
43
|
* @property {{outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}} adjacency
|
|
44
|
+
* @property {import('./services/BitmapNeighborProvider.js').default} [provider]
|
|
43
45
|
*/
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -172,6 +174,27 @@ export default class WarpGraph {
|
|
|
172
174
|
|
|
173
175
|
/** @type {number} */
|
|
174
176
|
this._auditSkipCount = 0;
|
|
177
|
+
|
|
178
|
+
/** @type {SyncController} */
|
|
179
|
+
this._syncController = new SyncController(this);
|
|
180
|
+
|
|
181
|
+
/** @type {MaterializedViewService} */
|
|
182
|
+
this._viewService = new MaterializedViewService({
|
|
183
|
+
codec: this._codec,
|
|
184
|
+
logger: this._logger || undefined,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/** @type {import('./services/BitmapNeighborProvider.js').LogicalIndex|null} */
|
|
188
|
+
this._logicalIndex = null;
|
|
189
|
+
|
|
190
|
+
/** @type {import('./services/PropertyIndexReader.js').default|null} */
|
|
191
|
+
this._propertyReader = null;
|
|
192
|
+
|
|
193
|
+
/** @type {string|null} */
|
|
194
|
+
this._cachedViewHash = null;
|
|
195
|
+
|
|
196
|
+
/** @type {Record<string, Uint8Array>|null} */
|
|
197
|
+
this._cachedIndexTree = null;
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
/**
|
|
@@ -215,6 +238,21 @@ export default class WarpGraph {
|
|
|
215
238
|
}
|
|
216
239
|
}
|
|
217
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Extracts the maximum Lamport timestamp from a WarpStateV5.
|
|
243
|
+
*
|
|
244
|
+
* @param {import('./services/JoinReducer.js').WarpStateV5} state
|
|
245
|
+
* @returns {number} Maximum Lamport value (0 if frontier is empty)
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
_maxLamportFromState(state) {
|
|
249
|
+
let max = 0;
|
|
250
|
+
for (const v of state.observedFrontier.values()) {
|
|
251
|
+
if (v > max) { max = v; }
|
|
252
|
+
}
|
|
253
|
+
return max;
|
|
254
|
+
}
|
|
255
|
+
|
|
218
256
|
/**
|
|
219
257
|
* Opens a multi-writer graph.
|
|
220
258
|
*
|
|
@@ -377,6 +415,11 @@ export default class WarpGraph {
|
|
|
377
415
|
}
|
|
378
416
|
return this._sortPatchesCausally(allPatches);
|
|
379
417
|
},
|
|
418
|
+
loadCheckpoint: async () => {
|
|
419
|
+
const ck = await this._loadLatestCheckpoint();
|
|
420
|
+
if (!ck) { return null; }
|
|
421
|
+
return { state: ck.state, maxLamport: this._maxLamportFromState(ck.state) };
|
|
422
|
+
},
|
|
380
423
|
});
|
|
381
424
|
}
|
|
382
425
|
return this._temporalQuery;
|
|
@@ -410,9 +453,26 @@ wireWarpMethods(WarpGraph, [
|
|
|
410
453
|
subscribeMethods,
|
|
411
454
|
provenanceMethods,
|
|
412
455
|
forkMethods,
|
|
413
|
-
syncMethods,
|
|
414
456
|
checkpointMethods,
|
|
415
457
|
patchMethods,
|
|
416
458
|
materializeMethods,
|
|
417
459
|
materializeAdvancedMethods,
|
|
418
460
|
]);
|
|
461
|
+
|
|
462
|
+
// ── Sync methods: direct delegation to SyncController (no stub file) ────────
|
|
463
|
+
const syncDelegates = /** @type {const} */ ([
|
|
464
|
+
'getFrontier', 'hasFrontierChanged', 'status',
|
|
465
|
+
'createSyncRequest', 'processSyncRequest', 'applySyncResponse',
|
|
466
|
+
'syncNeeded', 'syncWith', 'serve',
|
|
467
|
+
]);
|
|
468
|
+
for (const method of syncDelegates) {
|
|
469
|
+
Object.defineProperty(WarpGraph.prototype, method, {
|
|
470
|
+
// eslint-disable-next-line object-shorthand -- function keyword needed for `this` binding
|
|
471
|
+
value: /** @this {WarpGraph} @param {*[]} args */ function (...args) {
|
|
472
|
+
return this._syncController[method](...args);
|
|
473
|
+
},
|
|
474
|
+
writable: true,
|
|
475
|
+
configurable: true,
|
|
476
|
+
enumerable: false,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import IndexError from './IndexError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a shard's local ID counter exceeds 2^24.
|
|
5
|
+
*
|
|
6
|
+
* Each shard byte supports up to 2^24 local IDs. When this limit
|
|
7
|
+
* is reached, no more nodes can be registered in that shard.
|
|
8
|
+
*
|
|
9
|
+
* The `code` property is set to `'E_SHARD_ID_OVERFLOW'` and is correctly
|
|
10
|
+
* forwarded through the IndexError -> WarpError chain: IndexError passes
|
|
11
|
+
* the options object to WarpError, which prefers `options.code` over its
|
|
12
|
+
* default code (`'INDEX_ERROR'`).
|
|
13
|
+
*
|
|
14
|
+
* @class ShardIdOverflowError
|
|
15
|
+
* @extends IndexError
|
|
16
|
+
*/
|
|
17
|
+
export default class ShardIdOverflowError extends IndexError {
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} message
|
|
20
|
+
* @param {{ shardKey: string, nextLocalId: number }} context
|
|
21
|
+
*/
|
|
22
|
+
constructor(message, { shardKey, nextLocalId }) {
|
|
23
|
+
super(message, {
|
|
24
|
+
code: 'E_SHARD_ID_OVERFLOW',
|
|
25
|
+
context: { shardKey, nextLocalId },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -12,6 +12,7 @@ export { default as OperationAbortedError } from './OperationAbortedError.js';
|
|
|
12
12
|
export { default as QueryError } from './QueryError.js';
|
|
13
13
|
export { default as SyncError } from './SyncError.js';
|
|
14
14
|
export { default as ShardCorruptionError } from './ShardCorruptionError.js';
|
|
15
|
+
export { default as ShardIdOverflowError } from './ShardIdOverflowError.js';
|
|
15
16
|
export { default as ShardLoadError } from './ShardLoadError.js';
|
|
16
17
|
export { default as ShardValidationError } from './ShardValidationError.js';
|
|
17
18
|
export { default as StorageError } from './StorageError.js';
|