@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.
Files changed (46) hide show
  1. package/README.md +142 -10
  2. package/bin/cli/commands/registry.js +4 -0
  3. package/bin/cli/commands/reindex.js +41 -0
  4. package/bin/cli/commands/verify-index.js +59 -0
  5. package/bin/cli/infrastructure.js +7 -2
  6. package/bin/cli/schemas.js +19 -0
  7. package/bin/cli/types.js +2 -0
  8. package/index.d.ts +49 -12
  9. package/package.json +2 -2
  10. package/src/domain/WarpGraph.js +40 -0
  11. package/src/domain/errors/ShardIdOverflowError.js +28 -0
  12. package/src/domain/errors/index.js +1 -0
  13. package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
  14. package/src/domain/services/BitmapNeighborProvider.js +178 -0
  15. package/src/domain/services/CheckpointMessageCodec.js +3 -3
  16. package/src/domain/services/CheckpointService.js +77 -12
  17. package/src/domain/services/GraphTraversal.js +1239 -0
  18. package/src/domain/services/IncrementalIndexUpdater.js +765 -0
  19. package/src/domain/services/JoinReducer.js +233 -5
  20. package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
  21. package/src/domain/services/LogicalIndexBuildService.js +108 -0
  22. package/src/domain/services/LogicalIndexReader.js +315 -0
  23. package/src/domain/services/LogicalTraversal.js +321 -202
  24. package/src/domain/services/MaterializedViewService.js +379 -0
  25. package/src/domain/services/ObserverView.js +138 -47
  26. package/src/domain/services/PatchBuilderV2.js +3 -3
  27. package/src/domain/services/PropertyIndexBuilder.js +64 -0
  28. package/src/domain/services/PropertyIndexReader.js +111 -0
  29. package/src/domain/services/TemporalQuery.js +128 -14
  30. package/src/domain/types/PatchDiff.js +90 -0
  31. package/src/domain/types/WarpTypesV2.js +4 -4
  32. package/src/domain/utils/MinHeap.js +45 -17
  33. package/src/domain/utils/canonicalCbor.js +36 -0
  34. package/src/domain/utils/fnv1a.js +20 -0
  35. package/src/domain/utils/roaring.js +14 -3
  36. package/src/domain/utils/shardKey.js +40 -0
  37. package/src/domain/utils/toBytes.js +17 -0
  38. package/src/domain/warp/_wiredMethods.d.ts +7 -1
  39. package/src/domain/warp/checkpoint.methods.js +21 -5
  40. package/src/domain/warp/materialize.methods.js +17 -5
  41. package/src/domain/warp/materializeAdvanced.methods.js +142 -3
  42. package/src/domain/warp/query.methods.js +78 -12
  43. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
  44. package/src/ports/BlobPort.js +1 -1
  45. package/src/ports/NeighborProviderPort.js +59 -0
  46. 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
- <p align="center">
59
- <img src="docs/diagrams/fig-data-storage.svg" alt="WARP data storage — invisible to normal Git workflows" width="700">
60
- </p>
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
- <p align="center">
81
- <img src="docs/diagrams/fig-multi-writer.svg" alt="Multi-writer convergence — independent chains, deterministic merge" width="700">
82
- </p>
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
- <p align="center">
469
- <img src="docs/diagrams/fig-architecture.svg" alt="Hexagonal architecture — dependency rule: arrows point inward only" width="700">
470
- </p>
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` -- graph traversal over materialized state
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
-
@@ -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
- dfs(start: string, options?: {
248
- maxDepth?: number;
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
- }): Promise<string[]>;
252
- shortestPath(from: string, to: string, options?: {
253
- maxDepth?: number;
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
- }): Promise<{ found: boolean; path: string[]; length: number }>;
257
- connectedComponent(start: string, options?: {
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
- }): Promise<string[]>;
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": "11.5.1",
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": "^3.24.1"
110
+ "zod": "3.24.1"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@eslint/js": "^9.17.0",
@@ -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';