Skip to content

Commit c7b1900

Browse files
aksOpsclaude
andcommitted
feat: multi-backend graph storage — KuzuDB, SQLite, NetworkX adapters
Architecture: - GraphBackend protocol in backend.py with 16 methods - CypherBackend protocol for Cypher-capable backends - NetworkXBackend extracted from old GraphStore (zero behavioral change) - KuzuBackend with full Cypher support, file-based, bundleable - SqliteGraphBackend with recursive CTEs, zero-dependency, bundleable - GraphStore refactored as thin facade delegating to backend - Factory function create_backend() dispatches by name - GraphConfig added to config.py (backend, path) - --backend flag on analyze command - New bundle command: analyze + package graph DB into zip - Fixed 3 NetworkX leaks (query.py, views.py, layer_classifier.py) - pyproject.toml: optional kuzu dependency group All 361 tests pass. All 3 backends satisfy GraphBackend protocol. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c2ae257 commit c7b1900

7 files changed

Lines changed: 1001 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ dev = [
2727
"pytest>=8.0",
2828
"pytest-cov>=5.0",
2929
]
30+
kuzu = ["kuzu>=0.6"]
31+
all-backends = ["kuzu>=0.6"]
3032

3133
[project.scripts]
3234
code-intelligence = "code_intelligence.cli:app"

src/code_intelligence/analyzer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ def _report(msg: str) -> None:
275275
files_to_analyze: list[DiscoveredFile] = all_files
276276
files_cached = 0
277277

278-
builder = GraphBuilder()
278+
from code_intelligence.graph.backends import create_backend
279+
backend = create_backend(self._config.graph.backend, path=self._config.graph.path)
280+
builder = GraphBuilder(backend=backend)
279281

280282
if cache_cfg.enabled:
281283
cache_path = repo_path / cache_cfg.directory / cache_cfg.db_name

src/code_intelligence/cli.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def analyze(
2727
path: Annotated[Path, typer.Argument(help="Path to the codebase to analyze")] = Path("."),
2828
incremental: Annotated[bool, typer.Option("--incremental/--full", help="Use incremental analysis")] = True,
2929
parallelism: Annotated[int, typer.Option("--parallelism", "-j", help="Number of parallel workers")] = 8,
30+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend (networkx, kuzu, sqlite)")] = "networkx",
3031
config: Annotated[Optional[Path], typer.Option("--config", "-c", help="Path to config file")] = None,
3132
) -> None:
3233
"""Analyze a codebase and build the code intelligence graph."""
@@ -35,6 +36,13 @@ def analyze(
3536
cfg = _load_config(config)
3637
cfg.analysis.parallelism = parallelism
3738
cfg.analysis.incremental = incremental
39+
cfg.graph.backend = backend
40+
if backend in ("kuzu", "sqlite"):
41+
graph_dir = path.resolve() / ".code-intelligence"
42+
if backend == "kuzu":
43+
cfg.graph.path = str(graph_dir / "graph.kuzu")
44+
elif backend == "sqlite":
45+
cfg.graph.path = str(graph_dir / "graph.db")
3846

3947
console.print("🚀 Starting analysis…")
4048
analyzer = Analyzer(cfg)
@@ -312,5 +320,80 @@ def plugins(
312320
console.print("⚠️ Use 'list' or 'info <name>'.")
313321

314322

323+
@app.command()
324+
def bundle(
325+
path: Annotated[Path, typer.Argument(help="Path to analyzed codebase")] = Path("."),
326+
tag: Annotated[str, typer.Option("--tag", "-t", help="Version tag")] = "latest",
327+
backend: Annotated[str, typer.Option("--backend", "-b", help="Graph backend")] = "kuzu",
328+
output: Annotated[Path | None, typer.Option("--output", "-o", help="Output zip path")] = None,
329+
config: Annotated[Optional[Path], typer.Option("--config", "-c")] = None,
330+
) -> None:
331+
"""Analyze and package graph into a distributable bundle."""
332+
import json
333+
import zipfile
334+
from datetime import datetime, timezone
335+
336+
cfg = _load_config(config)
337+
cfg.graph.backend = backend
338+
339+
# Set default path for file-based backends
340+
graph_dir = path.resolve() / ".code-intelligence"
341+
if backend == "kuzu":
342+
cfg.graph.path = str(graph_dir / "graph.kuzu")
343+
elif backend == "sqlite":
344+
cfg.graph.path = str(graph_dir / "graph.db")
345+
346+
# Run analysis
347+
from code_intelligence.analyzer import Analyzer
348+
analyzer = Analyzer(cfg)
349+
result = analyzer.run(path.resolve(), incremental=False)
350+
351+
# Determine output path
352+
project_name = path.resolve().name
353+
if output is None:
354+
output = Path(f"{project_name}-{tag}-codegraph.zip")
355+
356+
# Create bundle
357+
manifest = {
358+
"tag": tag,
359+
"backend": backend,
360+
"project": project_name,
361+
"created_at": datetime.now(timezone.utc).isoformat(),
362+
"node_count": result.graph.node_count,
363+
"edge_count": result.graph.edge_count,
364+
"files_analyzed": result.total_files,
365+
"code_intelligence_version": "0.1.0",
366+
}
367+
368+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zf:
369+
# Write manifest
370+
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
371+
372+
# Bundle the graph database files
373+
if backend == "kuzu" and cfg.graph.path:
374+
graph_path = Path(cfg.graph.path)
375+
if graph_path.exists():
376+
for f in graph_path.rglob("*"):
377+
if f.is_file():
378+
zf.write(f, f"graph/{f.relative_to(graph_path)}")
379+
elif backend == "sqlite" and cfg.graph.path:
380+
graph_path = Path(cfg.graph.path)
381+
if graph_path.exists():
382+
zf.write(graph_path, "graph/graph.db")
383+
else:
384+
# NetworkX -- serialize to JSON
385+
model = result.graph.to_model()
386+
from code_intelligence.output.serializers import JsonSerializer
387+
zf.writestr("graph/graph.json", JsonSerializer().serialize(model))
388+
389+
result.graph.close()
390+
391+
console.print(f"Bundle created: [bold]{output}[/bold]")
392+
console.print(f" Tag: {tag}")
393+
console.print(f" Backend: {backend}")
394+
console.print(f" Nodes: {manifest['node_count']}, Edges: {manifest['edge_count']}")
395+
console.print(f" Size: {output.stat().st_size / 1024 / 1024:.1f} MB")
396+
397+
315398
if __name__ == "__main__":
316399
app()

src/code_intelligence/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,21 @@ class DomainMapping(BaseModel):
7171
modules: list[str]
7272

7373

74+
class GraphConfig(BaseModel):
75+
"""Graph storage backend configuration."""
76+
77+
backend: str = "networkx" # networkx | kuzu | sqlite
78+
path: str | None = None # For file-based backends (kuzu, sqlite)
79+
80+
7481
class Config(BaseModel):
7582
"""Root configuration for code-intelligence."""
7683

7784
discovery: DiscoveryConfig = Field(default_factory=DiscoveryConfig)
7885
cache: CacheConfig = Field(default_factory=CacheConfig)
7986
analysis: AnalysisConfig = Field(default_factory=AnalysisConfig)
8087
output: OutputConfig = Field(default_factory=OutputConfig)
88+
graph: GraphConfig = Field(default_factory=GraphConfig)
8189
domains: list[DomainMapping] = Field(default_factory=list)
8290

8391
@classmethod

0 commit comments

Comments
 (0)