@git-stunts/git-warp 11.5.1 → 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 +142 -10
- 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 +40 -0
- 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/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 +233 -5
- 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/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/warp/_wiredMethods.d.ts +7 -1
- 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/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,9 +67,32 @@ const result = await graph.query()
|
|
|
55
67
|
|
|
56
68
|
## How It Works
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
```
|
|
61
96
|
|
|
62
97
|
### The Multi-Writer Problem (and How It's Solved)
|
|
63
98
|
|
|
@@ -77,9 +112,20 @@ Every operation gets a unique **EventId** — `(lamport, writerId, patchSha, opI
|
|
|
77
112
|
|
|
78
113
|
## Multi-Writer Collaboration
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
```
|
|
83
129
|
|
|
84
130
|
Writers operate independently on the same Git repository. Sync happens through standard Git transport (push/pull) or the built-in HTTP sync protocol.
|
|
85
131
|
|
|
@@ -236,6 +282,45 @@ if (result.found) {
|
|
|
236
282
|
}
|
|
237
283
|
```
|
|
238
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
|
+
|
|
239
324
|
## Subscriptions & Reactivity
|
|
240
325
|
|
|
241
326
|
React to graph changes without polling. Handlers are called after `materialize()` when state has changed.
|
|
@@ -465,9 +550,50 @@ When a seek cursor is active, `query`, `info`, `materialize`, and `history` auto
|
|
|
465
550
|
|
|
466
551
|
## Architecture
|
|
467
552
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
```
|
|
471
597
|
|
|
472
598
|
The codebase follows hexagonal architecture with ports and adapters:
|
|
473
599
|
|
|
@@ -480,6 +606,7 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
480
606
|
- `LoggerPort` -- structured logging
|
|
481
607
|
- `ClockPort` -- time measurement
|
|
482
608
|
- `SeekCachePort` -- persistent seek materialization cache
|
|
609
|
+
- `NeighborProviderPort` -- abstract neighbor lookup interface
|
|
483
610
|
|
|
484
611
|
**Adapters** implement the ports:
|
|
485
612
|
- `GitGraphAdapter` -- wraps `@git-stunts/plumbing` for Git operations
|
|
@@ -496,7 +623,12 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
496
623
|
- `Writer` / `PatchSession` -- patch creation and commit
|
|
497
624
|
- `JoinReducer` -- CRDT-based state materialization
|
|
498
625
|
- `QueryBuilder` -- fluent query construction
|
|
499
|
-
- `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
|
|
500
632
|
- `SyncProtocol` -- multi-writer synchronization
|
|
501
633
|
- `CheckpointService` -- state snapshot creation and loading
|
|
502
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
|
@@ -19,6 +19,7 @@ import defaultClock from './utils/defaultClock.js';
|
|
|
19
19
|
import LogicalTraversal from './services/LogicalTraversal.js';
|
|
20
20
|
import LRUCache from './utils/LRUCache.js';
|
|
21
21
|
import SyncController from './services/SyncController.js';
|
|
22
|
+
import MaterializedViewService from './services/MaterializedViewService.js';
|
|
22
23
|
import { wireWarpMethods } from './warp/_wire.js';
|
|
23
24
|
import * as queryMethods from './warp/query.methods.js';
|
|
24
25
|
import * as subscribeMethods from './warp/subscribe.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
|
/**
|
|
@@ -175,6 +177,24 @@ export default class WarpGraph {
|
|
|
175
177
|
|
|
176
178
|
/** @type {SyncController} */
|
|
177
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;
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
/**
|
|
@@ -218,6 +238,21 @@ export default class WarpGraph {
|
|
|
218
238
|
}
|
|
219
239
|
}
|
|
220
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
|
+
|
|
221
256
|
/**
|
|
222
257
|
* Opens a multi-writer graph.
|
|
223
258
|
*
|
|
@@ -380,6 +415,11 @@ export default class WarpGraph {
|
|
|
380
415
|
}
|
|
381
416
|
return this._sortPatchesCausally(allPatches);
|
|
382
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
|
+
},
|
|
383
423
|
});
|
|
384
424
|
}
|
|
385
425
|
return this._temporalQuery;
|
|
@@ -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';
|