# LOE: Execution Flow Tracing in the Knowledge Graph

**Date:** 2026-03-06
**Scope:** mcp-vector-search — KG-backed execution flow tracing
**Analyst:** Research Agent

---

## 1. Current State: What Call Graph Data We Actually Have

### 1.1 Schema — CALLS Edge Is Defined

`KnowledgeGraph._create_schema()` creates a `CALLS` relationship table:

```sql
CREATE REL TABLE IF NOT EXISTS CALLS (
    FROM CodeEntity TO CodeEntity,
    weight DOUBLE DEFAULT 1.0,
    commit_sha STRING,
    MANY_MANY
)
```

Nodes: `CodeEntity(id, name, entity_type, file_path, commit_sha)`.
Edge direction is stored (`FROM → TO`), which is the correct foundation for transitive traversal.

### 1.2 `CodeChunk.calls` — Field Exists, Sparsely Populated

The `CodeChunk` dataclass has `calls: list[str]` (function/method call targets as name strings). This field is **populated by only 2 of the 9 language parsers**:

| Parser | Extracts `calls`? | Method |
|--------|------------------|--------|
| Python (`python_helpers/node_extractors.py`) | YES | Tree-sitter `call` node traversal |
| JavaScript / TypeScript | YES | Tree-sitter `call_expression` traversal, also handles method chains |
| Go | NO | No call extraction |
| Rust | NO | No call extraction |
| Java | NO | No call extraction |
| C# | NO | No call extraction |
| Ruby | NO | No call extraction |
| PHP | NO | No call extraction |
| Dart | NO | No call extraction |

**Python extraction:** `MetadataExtractor.extract_function_calls()` walks the tree-sitter AST for `call` nodes, capturing simple calls (`print`) and method calls (`self.save`, `db.query`).

**JavaScript/TypeScript extraction:** `_extract_calls_from_node()` walks for `call_expression` nodes, captures `foo()`, `obj.bar()`, and chained `a.b.c()` calls.

### 1.3 `KGBuilder._resolve_entity()` — Name Resolution Is Simple/Lossy

The builder stores a flat `dict[str, str]` mapping `name → chunk_id`. Calls are resolved only if the callee name (or its last dotted segment) matches a KG entity name exactly:

```python
if name in self._entity_map:
    return self._entity_map[name]
if "." in name:
    short_name = name.split(".")[-1]
    if short_name in self._entity_map:
        return self._entity_map[short_name]
return None  # Unresolved — CALLS edge is silently dropped
```

**Critical consequence:** Standard library calls, third-party calls, and any call where the callee name matches a `GENERIC_ENTITY_NAMES` exclusion list are silently dropped. Only same-project entity-to-entity calls survive. Cross-file calls resolve only if both sides happened to be indexed and the names match.

### 1.4 Existing KG Query Capabilities

| Tool | Location | Current Behavior |
|------|----------|-----------------|
| `kg_query` MCP tool | `kg_handlers.py` | 1-hop only: calls what? called by what? |
| `get_call_graph()` | `knowledge_graph.py` | Single hop: `(f)-[:CALLS]->(called)` UNION `(caller)-[:CALLS]->(f)`. No depth parameter. |
| `find_related()` | `knowledge_graph.py` | N-hop undirected traversal `-[*1..N]-` across ALL edge types (CALLS, IMPORTS, INHERITS, CONTAINS mixed together). Hardcoded `LIMIT 50`. Returns entity info but no path metadata, no edge type breakdown. |
| `kg calls` CLI | `kg.py` | Wraps `get_call_graph()` — single hop, no depth. |
| `kg query` CLI | `kg.py` | Wraps `find_related()` — N-hop, `--hops` param, but undirected and mixes all relationship types. |

**There is no existing directed, typed, multi-hop call chain traversal.** The closest primitive (`find_related`) is undirected, mixes all edge types, returns no path structure, and has no cycle protection beyond Kuzu's internal handling.

### 1.5 Kuzu Multi-Hop Traversal Capabilities

Kuzu supports **variable-length path patterns** in its Cypher dialect:

```cypher
MATCH (a)-[:CALLS*1..5]->(b)   -- directional, CALLS-only, up to 5 hops
```

This is already used in `get_inheritance_tree()`:
```cypher
MATCH (c:CodeEntity {id: $id})-[:INHERITS*]->(parent:CodeEntity)
```

Kuzu can therefore handle the core traversal query natively. The `find_related()` query already uses `[*1..N]` syntax. **No graph database engine change is needed.**

---

## 2. Gap Analysis

### Gap A: Parser Coverage (7 of 9 parsers have no call extraction)

Go, Rust, Java, C#, Ruby, PHP, Dart parsers do not extract `calls`. Each already uses tree-sitter AST, so the mechanism exists — it is simply not implemented.

**Impact:** For non-Python/JS codebases, the CALLS table will be effectively empty, making execution flow tracing useless.

### Gap B: Name Resolution Is Lossy (silent drops)

`_resolve_entity()` silently discards calls when the target is not in the in-memory name map. There is no disambiguation for overloaded function names (multiple entities with the same name in different files). The resolution happens at build-time and is not stored in the CALLS edge itself.

**Impact:** Even for Python/JS, call edges may be substantially incomplete for cross-file calls to same-named functions.

### Gap C: No Directed Multi-Hop Call Chain Method

`get_call_graph()` is 1-hop only. `find_related()` is undirected/mixed-type. There is no:

- `trace_execution_flow(entry_point, depth, direction)` method on `KnowledgeGraph`
- Cycle detection within traversal (risk of infinite loops in recursive call graphs)
- Path metadata (what is the chain? what is the call order?)
- Distinction between "callee chain" (outbound) vs "caller chain" (inbound, for impact analysis)

### Gap D: No MCP Tool for Execution Flow Tracing

The `kg_query` MCP tool is 1-hop and does not return path structure. There is no `trace_execution_flow` MCP tool. Adding one requires:

- New handler in `kg_handlers.py`
- New schema entry in `tool_schemas.py`
- Registration in `server.py`

### Gap E: No CLI Command for Execution Flow Tracing

The `kg calls` CLI command is 1-hop. A new `kg trace` (or `kg flow`) command would be needed with `--depth`, `--direction` (outbound/inbound/both), and `--format` (tree/list/json) options.

### Gap F: Output Format for Paths Not Defined

A call chain has inherent structure (ordered tree, not flat list). The current infrastructure returns flat lists of `{id, name, direction}`. Representing a 5-hop chain requires either:
- A tree structure (nested children)
- An ordered list with hop number and predecessor
- A subgraph (nodes + directed edges)

No output formatter for this exists yet.

---

## 3. LOE Breakdown

### Phase 1: Verify/Fix Call Edge Completeness Across Parsers

**Goal:** Ensure `calls` is extracted and populated for all supported languages, and that resolution quality is acceptable.

| Task | Size | Details |
|------|------|---------|
| Audit Python extraction quality (spot-check real indexed data) | S | Run `kg stats`, query CALLS count vs entity count. Check representative Python files. |
| Add call extraction to Go parser | M | Go has `call_expression` in tree-sitter. Write `_extract_calls_from_node()` analog. Test on Go files. |
| Add call extraction to Rust parser | M | Rust tree-sitter: `call_expression` and `method_call_expression`. Slightly different AST shape. |
| Add call extraction to Java parser | M | Java tree-sitter: `method_invocation`. Handles `obj.method()` and `method()`. |
| Add call extraction to C# parser | M | C# tree-sitter: `invocation_expression`. |
| Add call extraction to Ruby parser | S | Ruby: `call` node, simpler than Java. |
| Add call extraction to PHP/Dart parsers | S | Similar pattern; can share a helper. |
| Improve `_resolve_entity()` with file-scoped disambiguation | M | When multiple entities share a name, prefer the one in the same file as the caller. Requires passing `file_path` to resolver. |
| Write tests for call extraction per language | M | Unit tests verifying `calls` field is non-empty for known function bodies. |

**Phase 1 Total: L (6–8 dev-days)**

The Go/Rust/Java/C# parsers each need independent implementation and testing. The resolution improvement is subtle but high-value.

---

### Phase 2: Build KG Traversal Query for N-Hop Call Chains

**Goal:** Add `trace_execution_flow()` method to `KnowledgeGraph` with correct directed traversal, cycle detection, and structured path output.

| Task | Size | Details |
|------|------|---------|
| Implement `trace_execution_flow(entry_id, depth, direction)` in `knowledge_graph.py` | M | Kuzu Cypher: `MATCH (start)-[:CALLS*1..N]->(end)` (outbound). Inbound = reversed. Both = UNION. Return path nodes with hop index. |
| Add cycle detection / deduplication | S | Kuzu's variable-length path matching handles simple cycles via shortest-path semantics, but explicit `WHERE` deduplication may be needed for MANY_MANY graphs. |
| Return structured path data (ordered hop chain, not flat list) | M | Kuzu returns rows; reconstruct tree in Python layer. Each row = (hop_0_name, hop_1_name, ..., hop_N_name). Need to decide on output format (tree vs flat with depth). |
| Handle entry_point resolution by name (not just ID) | S | Reuse `find_entity_by_name()`. May need fuzzy matching for `ClassName.method_name` format. |
| Performance: depth cap enforcement and result limits | S | Enforce max depth (default 5, configurable). Add LIMIT to prevent runaway queries on large graphs. |
| Write integration tests for traversal | M | Tests against a small known KG: A calls B calls C calls D. Verify 1-hop, 2-hop, 3-hop results. |

**Phase 2 Total: M (3–4 dev-days)**

Kuzu already supports the core query pattern. The main work is the Python layer that reconstructs path structure from flat query results.

---

### Phase 3: New MCP Tool `trace_execution_flow(entry_point, depth)`

**Goal:** Expose the traversal as a callable MCP tool with well-designed input/output schema.

| Task | Size | Details |
|------|------|---------|
| Add `handle_trace_execution_flow()` in `kg_handlers.py` | S | Mirror pattern of `handle_kg_query()`. Call `kg.trace_execution_flow()`. Format output as JSON. |
| Add schema `_get_trace_execution_flow_schema()` in `tool_schemas.py` | S | Parameters: `entry_point` (str), `depth` (int, default 3, max 10), `direction` (enum: outbound/inbound/both), `limit` (int). Rich description with examples. |
| Register tool in `server.py` and `kg_handlers.py` dispatch | S | Follow existing registration pattern. |
| Design output format for path subgraph | M | Return: `{nodes: [...], edges: [...], paths: [...], entry_point, depth}`. Nodes include `{id, name, type, file_path, hop}`. Edges include `{from, to, hop}`. Paths = ordered list of node IDs per chain. |
| Handle empty/partial results gracefully | S | If entry_point not found, return informative error. If CALLS table empty, suggest running `kg_build`. |
| Write handler tests | S | Mock KG, verify tool input validation and output shaping. |

**Phase 3 Total: S-M (2–3 dev-days)**

The handler layer is thin (existing patterns are well-established). Most effort is the output schema design.

---

### Phase 4: CLI Integration + Output Formatting

**Goal:** Add `mcp-vector-search kg trace <entry_point>` CLI command with human-readable tree output.

| Task | Size | Details |
|------|------|---------|
| Add `@kg_app.command("trace")` in `kg.py` | S | Arguments: `entry_point` (required), `--depth` (default 3), `--direction` (outbound/inbound/both), `--format` (tree/json/list). |
| Tree output formatter (Rich Tree) | M | Use `rich.tree.Tree` to render call chain as indented tree. Show `function_name [file:line]` at each node. Annotate with hop number. Handle cycles (mark as `[already visited]`). |
| JSON/list output mode | S | `--format json` dumps raw `trace_execution_flow()` result. `--format list` prints flat ordered list with depth prefix. |
| Integration test: CLI invocation | S | `subprocess` test calling `kg trace` and checking output structure. |

**Phase 4 Total: S (1–2 dev-days)**

The CLI layer is mechanical. The Rich tree formatter is the most interesting part and fits existing CLI patterns.

---

## 4. Total LOE Summary

| Phase | Description | Size | Dev-Days (Est.) |
|-------|-------------|------|-----------------|
| Phase 1 | Parser call extraction (7 parsers) + resolver improvement | L | 6–8 days |
| Phase 2 | KG traversal method + cycle handling + path structure | M | 3–4 days |
| Phase 3 | MCP tool: handler, schema, registration, output format | S-M | 2–3 days |
| Phase 4 | CLI command + Rich tree formatter | S | 1–2 days |
| **Total** | | | **12–17 dev-days** |

**Conservative estimate: 15 dev-days (3 weeks for a single focused engineer).**

If parser work is scoped down to Python + JS only (already done), and Go/Rust/Java/C# are deferred:
- Reduced Phase 1: 1–2 days (just audit + resolver improvement)
- **Reduced total: 7–11 dev-days**

---

## 5. Risks and Blockers

### Risk 1: CALLS Table May Be Sparse Even for Python/JS (MEDIUM)

`_resolve_entity()` silently drops unresolvable calls. In a typical codebase, a large fraction of calls are to standard library functions (not in the KG) or to third-party imports. The `GENERIC_ENTITY_NAMES` filter also blocks common names. Without empirical measurement on a real indexed project, the actual CALLS density is unknown.

**Mitigation:** Before Phase 2 work, run `kg stats` on a real project to check CALLS count vs entity count ratio. If CALLS count is near zero, Phase 1 becomes the critical path.

### Risk 2: Kuzu Variable-Length Path Performance on Large Graphs (LOW-MEDIUM)

`MATCH (a)-[:CALLS*1..10]->(b)` on a graph with 10,000+ entities could be slow. Kuzu is an embedded OLAP graph database and handles path queries well, but unbounded fan-out at each hop (a function calling 20 others, each calling 20 more) creates exponential result growth.

**Mitigation:** Cap depth at 10 (configurable), add LIMIT to queries, and consider returning only a BFS-ordered first-N results rather than all paths.

### Risk 3: Name Resolution Ambiguity (MEDIUM)

Multiple functions with the same name in different files (e.g., `validate()` in 10 different modules) will all resolve to whichever was last stored in `_entity_map`. This is a build-time collision that produces incorrect CALLS edges.

**Mitigation:** Phase 1 resolver improvement should prefer file-scope disambiguation. Longer term, CALLS edges should encode the caller's file path for precise resolution.

### Risk 4: Direction Semantics for "Inbound" Tracing (LOW)

Inbound tracing ("who calls this function, and who calls them?") requires reversing the CALLS direction. Kuzu supports `<-[:CALLS*1..N]-` syntax. However, the current schema uses `MANY_MANY` which allows traversal in both directions — care is needed in query authoring.

**Mitigation:** Explicit directional syntax (`->` vs `<-`) in queries. Tests should verify directionality.

### Risk 5: Method Calls on `self` (Python) Are Not Resolvable (LOW)

Python calls like `self.save()` extract the callee as `self.save`. The resolver strips to `save` (last dotted segment), which may match a KG entity in a different class. This produces false CALLS edges between unrelated functions.

**Mitigation:** Filter out `self.` prefix before resolution, or store the class context on the caller entity to enable class-scoped resolution.

### Risk 6: No Test Coverage for KG Call Extraction Today (MEDIUM)

There are no existing tests that verify the end-to-end pipeline from parser → `calls` field → KG CALLS edge. Regressions in this path would be silent.

**Mitigation:** Phase 1 test work is mandatory, not optional.

---

## 6. Implementation Order Recommendation

1. **First:** Measure actual CALLS density on a real project (`kg stats`). This is 30 minutes of work and determines whether Phase 1 is blocking.
2. **If CALLS is non-zero for Python:** Proceed to Phase 2 (traversal method) as a proof-of-concept using existing Python/JS data only.
3. **Phase 3** (MCP tool) can be delivered alongside Phase 2 — they are independent once the `KnowledgeGraph` method exists.
4. **Phase 4** (CLI) is the lowest priority and can be delivered last or skipped for the initial MCP-only release.
5. **Phase 1** (parser coverage) can be parallelized by language — each parser is independent.

---

## 7. Files to Modify

| File | Change |
|------|--------|
| `core/knowledge_graph.py` | Add `trace_execution_flow()` method |
| `core/kg_builder.py` | Improve `_resolve_entity()` with file-scoped disambiguation |
| `parsers/go.py` | Add `_extract_calls_from_node()` |
| `parsers/rust.py` | Add call extraction |
| `parsers/java.py` | Add call extraction |
| `parsers/csharp.py` | Add call extraction |
| `parsers/ruby.py` | Add call extraction |
| `parsers/php.py` | Add call extraction |
| `parsers/dart.py` | Add call extraction |
| `mcp/kg_handlers.py` | Add `handle_trace_execution_flow()` |
| `mcp/tool_schemas.py` | Add `_get_trace_execution_flow_schema()` |
| `mcp/server.py` | Register new tool |
| `cli/commands/kg.py` | Add `@kg_app.command("trace")` |
| `tests/` | New test files for each phase |
