# Section 06: Tool C01 -- Dependency Graph

## Overview

Implement `godotiq_dependency_graph` -- the "what does this file touch" tool. Shows bidirectional dependencies: what a file depends on and what depends on it. Cross-references signals with their listeners, tracks autoload dependencies, and computes an impact rating.

**File to create:** `src/godotiq/tools/code.py` (replace placeholder content)
**Test file to create:** `tests/test_tools/test_dependency_graph.py`

---

## Dependencies

- **Upstream:** Section 03 (server integration)
- **Downstream:** None (independent of sections 04, 05, 07)

---

## Implementation Details

### `src/godotiq/tools/code.py`

```python
"""Code Intelligence tools -- dependency graph, signal tracing.

These tools help AI agents understand code relationships and impact.
"""

from __future__ import annotations

from godotiq.session import GodotIQSession


def _build_dependency_graph(session: GodotIQSession, file_path: str, depth: int = 1) -> dict:
    """Build dependency graph for a .gd file.

    Args:
        session: Loaded GodotIQSession.
        file_path: res:// path or relative path.
        depth: How many levels deep to trace (1 = direct only).

    Returns:
        Dictionary with dependency graph data.
    """
    ...
```

### Output Structure

```python
{
    "file": str,                      # res:// path
    "class_name": str,
    "extends": str,
    "is_autoload": bool,

    "emits_signals": [
        {
            "signal_name": str,
            "listeners": [            # cross-referenced: who connects to this signal?
                {
                    "script": str,    # res:// path of listener
                    "connection": str, # the .connect() target
                }
            ],
        }
    ],

    "listens_to": [
        {
            "signal_source": str,     # what signal is connected
            "target": str,            # callback function name
            "line": int,
        }
    ],

    "references_autoloads": [
        {
            "autoload_name": str,
            "access_pattern": str,    # "direct", "has_node", "get_node", "get_node_or_null"
            "line": int,
        }
    ],

    "referenced_by": [str],          # scripts that depend on this file (autoload refs, signal connections)

    "preloads": [str],               # preloaded/loaded resource paths

    "scene_usage": [str],            # scenes where this script is used

    "depth_trace": {                  # only if depth > 1
        str: {                        # res:// path of direct dependency
            "depends_on": [str],      # that dependency's dependencies
            "depended_by": [str],
        }
    },

    "impact_rating": str,             # "low", "medium", "high", "critical"
    "impact_details": str,            # human-readable explanation
}
```

### Signal Cross-Referencing Algorithm

To find listeners for signals emitted by a file:

1. Get the GdScript for the target file
2. For each signal emission in the file, get the signal_name
3. Search across ALL scripts in `session._gd_scripts` for signal_connections where the signal_source matches
4. Also check: if this file defines signals, find any script that connects to them

The tricky part is matching signal names between emissions and connections. The connection's `signal_source` is typically `self.signal_name` or `$Node.signal_name`. The matching logic:
- Extract the signal name from the connection source (last component after `.`)
- Match against signal_name in emissions
- Also check against signals defined by this script

### Referenced-By Computation

A file is referenced by another if:
1. The other script references this file's autoload name (if it's an autoload)
2. The other script preloads/loads this file's path
3. The other script connects to signals defined by this file

### Impact Rating Algorithm

```python
def _compute_impact_rating(
    is_autoload: bool,
    referenced_by_count: int,
    signal_listener_count: int,
    scene_usage_count: int,
) -> tuple[str, str]:
    """Compute impact rating and explanation."""
    score = 0
    reasons = []

    if is_autoload:
        score += 3
        reasons.append("autoload singleton (global)")

    if referenced_by_count >= 5:
        score += 3
        reasons.append(f"referenced by {referenced_by_count} scripts")
    elif referenced_by_count >= 2:
        score += 2
        reasons.append(f"referenced by {referenced_by_count} scripts")
    elif referenced_by_count >= 1:
        score += 1
        reasons.append(f"referenced by {referenced_by_count} script")

    if signal_listener_count >= 3:
        score += 2
        reasons.append(f"{signal_listener_count} signal listeners")
    elif signal_listener_count >= 1:
        score += 1
        reasons.append(f"{signal_listener_count} signal listener(s)")

    if scene_usage_count >= 3:
        score += 1
        reasons.append(f"used in {scene_usage_count} scenes")

    if score >= 6:
        rating = "critical"
    elif score >= 4:
        rating = "high"
    elif score >= 2:
        rating = "medium"
    else:
        rating = "low"

    return rating, "; ".join(reasons) if reasons else "isolated file"
```

### Depth Tracing

When `depth > 1`, for each direct dependency (autoload or preloaded script), recursively compute its dependencies up to the specified depth. Cap at depth=3 to prevent excessive computation.

### Error Handling

If the file is not found or is not a .gd file:
```python
{
    "error": "File not found or not a .gd script",
    "file": str,
}
```

### MCP Tool Registration

```python
from mcp.server.fastmcp import Context
from godotiq.server import mcp


@mcp.tool(
    name="godotiq_dependency_graph",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_dependency_graph(
    file: str,
    depth: int = 1,
    ctx: Context = None,
) -> dict:
    """Dependency graph for a file. Shows what it depends on and what depends on it.

    Args:
        file: Path to .gd file (res:// or relative).
        depth: How many levels deep to trace (1 = direct deps only, 2 = deps of deps).
    """
    session = ctx.request_context.lifespan_state["session"]
    if session is None:
        return {"error": "No Godot project loaded"}
    return _build_dependency_graph(session, file, depth)
```

---

## Tests: `tests/test_tools/test_dependency_graph.py`

```python
"""Tests for godotiq_dependency_graph tool."""

from __future__ import annotations

import pytest

from godotiq.session import GodotIQSession
from godotiq.tools.code import _build_dependency_graph


class TestDependencyGraphBasic:
    def test_graph_basic(self, session: GodotIQSession) -> None:
        """Basic graph structure is correct."""
        result = _build_dependency_graph(session, "res://scripts/autoloads/economy_manager.gd")
        assert result["file"] == "res://scripts/autoloads/economy_manager.gd"
        assert result["class_name"] == "EconomyManagerClass"
        assert result["is_autoload"] is True
        assert "emits_signals" in result
        assert "impact_rating" in result

    def test_graph_autoload_refs(self, session: GodotIQSession) -> None:
        """Autoload references are listed."""
        result = _build_dependency_graph(session, "res://scripts/autoloads/progression_manager.gd")
        autoload_names = [r["autoload_name"] for r in result["references_autoloads"]]
        assert "GameManager" in autoload_names
        assert "EconomyManager" in autoload_names

    def test_graph_referenced_by(self, session: GodotIQSession) -> None:
        """Referenced-by shows scripts that depend on this file."""
        # GameManager is referenced by progression_manager and printer
        result = _build_dependency_graph(session, "res://scripts/entities/printer.gd")
        # printer.gd is the GameManager autoload script, so other scripts may reference it
        assert "referenced_by" in result

    def test_graph_signal_listeners(self, session: GodotIQSession) -> None:
        """Signal emissions are cross-referenced with listeners."""
        result = _build_dependency_graph(session, "res://scripts/autoloads/economy_manager.gd")
        assert "emits_signals" in result
        # economy_manager emits cash_changed
        signal_names = [s["signal_name"] for s in result["emits_signals"]]
        assert "cash_changed" in signal_names


class TestDependencyGraphDepth:
    def test_graph_depth_1(self, session: GodotIQSession) -> None:
        """Depth 1 shows only direct dependencies."""
        result = _build_dependency_graph(session, "res://scripts/autoloads/progression_manager.gd", depth=1)
        # depth_trace should not be present or should be empty for depth=1
        # (implementation detail: only populated for depth > 1)
        assert result.get("depth_trace") is None or result.get("depth_trace") == {}


class TestDependencyGraphImpact:
    def test_graph_impact_rating_autoload(self, session: GodotIQSession) -> None:
        """Autoload scripts get higher impact rating."""
        result = _build_dependency_graph(session, "res://scripts/autoloads/economy_manager.gd")
        assert result["impact_rating"] in ("medium", "high", "critical")

    def test_graph_impact_rating_entity(self, session: GodotIQSession) -> None:
        """Entity scripts get appropriate impact rating."""
        result = _build_dependency_graph(session, "res://scripts/entities/worker.gd")
        assert result["impact_rating"] in ("low", "medium", "high", "critical")

    def test_graph_missing_file(self, session: GodotIQSession) -> None:
        """Missing file returns error."""
        result = _build_dependency_graph(session, "res://nonexistent.gd")
        assert "error" in result
```

---

## Checklist

- [x] `src/godotiq/tools/code/__init__.py` updated with `_build_dependency_graph` and MCP tool
- [x] Signal cross-referencing: emissions matched with connections across all scripts
- [x] Autoload references listed with access pattern
- [x] referenced_by computed from reverse dependency search (autoloads, preloads, signals)
- [x] impact_rating computed from dependency metrics
- [x] depth parameter clamped to [1, 3], single-level depth trace for depth > 1
- [x] Error handling for missing files and non-.gd files
- [x] `tests/test_tools/test_dependency_graph.py` created with 9 tests
- [x] All 294 tests pass: `uv run pytest tests/ -v`

## Implementation Notes

- File path: `src/godotiq/tools/code/__init__.py` (package, not module as plan stated)
- Depth trace simplified to single-level (not recursive) per code review - sufficient for practical use
- Depth parameter clamped with `max(1, min(depth, 3))` per code review
- Uses `lifespan_context` matching existing project pattern (not `lifespan_state` from plan)
