# Implementation Plan: godotiq_trace_flow

## 1. What We're Building

A new MCP tool called `godotiq_trace_flow` that answers "what happens when the player does X?" by tracing the complete chain of function calls and signal emissions through all `.gd` files in a Godot project.

Given a trigger (exact function name or signal name), the tool:
1. Finds where that trigger lives in the codebase
2. Analyzes the function body for signal emissions and autoload calls
3. Follows each emitted signal to all its listeners across the project
4. Follows each autoload call into the autoload script
5. Recurses until reaching a dead end, depth limit, or cycle

The output is a list of flow steps (file, function, line, action, what it emits/calls) plus detected failure points (early returns, null checks).

This is a pure filesystem tool — no GDScript addon, no running game. It operates on the same parsed data that `dependency_graph` and `signal_map` already use.

---

## 2. Why This Design

### Reusing Existing Infrastructure

GodotIQ already has:
- **gd_parser** which extracts signals, emissions, connections, autoload refs, and function definitions with line ranges from every `.gd` file
- **ProjectIndex** which scans all project files and knows the autoload name→path mapping
- **GodotIQSession** which holds all parsed scripts and provides cross-reference queries

trace_flow builds on top of these. It does NOT reparse files or build its own index.

### Why Exact Matching

Substring matching (e.g. trigger="order" matching "accept_order", "cancel_order") would produce noisy results in real projects. Exact match is predictable — the AI caller knows exactly what function/signal it wants to trace.

### Why Code Connections Only

Godot has two connection mechanisms: code-based `.connect()` calls (parsed by gd_parser into `GdSignalConnection`) and editor-wired connections in `.tscn` files. For v1, we only follow code connections. This keeps the implementation simple and covers the most common pattern (event bus + programmatic wiring). TSCN connections are a natural v2 enhancement.

### Why Trace All Matches

When a trigger name exists in multiple files (e.g. two scripts both have a function called `reset`), we trace from ALL matches rather than asking the caller to disambiguate. This gives the complete picture in a single call.

---

## 3. Architecture

### Data Flow

```
User provides trigger string
        ↓
_find_trigger(trigger, scripts)
  → Searches all GdScript objects for:
    a) Functions with exact name match
    b) Signals with exact name match → finds functions that emit them
  → Returns list[(file_path, function_name)]
        ↓
For each entry point:
  _trace_from_function(file, func, scripts, listeners, autoloads, visited, depth)
    → Builds FlowStep for this function
    → Detects failure points via regex
    → Follows emitted signals → their listeners (recursive)
    → Follows autoload calls → their target functions (recursive)
    → Returns list[FlowStep], list[FailurePoint]
        ↓
Combine all steps and failure points
        ↓
Return result dict with flow, failure_points, files_involved, total_steps
```

### Key Data Structures

```python
@dataclass
class FlowStep:
    file: str           # res:// path
    function: str       # function name
    line: int           # line number
    action: str         # human-readable description
    emits: list[str]    # signal names emitted
    calls: list[str]    # "AutoloadName.method" calls made
```

```python
@dataclass
class FailurePoint:
    step: int           # index into flow list
    condition: str      # the "if" condition
    consequence: str    # what happens (e.g. "flow stops, returns false")
```

### Signal Listener Map

Before tracing begins, build a project-wide map of signal listeners:

```python
def _build_signal_listeners(scripts: dict[str, GdScript]) -> dict[str, list[tuple[str, str]]]:
    """Build signal_name → [(file_path, handler_function)] from all .connect() calls."""
```

This iterates all `GdSignalConnection` objects across all scripts. For each connection:
- Extract the signal name from `signal_source` by splitting on `.` and taking the last part (e.g. "Events.order_accepted" → "order_accepted", "self.my_signal" → "my_signal")
- The handler function name comes from the `target` field
- **Skip lambda targets**: If `target.startswith("func")`, skip this connection — inline lambdas cannot be traced further
- **Skip qualified targets**: If `target` contains a `.` (e.g. `some_node.handler`), skip — the handler lives in another object, not the connecting script
- Store as `listeners[signal_name].append((script_path, target))`

**Known limitation**: Signal name collisions — if two unrelated scripts define a signal with the same name (e.g. both have `completed`), the listener map may associate listeners with the wrong emitter. This is acceptable for v1 given the event bus pattern dominance in Godot projects.

### Trigger Finding

```python
def _find_trigger(trigger: str, scripts: dict[str, GdScript]) -> list[tuple[str, str]]:
    """Find all (file, function) pairs matching the trigger exactly."""
```

Two search modes:
1. **Function name match**: For each script, check if any `GdFunction.name == trigger`
2. **Signal name match**: For each script, check if any `GdSignal.name == trigger`. If found, locate functions that emit this signal by checking `GdSignalEmission` objects and finding which function's line range contains the emission.

Helper needed: `_find_containing_function(script, line)` — given a line number, find which function's `line..end_line` range contains it.

### Recursive Tracing

```python
def _trace_from_function(
    file_path: str,
    function_name: str,
    scripts: dict[str, GdScript],
    listeners: dict[str, list[tuple[str, str]]],
    autoloads: dict[str, str],
    visited: set[tuple[str, str]],
    depth: int,
    max_depth: int,
) -> tuple[list[FlowStep], list[FailurePoint]]:
```

Algorithm:
1. **Base cases**: Return empty if `depth >= max_depth`, if `(file_path, function_name)` already in `visited` (cycle), or if script/function not found
2. **Mark visited**: Add `(file_path, function_name)` to visited set
3. **Find function**: Look up `GdFunction` by exact name in the script
4. **Collect emissions**: Filter `script.signal_emissions` to those within `func.line..func.end_line`
5. **Collect autoload calls**: Filter `script.autoload_refs` to those within `func.line..func.end_line`. Only include refs where `ref.pattern == "direct"` (skip `has_node`, `get_node`, `get_node_or_null` patterns which have empty or path-based `access` fields). Additionally, verify the ref is a method call (not property access) by checking if the raw source line contains `ref.autoload_name + "." + ref.access + "("`. The autoload call is `ref.autoload_name + "." + ref.access`
6. **Build FlowStep** with action description combining emissions and calls
7. **Detect failure points** in the function body (see section below)
8. **Recurse on emissions**: For each emitted signal, look up listeners in the listener map. For each listener `(listener_file, listener_func)`, recurse with `depth + 1`
9. **Recurse on autoload calls**: For each autoload ref, resolve the autoload name to a script path via `autoloads[ref.autoload_name]`, then recurse into `ref.access` as the function name with `depth + 1`
10. **Return** accumulated steps and failure points

### Failure Point Detection

```python
def _detect_failure_points(file_path: str, func: GdFunction, step_index: int) -> list[FailurePoint]:
```

Read the source file lines between `func.line` and `func.end_line`. Apply regex patterns:
- `if\s+not\s+(\w+):\s*return` → "if not {X}, flow stops"
- `if\s+(\w+)\s*==\s*null:\s*return` → "if {X} is null, flow stops"
- `if\s+(\w+)\.is_empty\(\):\s*return` → "if {X} is empty, flow stops"
- `if\s+not\s+is_instance_valid\((\w+)\):\s*return` → "if {X} is invalid, flow stops"

This is intentionally simple — good enough for v1. Returns a list of `FailurePoint` objects.

**Important**: This requires reading the raw `.gd` file to get source lines. The `GdScript` dataclass does not store raw source. The function needs the project root path to construct the full file path.

**Performance**: To avoid re-reading the same file multiple times during deep traces, `_build_trace_flow` should build a `dict[str, list[str]]` cache of `file_path -> lines` at the top level, and pass it to `_trace_from_function` / `_detect_failure_points`.

### Top-Level Function

```python
async def _build_trace_flow(
    session: GodotIQSession,
    trigger: str,
    depth: int = 10,
    detail: str = "normal",
) -> dict:
```

Steps:
1. Validate trigger is not empty
2. Get all parsed scripts from `session._gd_scripts`
3. Get autoloads from `session._index.autoloads`
4. Build signal listener map via `_build_signal_listeners`
5. Find trigger entry points via `_find_trigger`
6. If no entry points found, return `{"flow": [], "failure_points": [], "files_involved": 0, "total_steps": 0, "error": "Trigger not found"}`
7. For each entry point, call `_trace_from_function` with a **shared** visited set across all entries (prevents duplicate downstream paths when multiple entry points converge on the same function)
8. Apply detail filtering:
   - "brief": Strip failure_points, keep only file/function/action in steps
   - "normal": Full output
   - "full": Same as normal (per design decision — metadata only)
9. Return result dict

---

## 4. File Changes

### `src/godotiq/tools/flow/__init__.py` (MODIFY — currently just a docstring)

Add all implementation:
- `FlowStep` and `FailurePoint` dataclasses
- `_build_signal_listeners()` helper
- `_find_trigger()` helper
- `_find_containing_function()` helper
- `_trace_from_function()` recursive tracer
- `_detect_failure_points()` regex-based detector
- `_build_trace_flow()` top-level entry point (follows convention of `_build_dependency_graph`, `_build_signal_map`)

### `src/godotiq/tools/flow/__init__.py` (MODIFY — tool registration)

Following the existing pattern (all tools self-register via `@mcp.tool()` in their module file), add the tool registration here alongside the implementation:
```python
@mcp.tool(
    name="godotiq_trace_flow",
    annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": False},
)
async def godotiq_trace_flow(trigger: str, depth: int = 10, detail: str = "normal", ctx: Context = None) -> dict:
    """Trace how a game action flows through the entire codebase."""
```

Thin wrapper that calls `_build_trace_flow(session, trigger, depth, detail)`.

### `src/godotiq/server.py` (MODIFY)

Add import to trigger tool registration (following existing pattern):
```python
from godotiq.tools import flow  # noqa: E402, F401
```

### `src/godotiq/prompts/godot_development.md` (MODIFY)

Add to UNDERSTAND section:
> `godotiq_trace_flow` — Trace how a game action flows through the entire codebase. Given a trigger (function or signal name), follows the chain of calls and signal emissions through all files. Returns the complete flow with steps and failure points. No game needed — pure filesystem analysis.

### `tests/fixtures/trace_flow/` (CREATE)

Minimal fixture project with cross-file signal chain:

**project.godot**: Declares autoloads `Events` and `PrinterManager`

**events.gd**: Signal bus autoload
```
signal order_accepted
signal printer_reserved
```

**order_manager.gd**: Function `accept_order()` that emits `order_accepted`

**pipeline_controller.gd**: Connects to `Events.order_accepted` in `_ready()`, handler calls `PrinterManager.reserve_printer()`

**printer_manager.gd**: Autoload with `reserve_printer()` that emits `printer_reserved`. Also has an early return (`if not has_available: return`) for failure point testing.

### `tests/test_tools/test_trace_flow.py` (CREATE)

Test file with conftest fixture for the trace_flow fixture project.

---

## 5. Test Strategy

### Fixture Setup

Add to existing `tests/test_tools/conftest.py` (consistent with other tool test fixtures):
```python
@pytest.fixture
def trace_flow_session() -> GodotIQSession:
    """Session loaded from the trace_flow fixture project."""
```

### Test Cases

| Test | What It Verifies |
|------|-----------------|
| test_find_trigger_by_function_name | `_find_trigger("accept_order", ...)` returns order_manager.gd entry |
| test_find_trigger_by_signal_name | `_find_trigger("order_accepted", ...)` finds the emitter function |
| test_trace_single_step | Function with no emissions/calls → 1 step, 0 sub-steps |
| test_trace_signal_chain | accept_order → emits order_accepted → pipeline_controller listens → calls PrinterManager.reserve_printer → printer_manager.gd |
| test_trace_autoload_call | Pipeline controller's autoload call traces into printer_manager.gd |
| test_depth_limit | With depth=1, trace stops after first step |
| test_cycle_detection | Inject a cycle (A→B→A) via fake data, verify no infinite loop |
| test_trigger_not_found | Unknown trigger returns empty flow with error message |
| test_multiple_listeners | Add second listener to order_accepted, verify both branches appear |
| test_detail_brief | Brief mode excludes failure_points and extra step fields |
| test_failure_point_detection | printer_manager's early return is detected as a failure point |
| test_lambda_target_skipped | Connection with lambda target is not included in listener map |
| test_non_direct_autoload_ref_skipped | `has_node`/`get_node` autoload refs do not produce flow steps |
| test_property_access_not_traced | Autoload property access (no `()`) does not produce a flow step |
| test_server_registration | `godotiq_trace_flow` appears in `mcp` registered tools |

### Verification

1. All 695+ existing tests still pass
2. All new trace_flow tests pass
3. Import check works: `from godotiq.tools.flow import _build_trace_flow`

---

## 6. Risks and Mitigations

| Risk | Mitigation |
|------|-----------|
| `GdAutoloadRef.access` may include property access, not just method calls | Only follow `ref.pattern == "direct"` refs. Check raw source line for `(` after the access to confirm it's a method call, not a property access |
| Autoload res:// path format | Both `session._index.autoloads` values and `session._gd_scripts` keys use `res://` paths — direct lookup works. Use defensive `.get()` with None check |
| Function `end_line` edge case | Parser already sets `end_line` correctly at EOF cleanup. If `func.end_line < func.line`, treat as degenerate and only scan `func.line` itself. Do NOT use a 99999 sentinel |
| Non-"direct" autoload ref patterns | `has_node`, `get_node`, `get_node_or_null` patterns have empty or path-based access fields — filter these out before tracing |
| Lambda targets in signal connections | Connections with `target.startswith("func")` are inline lambdas — skip during listener map construction |
| Failure point detection requires reading raw .gd files | Use `session._index.project_root` to construct file paths, read with Path.read_text() |
| Signal bus signals (emitted from one file, defined in another) may not match | The listener map is based on `.connect()` calls, not definitions — this should work correctly |

---

## 7. What We're NOT Building

- TSCN editor connection following (v2 enhancement)
- Substring/fuzzy trigger matching
- Source code snippets in output
- Interactive disambiguation for multiple matches
- Call hierarchy for non-signal function calls (e.g. direct method calls between scripts)
- Visualization or graph rendering
