@git-stunts/git-warp 11.5.1 → 12.1.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 +137 -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 +52 -15
- package/package.json +3 -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 +132 -69
- 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/QueryBuilder.js +15 -44
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/services/TranslationCost.js +8 -24
- 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/matchGlob.js +51 -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 +83 -15
- 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,13 @@
|
|
|
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.1.0
|
|
12
|
+
|
|
13
|
+
- **Multi-pattern glob support** — `graph.observer()`, `query().match()`, and `translationCost()` now accept an array of glob patterns (e.g. `['campaign:*', 'milestone:*']`). Nodes matching *any* pattern in the array are included (OR semantics).
|
|
14
|
+
- **Release preflight** — `npm run release:preflight` runs a 10-check local gate (version agreement, CHANGELOG, README, lint, types, tests, pack dry-runs) before tagging.
|
|
15
|
+
|
|
16
|
+
See the [full changelog](CHANGELOG.md) for details.
|
|
17
|
+
|
|
11
18
|
## The Core Idea
|
|
12
19
|
|
|
13
20
|
**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 +62,32 @@ const result = await graph.query()
|
|
|
55
62
|
|
|
56
63
|
## How It Works
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
```mermaid
|
|
66
|
+
flowchart TB
|
|
67
|
+
subgraph normal["Normal Git Objects"]
|
|
68
|
+
h["HEAD"] -.-> m["refs/heads/main"]
|
|
69
|
+
m -.-> c3["C3 · a1b2c3d"]
|
|
70
|
+
c3 -->|parent| c2["C2 · e4f5a6b"]
|
|
71
|
+
c2 -->|parent| c1["C1 · 7c8d9e0"]
|
|
72
|
+
c3 -->|tree| t3["tree"]
|
|
73
|
+
t3 --> src["src/index.js"]
|
|
74
|
+
t3 --> pkg["package.json"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
subgraph warp["WARP Patch Objects"]
|
|
78
|
+
wr["refs/warp/myGraph/<br/>writers/alice"] -.-> p3["P3 lamport=3<br/>f1a2b3c"]
|
|
79
|
+
p3 -->|parent| p2["P2 lamport=2<br/>d4e5f6a"]
|
|
80
|
+
p2 -->|parent| p1["P1 lamport=1<br/>b7c8d9e"]
|
|
81
|
+
p3 -->|tree| wt["empty tree"]
|
|
82
|
+
wt --> pc["patch.cbor"]
|
|
83
|
+
wt --> cc["_content_*"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
vis1["✔ visible to git log"]
|
|
87
|
+
vis2["✘ invisible to git log<br/>(lives under refs/warp/)"]
|
|
88
|
+
vis1 ~~~ normal
|
|
89
|
+
vis2 ~~~ warp
|
|
90
|
+
```
|
|
61
91
|
|
|
62
92
|
### The Multi-Writer Problem (and How It's Solved)
|
|
63
93
|
|
|
@@ -77,9 +107,20 @@ Every operation gets a unique **EventId** — `(lamport, writerId, patchSha, opI
|
|
|
77
107
|
|
|
78
108
|
## Multi-Writer Collaboration
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
```mermaid
|
|
111
|
+
flowchart TB
|
|
112
|
+
subgraph alice["Alice"]
|
|
113
|
+
pa1["Pa1 · L=1"] --> pa2["Pa2 · L=2"] --> pa3["Pa3 · L=4"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
subgraph bob["Bob"]
|
|
117
|
+
pb1["Pb1 · L=1"] --> pb2["Pb2 · L=3"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
pa3 & pb2 --> sort["Sort by Lamport"]
|
|
121
|
+
sort --> reducer["JoinReducer<br/>OR-Set merge · LWW merge"]
|
|
122
|
+
reducer --> state["WarpStateV5<br/>nodeAlive · edgeAlive · prop · frontier"]
|
|
123
|
+
```
|
|
83
124
|
|
|
84
125
|
Writers operate independently on the same Git repository. Sync happens through standard Git transport (push/pull) or the built-in HTTP sync protocol.
|
|
85
126
|
|
|
@@ -236,6 +277,45 @@ if (result.found) {
|
|
|
236
277
|
}
|
|
237
278
|
```
|
|
238
279
|
|
|
280
|
+
```javascript
|
|
281
|
+
// Weighted shortest path (Dijkstra) with edge weight function
|
|
282
|
+
const weighted = await graph.traverse.weightedShortestPath('city:a', 'city:z', {
|
|
283
|
+
dir: 'outgoing',
|
|
284
|
+
weightFn: (from, to, label) => edgeWeights.get(`${from}-${to}`) ?? 1,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Weighted shortest path with per-node weight function
|
|
288
|
+
const nodeWeighted = await graph.traverse.weightedShortestPath('city:a', 'city:z', {
|
|
289
|
+
dir: 'outgoing',
|
|
290
|
+
nodeWeightFn: (nodeId) => nodeDelays.get(nodeId) ?? 0,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// A* search with heuristic
|
|
294
|
+
const astar = await graph.traverse.aStarSearch('city:a', 'city:z', {
|
|
295
|
+
dir: 'outgoing',
|
|
296
|
+
heuristic: (nodeId) => euclideanDistance(coords[nodeId], coords['city:z']),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Topological sort (Kahn's algorithm with cycle detection)
|
|
300
|
+
const sorted = await graph.traverse.topologicalSort('task:root', {
|
|
301
|
+
dir: 'outgoing',
|
|
302
|
+
labelFilter: 'depends-on',
|
|
303
|
+
});
|
|
304
|
+
// sorted = ['task:root', 'task:auth', 'task:caching', ...]
|
|
305
|
+
|
|
306
|
+
// Longest path on DAGs (critical path)
|
|
307
|
+
const critical = await graph.traverse.weightedLongestPath('task:start', 'task:end', {
|
|
308
|
+
dir: 'outgoing',
|
|
309
|
+
weightFn: (from, to, label) => taskDurations.get(to) ?? 1,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Reachability check (fast, no path reconstruction)
|
|
313
|
+
const canReach = await graph.traverse.isReachable('user:alice', 'user:bob', {
|
|
314
|
+
dir: 'outgoing',
|
|
315
|
+
});
|
|
316
|
+
// canReach = true | false
|
|
317
|
+
```
|
|
318
|
+
|
|
239
319
|
## Subscriptions & Reactivity
|
|
240
320
|
|
|
241
321
|
React to graph changes without polling. Handlers are called after `materialize()` when state has changed.
|
|
@@ -465,9 +545,50 @@ When a seek cursor is active, `query`, `info`, `materialize`, and `history` auto
|
|
|
465
545
|
|
|
466
546
|
## Architecture
|
|
467
547
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
548
|
+
```mermaid
|
|
549
|
+
flowchart TB
|
|
550
|
+
subgraph adapters["Adapters — infrastructure implementations"]
|
|
551
|
+
git["GitGraphAdapter"]
|
|
552
|
+
cbor["CborCodec"]
|
|
553
|
+
webcrypto["WebCryptoAdapter"]
|
|
554
|
+
clocka["ClockAdapter"]
|
|
555
|
+
consolel["ConsoleLogger"]
|
|
556
|
+
cascache["CasSeekCacheAdapter"]
|
|
557
|
+
|
|
558
|
+
subgraph ports["Ports — abstract interfaces"]
|
|
559
|
+
pp["GraphPersistencePort"]
|
|
560
|
+
cp["CodecPort"]
|
|
561
|
+
crp["CryptoPort"]
|
|
562
|
+
clp["ClockPort"]
|
|
563
|
+
lp["LoggerPort"]
|
|
564
|
+
ip["IndexStoragePort"]
|
|
565
|
+
sp["SeekCachePort"]
|
|
566
|
+
np["NeighborProviderPort"]
|
|
567
|
+
|
|
568
|
+
subgraph domain["Domain Core"]
|
|
569
|
+
wg["WarpGraph — main API facade"]
|
|
570
|
+
jr["JoinReducer"]
|
|
571
|
+
pb["PatchBuilderV2"]
|
|
572
|
+
cs["CheckpointService"]
|
|
573
|
+
qb["QueryBuilder"]
|
|
574
|
+
lt["LogicalTraversal"]
|
|
575
|
+
gt["GraphTraversal"]
|
|
576
|
+
mvs["MaterializedViewService"]
|
|
577
|
+
crdts["CRDTs: VersionVector · ORSet · LWW"]
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
pp -.->|implements| git
|
|
583
|
+
cp -.->|implements| cbor
|
|
584
|
+
crp -.->|implements| webcrypto
|
|
585
|
+
clp -.->|implements| clocka
|
|
586
|
+
lp -.->|implements| consolel
|
|
587
|
+
sp -.->|implements| cascache
|
|
588
|
+
ip -.->|via| git
|
|
589
|
+
|
|
590
|
+
wg --> pp & cp & crp & clp & lp & ip & sp
|
|
591
|
+
```
|
|
471
592
|
|
|
472
593
|
The codebase follows hexagonal architecture with ports and adapters:
|
|
473
594
|
|
|
@@ -480,6 +601,7 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
480
601
|
- `LoggerPort` -- structured logging
|
|
481
602
|
- `ClockPort` -- time measurement
|
|
482
603
|
- `SeekCachePort` -- persistent seek materialization cache
|
|
604
|
+
- `NeighborProviderPort` -- abstract neighbor lookup interface
|
|
483
605
|
|
|
484
606
|
**Adapters** implement the ports:
|
|
485
607
|
- `GitGraphAdapter` -- wraps `@git-stunts/plumbing` for Git operations
|
|
@@ -496,7 +618,12 @@ The codebase follows hexagonal architecture with ports and adapters:
|
|
|
496
618
|
- `Writer` / `PatchSession` -- patch creation and commit
|
|
497
619
|
- `JoinReducer` -- CRDT-based state materialization
|
|
498
620
|
- `QueryBuilder` -- fluent query construction
|
|
499
|
-
- `LogicalTraversal` --
|
|
621
|
+
- `LogicalTraversal` -- deprecated facade, delegates to GraphTraversal
|
|
622
|
+
- `GraphTraversal` -- unified traversal engine (11 algorithms, `nodeWeightFn`)
|
|
623
|
+
- `MaterializedViewService` -- orchestrate build/persist/load of materialized views
|
|
624
|
+
- `IncrementalIndexUpdater` -- O(diff) bitmap index updates
|
|
625
|
+
- `LogicalIndexBuildService` / `LogicalIndexReader` -- logical bitmap indexes
|
|
626
|
+
- `AdjacencyNeighborProvider` / `BitmapNeighborProvider` -- neighbor provider implementations
|
|
500
627
|
- `SyncProtocol` -- multi-writer synchronization
|
|
501
628
|
- `CheckpointService` -- state snapshot creation and loading
|
|
502
629
|
- `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
|
@@ -226,7 +226,7 @@ export interface HopOptions {
|
|
|
226
226
|
* Fluent query builder.
|
|
227
227
|
*/
|
|
228
228
|
export class QueryBuilder {
|
|
229
|
-
match(pattern: string): QueryBuilder;
|
|
229
|
+
match(pattern: string | string[]): QueryBuilder;
|
|
230
230
|
where(fn: ((node: QueryNodeSnapshot) => boolean) | Record<string, unknown>): QueryBuilder;
|
|
231
231
|
outgoing(label?: string, options?: HopOptions): QueryBuilder;
|
|
232
232
|
incoming(label?: string, options?: HopOptions): QueryBuilder;
|
|
@@ -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. */
|
|
@@ -1157,8 +1194,8 @@ export const CONTENT_PROPERTY_KEY: '_content';
|
|
|
1157
1194
|
* Configuration for an observer view.
|
|
1158
1195
|
*/
|
|
1159
1196
|
export interface ObserverConfig {
|
|
1160
|
-
/** Glob pattern for visible nodes (e.g. 'user:*') */
|
|
1161
|
-
match: string;
|
|
1197
|
+
/** Glob pattern or array of patterns for visible nodes (e.g. 'user:*' or ['user:*', 'team:*']) */
|
|
1198
|
+
match: string | string[];
|
|
1162
1199
|
/** Property keys to include (whitelist). If omitted, all non-redacted properties are visible. */
|
|
1163
1200
|
expose?: string[];
|
|
1164
1201
|
/** Property keys to exclude (blacklist). Takes precedence over expose. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@git-stunts/git-warp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.1.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",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"setup:hooks": "node scripts/setup-hooks.js",
|
|
80
80
|
"prepare": "patch-package && node scripts/setup-hooks.js",
|
|
81
81
|
"prepack": "npm run lint && npm run test:local && npm run typecheck:consumer",
|
|
82
|
+
"release:preflight": "bash scripts/release-preflight.sh",
|
|
82
83
|
"install:git-warp": "bash scripts/install-git-warp.sh",
|
|
83
84
|
"uninstall:git-warp": "bash scripts/uninstall-git-warp.sh",
|
|
84
85
|
"test:node20": "docker compose -f docker-compose.test.yml --profile node20 run --build --rm test-node20",
|
|
@@ -107,7 +108,7 @@
|
|
|
107
108
|
"roaring": "^2.7.0",
|
|
108
109
|
"string-width": "^7.1.0",
|
|
109
110
|
"wrap-ansi": "^9.0.0",
|
|
110
|
-
"zod": "
|
|
111
|
+
"zod": "3.24.1"
|
|
111
112
|
},
|
|
112
113
|
"devDependencies": {
|
|
113
114
|
"@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';
|