@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.
Files changed (49) hide show
  1. package/README.md +137 -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 +52 -15
  9. package/package.json +3 -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 +132 -69
  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/QueryBuilder.js +15 -44
  30. package/src/domain/services/TemporalQuery.js +128 -14
  31. package/src/domain/services/TranslationCost.js +8 -24
  32. package/src/domain/types/PatchDiff.js +90 -0
  33. package/src/domain/types/WarpTypesV2.js +4 -4
  34. package/src/domain/utils/MinHeap.js +45 -17
  35. package/src/domain/utils/canonicalCbor.js +36 -0
  36. package/src/domain/utils/fnv1a.js +20 -0
  37. package/src/domain/utils/matchGlob.js +51 -0
  38. package/src/domain/utils/roaring.js +14 -3
  39. package/src/domain/utils/shardKey.js +40 -0
  40. package/src/domain/utils/toBytes.js +17 -0
  41. package/src/domain/warp/_wiredMethods.d.ts +7 -1
  42. package/src/domain/warp/checkpoint.methods.js +21 -5
  43. package/src/domain/warp/materialize.methods.js +17 -5
  44. package/src/domain/warp/materializeAdvanced.methods.js +142 -3
  45. package/src/domain/warp/query.methods.js +83 -15
  46. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
  47. package/src/ports/BlobPort.js +1 -1
  48. package/src/ports/NeighborProviderPort.js +59 -0
  49. 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
- <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>
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
- <p align="center">
81
- <img src="docs/diagrams/fig-multi-writer.svg" alt="Multi-writer convergence — independent chains, deterministic merge" width="700">
82
- </p>
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
- <p align="center">
469
- <img src="docs/diagrams/fig-architecture.svg" alt="Hexagonal architecture — dependency rule: arrows point inward only" width="700">
470
- </p>
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` -- graph traversal over materialized state
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
-
@@ -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
- 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. */
@@ -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": "11.5.1",
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": "^3.24.1"
111
+ "zod": "3.24.1"
111
112
  },
112
113
  "devDependencies": {
113
114
  "@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';