---
id: 001-code-graph-structural-analysis-for
level: adr
title: "Code Graph: Structural Analysis for Context Navigation"
number: 1
short_code: "PROJEC-A-0002"
created_at: 2026-01-07T20:19:39.975601+00:00
updated_at: 2026-01-10T17:13:13.785969+00:00
decision_date: 
decision_maker: 
parent: 
archived: false

tags:
  - "#adr"
  - "#phase/decided"


exit_criteria_met: false
strategy_id: NULL
initiative_id: NULL
---

# ADR-2: Code Graph - Structural Analysis for Context Navigation

## Context

Muninn's RLM-style context selection needs to navigate codebases intelligently. Text-based search (grep) finds string matches but misses structural relationships:

- "What calls this function?" requires call graph traversal
- "What would break if I change this interface?" requires reverse dependency analysis
- "Show me the auth flow" requires understanding inheritance and composition
- "What modules does this depend on?" requires import graph

These are **structural queries** that text search cannot answer efficiently. A code graph provides the structural map that complements text search and semantic embeddings.

### Relationship to Other Memory Layers

| Component | What it answers | Query type |
|-----------|-----------------|------------|
| **Code Graph** | "What IS the structure?" | Structural/relational |
| **Embeddings** | "What is similar to X?" | Semantic similarity |
| **Session Memory** | "What mattered before?" | Historical relevance |
| **Curated Knowledge** | "What did we decide?" | Team conventions |

The code graph is **derived data** (Layer 1) - 100% rebuildable from source code.

## Decision

### Graph Schema

The code graph models code as nodes and edges. The schema is designed to cover constructs across all major languages precisely.

**Node Types:**
```
┌─────────────┬────────────────────────────────────────────────────────────────┐
│ Node Type   │ Represents                                     │ Languages     │
├─────────────┼──────────────────────────────────────────────────────────────────┤
│ File        │ Source file (.py, .ts, .rs, etc.)              │ All           │
│ Module      │ Logical module/package/namespace               │ All           │
│ Class       │ Class definition (OOP classes)                 │ Py, TS, Java, C++ │
│ Struct      │ Struct/record (data-focused, no inheritance)   │ Rust, Go, C   │
│ Interface   │ Interface/trait/protocol (contracts)           │ Rust, Go, TS, Java │
│ Enum        │ Enum type (may have variants/methods)          │ Rust, TS, Java, Py │
│ Function    │ Function/method definition                     │ All           │
│ Variable    │ Module/class-level variable or constant        │ All           │
│ Type        │ Type alias (type X = Y)                        │ TS, Rust, Go  │
│ Macro       │ Macro definition (compile-time codegen)        │ Rust, C/C++   │
└─────────────┴────────────────────────────────────────────────────────────────┘
```

**Edge Types:**
```
┌─────────────────┬────────────────────────────────────────────────────────────┐
│ Edge Type       │ Meaning                                                    │
├─────────────────┼────────────────────────────────────────────────────────────┤
│ CONTAINS        │ File contains Class, Class contains Method                 │
│ IMPORTS         │ File/Module imports another                                │
│ CALLS           │ Function calls another function (static dispatch)          │
│ CALLS_DYNAMIC   │ Dynamic/runtime dispatch (getattr, reflection, vtable)     │
│ CALLS_FFI       │ Cross-language call (Python→Rust, Rust→C, etc.)            │
│ CALLS_API       │ HTTP/RPC call to external service                          │
│ INHERITS        │ Class extends another class                                │
│ IMPLEMENTS      │ Class/Struct implements Interface/Trait                    │
│ USES_TYPE       │ Function/variable uses a type                              │
│ INSTANTIATES    │ Code creates instance of class/struct                      │
│ REFERENCES      │ General reference (read/write variable)                    │
│ EXPANDS_TO      │ Macro expands to generated code                            │
│ GENERATED_BY    │ Code was generated by macro (inverse of EXPANDS_TO)        │
└─────────────────┴────────────────────────────────────────────────────────────┘
```

**Node Properties:**
```python
{
    "id": "src/auth/login.py::LoginHandler",
    "type": "class",
    "name": "LoginHandler",
    "file": "src/auth/login.py",
    "line_start": 45,
    "line_end": 120,
    "docstring": "Handles user login flow...",
    "visibility": "public",       # public/private/protected
    "language": "python",
    "generated": false,           # true if macro-generated
    "generator": null             # macro that generated this, if any
}
```

**Edge Properties:**
```python
{
    "from": "src/api/routes.py::handle_login",
    "to": "src/auth/login.py::LoginHandler",
    "type": "INSTANTIATES",
    "line": 78,                   # where the relationship occurs
    "context": "handler = LoginHandler(request)",  # code snippet
    "confidence": 1.0,            # 1.0 = static, <1.0 = dynamic/inferred
    "cross_language": false,      # true for FFI edges
    "target_language": null       # for FFI: target language
}
```

### Special Edge Handling

**Dynamic Calls** (`CALLS_DYNAMIC`):
```python
# Python: getattr, __getattr__, reflection
handler = getattr(module, handler_name)()
# Edge: CALLS_DYNAMIC with confidence=0.5, pattern="getattr"

# JavaScript: bracket notation, eval
obj[methodName]()
# Edge: CALLS_DYNAMIC with confidence=0.3
```

Dynamic edges have lower confidence scores. The LLM context engine can use confidence to weight relevance.

**FFI Calls** (`CALLS_FFI`):
```python
# Python calling Rust via PyO3
from my_rust_lib import fast_hash  # → CALLS_FFI to Rust symbol
result = fast_hash(data)

# Rust calling C
extern "C" { fn malloc(size: usize) -> *mut u8; }  # → CALLS_FFI to C symbol
```

FFI edges track `target_language` so impact analysis can cross language boundaries.

**Dynamic/Plugin FFI** (`CALLS_FFI` with `dynamic_load=true`):

Plugin systems use dlopen/dlsym for runtime loading. The target isn't known at compile time.

```rust
// Plugin definition (macro-generated FFI)
#[plugin_module]
#[plugin_function]
async fn my_handler(ctx: Context) -> Result<Output> { ... }

// Generates:
// - extern "C" fn get_plugin_metadata() -> *const PluginMetadata
// - extern "C" fn execute_plugin_function(...) -> i32
```

```rust
// Plugin loader (dynamic FFI)
let lib = libloading::Library::new("plugin.so")?;
let metadata: Symbol<fn() -> *const PluginMetadata> = lib.get(b"get_plugin_metadata")?;
let execute: Symbol<fn(...) -> i32> = lib.get(b"execute_plugin_function")?;
```

**Graph representation:**
```
┌────────────────────┐
│ #[plugin_function] │──── EXPANDS_TO ────►┌──────────────────────────┐
│ (Macro)            │                     │ extern "C"               │
└────────────────────┘                     │ execute_plugin_function  │
        │                                  │ (Function, generated)    │
        │ EXPANDS_TO                       └──────────────────────────┘
        ▼                                              ▲
┌────────────────────┐                                 │
│ my_handler         │                                 │ CALLS_FFI
│ (Function, async)  │                                 │ dynamic_load=true
└────────────────────┘                                 │
                                           ┌───────────┴──────────────┐
                                           │ plugin_loader.rs         │
                                           │ lib.get("execute_...")   │
                                           └──────────────────────────┘
```

**Edge properties for dynamic FFI:**
```python
{
    "type": "CALLS_FFI",
    "dynamic_load": true,           # loaded via dlopen/libloading, not linked
    "plugin_system": "colliery",    # or "libloading", "abi_stable", "dlopen"
    "symbol_name": "execute_plugin_function",  # runtime symbol lookup
    "confidence": 0.8               # lower confidence - runtime resolution
}
```

**Detection heuristics:**
- `libloading::Library::new()` / `dlopen()` → marks call site as dynamic loader
- `.get(b"symbol_name")` / `dlsym()` → extracts symbol name for edge
- `#[plugin_module]`, `#[plugin_function]` → macro-generated FFI exports
- `extern "C"` + `#[no_mangle]` → FFI export target

**API Calls** (`CALLS_API`):
```python
# HTTP call to another service
response = httpx.post("https://auth-service/validate", ...)
# Edge: CALLS_API with target="auth-service:/validate"
```

API edges are heuristic (detected via URL patterns, client library usage). Lower confidence but valuable for system understanding.

### Generated Code and Macros

Macros (especially Rust proc macros) generate significant code. The graph tracks this explicitly:

```rust
#[derive(Debug, Clone, Serialize)]  // Macro invocation
struct User {                        // Source struct
    name: String,
}
// Generates: impl Debug for User, impl Clone for User, impl Serialize for User
```

**Graph representation:**
```
┌──────────────────┐
│  derive(Debug)   │──── EXPANDS_TO ────►┌─────────────────────┐
│  (Macro)         │                     │ impl Debug for User │
└──────────────────┘                     │ (Function, generated)│
                                         └─────────────────────┘
                                                    │
                                                    │ GENERATED_BY
                                                    ▼
                                         ┌──────────────────┐
                                         │  derive(Debug)   │
                                         └──────────────────┘
```

**Storage approach:**
1. Index macro invocation sites
2. Optionally expand macros and index generated code (cargo expand, etc.)
3. Link generated code back to source macro via GENERATED_BY
4. Mark generated nodes with `generated: true`

**Trade-off:** Full macro expansion is expensive. Options:
- **Lazy expansion**: Only expand on-demand when querying
- **Common macros only**: Expand derive, serde, etc.; skip custom
- **Configurable**: Let user specify which macros to expand

### Example Graph Structure

```
                    ┌──────────────────┐
                    │   routes.py      │
                    │   (File)         │
                    └────────┬─────────┘
                             │ CONTAINS
                             ▼
                    ┌──────────────────┐
                    │  handle_login    │───── CALLS ────►┌──────────────┐
                    │  (Function)      │                 │ validate_jwt │
                    └────────┬─────────┘                 └──────────────┘
                             │ INSTANTIATES
                             ▼
┌──────────────┐    ┌──────────────────┐
│  BaseHandler │◄───│  LoginHandler    │───── USES_TYPE ─►┌──────────────┐
│  (Class)     │    │  (Class)         │                  │ UserSession  │
└──────────────┘    └──────────────────┘                  │ (Type)       │
    INHERITS                                              └──────────────┘
```

### Query Patterns

The graph enables structural queries that RLM exploration can use:

**1. Impact Analysis**
```
Query: "What would break if I change LoginHandler?"
Graph: Find all nodes with edges pointing TO LoginHandler
Result: routes.py::handle_login, tests/test_auth.py::TestLogin
```

**2. Dependency Understanding**
```
Query: "What does the auth module depend on?"
Graph: Traverse IMPORTS/CALLS/USES_TYPE edges FROM auth/*
Result: jwt_utils, database.users, config.settings
```

**3. Call Chain Tracing**
```
Query: "How does a request reach the database?"
Graph: Find paths from routes.* to database.* via CALLS edges
Result: routes → handlers → services → repositories → database
```

**4. Inheritance Hierarchy**
```
Query: "What classes extend BaseHandler?"
Graph: Find all INHERITS edges TO BaseHandler
Result: LoginHandler, LogoutHandler, ProfileHandler
```

**5. Symbol Location**
```
Query: "Where is UserSession defined?"
Graph: Find node with name=UserSession, type=Class|Type
Result: src/models/session.py:23-45
```

### RLM Tool Integration

The code graph exposes tools for RLM exploration:

```python
# Available in RLM REPL environment
graph.find_symbol("LoginHandler")           # → node details + location
graph.callers_of("validate_jwt")            # → functions that call this
graph.callees_of("handle_login")            # → functions this calls
graph.dependencies_of("src/auth/")          # → what auth module imports
graph.dependents_of("src/auth/")            # → what imports auth module
graph.inheritance_tree("BaseHandler")       # → class hierarchy
graph.path_between("routes", "database")    # → call chain
```

These complement existing tools:
- `repo.grep(pattern)` - text search
- `repo.read_file(path)` - content retrieval
- `graph.*` - structural navigation

### Storage: graphqlite

Use **graphqlite** (SQLite-based graph storage) for:
- Consistency with sqlite-vec for embeddings
- Single-file database, easy to gitignore or LFS
- **Cypher query language** for graph traversal
- No external service dependencies

**Schema (simplified):**
```sql
CREATE TABLE nodes (
    id TEXT PRIMARY KEY,
    type TEXT NOT NULL,
    name TEXT NOT NULL,
    file TEXT,
    line_start INTEGER,
    line_end INTEGER,
    language TEXT,
    generated BOOLEAN DEFAULT FALSE,
    properties JSON
);

CREATE TABLE edges (
    from_id TEXT REFERENCES nodes(id),
    to_id TEXT REFERENCES nodes(id),
    type TEXT NOT NULL,
    confidence REAL DEFAULT 1.0,
    properties JSON,
    PRIMARY KEY (from_id, to_id, type)
);

CREATE INDEX idx_nodes_type ON nodes(type);
CREATE INDEX idx_nodes_file ON nodes(file);
CREATE INDEX idx_nodes_language ON nodes(language);
CREATE INDEX idx_edges_to ON edges(to_id);
CREATE INDEX idx_edges_type ON edges(type);
CREATE INDEX idx_edges_confidence ON edges(confidence);
```

### Query Language: Cypher

graphqlite supports Cypher, the standard graph query language. Examples:

**Find all callers of a function:**
```cypher
MATCH (caller:Function)-[:CALLS]->(target:Function {name: 'validate_jwt'})
RETURN caller.file, caller.name, caller.line_start
```

**Find FFI boundaries:**
```cypher
MATCH (src)-[:CALLS_FFI]->(target)
RETURN src.language, src.name, target.language, target.name
```

**Trace call chain from routes to database:**
```cypher
MATCH path = (start:Function)-[:CALLS*1..5]->(end:Function)
WHERE start.file STARTS WITH 'src/routes'
  AND end.file STARTS WITH 'src/database'
RETURN path
```

**Find all code generated by a macro:**
```cypher
MATCH (macro:Macro {name: 'derive'})-[:EXPANDS_TO]->(generated)
RETURN generated.name, generated.type, generated.file
```

**Impact analysis with confidence weighting:**
```cypher
MATCH (target:Class {name: 'LoginHandler'})<-[r:CALLS|INSTANTIATES|USES_TYPE]-(dependent)
WHERE r.confidence > 0.5
RETURN dependent.name, r.type, r.confidence
ORDER BY r.confidence DESC
```

### RLM Tool Integration (Updated)

The code graph exposes both high-level tools and raw Cypher for RLM exploration:

```python
# High-level API (convenience wrappers)
graph.find_symbol("LoginHandler")           # → node details + location
graph.callers_of("validate_jwt")            # → functions that call this
graph.callees_of("handle_login")            # → functions this calls
graph.dependencies_of("src/auth/")          # → what auth module imports
graph.dependents_of("src/auth/")            # → what imports auth module
graph.inheritance_tree("BaseHandler")       # → class hierarchy
graph.path_between("routes", "database")    # → call chain
graph.ffi_boundaries()                      # → all cross-language edges
graph.generated_by("derive")                # → all macro-generated code

# Low-level: raw Cypher for complex queries
graph.query("""
    MATCH (f:Function)-[:CALLS_DYNAMIC]->(target)
    WHERE f.file STARTS WITH 'src/api'
    RETURN f.name, target.name, f.confidence
""")
```

Exposing Cypher directly lets the LLM construct complex queries when high-level APIs aren't sufficient.

### Parsing Architecture

Each language requires a **lexer/parser** that maps language-specific constructs to our common data model (nodes + edges). Tree-sitter provides the parsing foundation; we build language-specific extractors on top.

**Architecture:**
```
┌─────────────────────────────────────────────────────────────────────────┐
│                        Language Parsers                                  │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐                    │
│  │ Rust Parser │   │ Python      │   │ C/C++       │   ...              │
│  │             │   │ Parser      │   │ Parser      │                    │
│  │ - tree-     │   │ - tree-     │   │ - tree-     │                    │
│  │   sitter-   │   │   sitter-   │   │   sitter-   │                    │
│  │   rust      │   │   python    │   │   c/cpp     │                    │
│  │ - extractor │   │ - extractor │   │ - extractor │                    │
│  └──────┬──────┘   └──────┬──────┘   └──────┬──────┘                    │
│         │                 │                 │                            │
│         └────────────────┬┴─────────────────┘                            │
│                          ▼                                               │
│              ┌───────────────────────┐                                   │
│              │  Common Data Model    │                                   │
│              │  (Nodes + Edges)      │                                   │
│              └───────────┬───────────┘                                   │
│                          ▼                                               │
│              ┌───────────────────────┐                                   │
│              │  graphqlite           │                                   │
│              │  (graph.db)           │                                   │
│              └───────────────────────┘                                   │
└─────────────────────────────────────────────────────────────────────────┘
```

**Language Priority:**
| Phase | Language | Rationale |
|-------|----------|-----------|
| **1** | Rust | Macro system, FFI to C, complex type system, primary target |
| **2** | Python | Dynamic calls, FFI (PyO3, ctypes), wide usage |
| **3** | C/C++ | FFI target for Rust/Python, macros, templates |
| **4** | TypeScript/JavaScript, Go, Java | Community demand |

**Per-Language Extractor Responsibilities:**

Each extractor must:
1. Parse source files using tree-sitter grammar
2. Extract nodes (map language constructs → our node types)
3. Extract edges (map language relationships → our edge types)
4. Handle language-specific features:

| Language | Special Handling Required |
|----------|--------------------------|
| **Rust** | Macros (derive, proc_macro), traits → Interface, impl blocks, FFI (extern "C"), generics |
| **Python** | Dynamic calls (getattr), decorators, type hints, FFI (ctypes, PyO3, cffi) |
| **C/C++** | Preprocessor macros (#define), templates, header/impl split, extern "C" |

**Extractor Interface:**
```python
class LanguageExtractor(Protocol):
    """Interface all language extractors must implement."""
    
    def parse_file(self, path: Path) -> ParseResult:
        """Parse a single file, return AST."""
        ...
    
    def extract_nodes(self, ast: ParseResult) -> list[Node]:
        """Extract all nodes from AST."""
        ...
    
    def extract_edges(self, ast: ParseResult, nodes: list[Node]) -> list[Edge]:
        """Extract all edges from AST."""
        ...
    
    def expand_macros(self, path: Path) -> Optional[str]:
        """Optionally expand macros and return generated source."""
        ...
```

### Implementation: Rust Core with Python Bindings

The parsing and graph layer is implemented in **Rust** for performance, exposed to Python via **PyO3** for integration with the Muninn gateway.

**Why Rust for the core:**
- graphqlite has native Rust bindings
- tree-sitter has native Rust bindings
- Parsing large codebases is CPU-intensive
- Rust extractors can handle Rust code natively (dogfooding)
- Single binary for the indexing/query layer

**Architecture:**
```
┌─────────────────────────────────────────────────────────────────────────┐
│  Muninn Gateway (Python)                                                │
│  - FastAPI server                                                       │
│  - RLM context engine                                                   │
│  - Memory management                                                    │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │ PyO3 bindings
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  muninn-graph (Rust crate)                                              │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  Language Extractors                                             │    │
│  │  - RustExtractor (tree-sitter-rust)                              │    │
│  │  - PythonExtractor (tree-sitter-python)                          │    │
│  │  - CppExtractor (tree-sitter-c, tree-sitter-cpp)                 │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  Graph Storage (graphqlite Rust bindings)                        │    │
│  │  - Cypher queries                                                │    │
│  │  - Node/Edge CRUD                                                │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │  Python API (PyO3)                                               │    │
│  │  - graph.index(path)                                             │    │
│  │  - graph.query(cypher)                                           │    │
│  │  - graph.find_symbol(name)                                       │    │
│  │  - graph.callers_of(symbol)                                      │    │
│  │  - etc.                                                          │    │
│  └─────────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────────┘
```

**Crate structure:**
```
muninn-graph/
├── Cargo.toml
├── src/
│   ├── lib.rs              # PyO3 module definition
│   ├── graph.rs            # graphqlite wrapper
│   ├── model.rs            # Node, Edge types
│   ├── extractors/
│   │   ├── mod.rs
│   │   ├── rust.rs         # Rust extractor
│   │   ├── python.rs       # Python extractor
│   │   └── cpp.rs          # C/C++ extractor
│   └── query.rs            # High-level query API
└── python/
    └── muninn_graph/       # Python package (built by maturin)
        └── __init__.py
```

**Build tooling:** Use `maturin` to build Python wheels from Rust.

### Tree-sitter Foundation

Use **tree-sitter** as the parsing foundation for all languages:

**Advantages:**
- Consistent parsing API across languages
- Incremental parsing (only re-parse changed regions)
- Battle-tested (GitHub, Neovim, Zed, Helix)
- Error-tolerant (partial parses for incomplete code)
- **Native Rust bindings** (tree-sitter crate)

**Tree-sitter grammars (Rust crates):**
- `tree-sitter-rust`
- `tree-sitter-python`
- `tree-sitter-c` / `tree-sitter-cpp`

**Parsing Pipeline:**
```
Source File
    │
    ▼ tree-sitter parse (Rust, native speed)
Concrete Syntax Tree (CST)
    │
    ▼ Language Extractor (Rust)
Nodes + Edges (common data model)
    │
    ▼ graphqlite (Rust bindings)
graph.db
    │
    ▼ PyO3 bindings
Python API for Muninn gateway
```

### Incremental Updates

Full rebuild on every change is expensive. Support incremental updates:

**File Watcher Mode:**
```bash
muninn watch  # monitors for file changes, updates graph incrementally
```

**On-Demand Mode:**
```bash
muninn index --incremental  # only process changed files since last index
```

**Implementation:**
1. Track file mtimes in graph metadata
2. On change: delete nodes/edges from that file, re-parse, re-insert
3. Cross-file edges (imports, calls) need dependency tracking

**Trade-off:** Incremental is faster but can drift. Periodic full rebuild ensures consistency:
```bash
muninn index --full  # rebuild from scratch
```

### Storage Location

```
.muninn/
├── cache/
│   ├── graph.db        # graphqlite database (gitignored)
│   ├── embeddings.db   # sqlite-vec database (gitignored)
│   └── index_meta.json # timestamps, file hashes for incremental
├── memory/             # session memory (git-tracked)
└── knowledge/          # curated knowledge (git-tracked)
```

The graph is **rebuildable** - if `.muninn/cache/` is missing or corrupt:
```bash
muninn index  # rebuilds graph + embeddings from source
```

## Alternatives Considered

### Alternative: Language Server Protocol (LSP)

Use existing LSP servers (pyright, tsserver, rust-analyzer) for symbol/reference data.

| Aspect | Pros | Cons |
|--------|------|------|
| Accuracy | Very accurate, production-tested | Requires running LSP server per language |
| Setup | Users may already have LSP | Heavy runtime dependency |
| Features | Rich type info, diagnostics | Overkill for graph queries |
| Latency | Real-time | Startup cost, memory overhead |

**Deferred**: LSP is accurate but heavy. Tree-sitter is lighter and sufficient for structural navigation. Could add LSP integration later for enhanced type information.

### Alternative: ctags/Universal Ctags

Use ctags for symbol extraction.

| Aspect | Pros | Cons |
|--------|------|------|
| Simplicity | Well-known, fast | Symbols only, no relationships |
| Coverage | Many languages | No call graph, no imports |
| Output | Simple format | Missing edges we need |

**Rejected**: ctags gives symbols but not relationships. We need the graph structure.

### Alternative: Custom Regex Parsing

Hand-written regex per language for symbol extraction.

| Aspect | Pros | Cons |
|--------|------|------|
| Control | Full control | Maintenance nightmare |
| Performance | Can be fast | Brittle, misses edge cases |
| Accuracy | Good for simple cases | Fails on complex syntax |

**Rejected**: Tree-sitter is battle-tested and handles edge cases we'd miss with regex.

## Consequences

### Positive
- Structural queries enable smarter context selection
- "What calls X?" is now O(1) lookup instead of grep-and-parse
- Impact analysis before changes ("what would break?")
- Language-agnostic via tree-sitter
- Incremental updates keep graph fresh

### Negative
- Another component to build and maintain
- Tree-sitter grammar quality varies by language
- Graph can get large for huge codebases
- Edge extraction heuristics may miss some relationships

### Neutral
- Graph is derived/rebuildable - no data loss if corrupted
- Storage size scales with codebase size
- Query performance depends on graph DB efficiency

## Open Questions

1. **Visualization**: Should `muninn graph` render a visual? What format? Deferred - not an exploration tool for MVP.

2. **Macro expansion cost**: How expensive is `cargo expand` for large projects? Lazy expansion + cache results.

3. **FFI symbol resolution**: How to link Python FFI calls to Rust/C symbols? Start with PyO3 annotation parsing, extend to other FFI interfaces (ctypes, cffi, cxx).

## Resolved Decisions

| Question | Resolution |
|----------|------------|
| Cross-language edges | `CALLS_FFI` edge type with `target_language` property |
| Dynamic calls | `CALLS_DYNAMIC` edge type with `confidence` < 1.0 |
| Generated code | Track via `EXPANDS_TO`/`GENERATED_BY` edges, `generated: true` node property |
| Query language | Cypher (graphqlite native support) |
| Implementation language | Rust core with PyO3 bindings to Python |
| Language priority | Rust → Python → C/C++ |
| Monorepo handling | Single graph for entire repo; partitions emerge naturally if no cross-package calls |
| Incremental updates | File watcher with continuous per-file rebuild; dangling edges handled at query time |

## Review Triggers

- After implementing Phase 1 languages: evaluate tree-sitter grammar quality
- After real-world usage: evaluate which query patterns are most valuable
- After scaling to 100k+ file repo: evaluate storage size and query performance