# Section 05: Tool G02 -- File Context

## Overview

Implement `godotiq_file_context` -- the "deep context before editing" tool. An AI agent calls this before editing any file to understand the file's dependencies, public API, signal connections, and risk surface. Works for both .gd scripts and .tscn scenes.

**File to modify:** `src/godotiq/tools/memory.py` (add to existing file from section 04)
**Test file to create:** `tests/test_tools/test_file_context.py`

---

## Dependencies

- **Upstream:** Section 03 (server integration), Section 04 (memory.py already exists)
- **Downstream:** None (independent of sections 06 and 07)

---

## Implementation Details

### Add to `src/godotiq/tools/memory.py`

#### Internal Logic Function

```python
def _build_file_context(session: GodotIQSession, file_path: str) -> dict:
    """Build deep context for a single file.

    Args:
        session: Loaded GodotIQSession.
        file_path: res:// path or relative path from project root.

    Returns:
        Dictionary with file context data. Returns error dict if file not found.
    """
    ...
```

#### Path Resolution

The tool accepts both `res://scripts/entities/printer.gd` and `scripts/entities/printer.gd`. Use `session.resolve_file_path()` to normalize to res:// format.

#### Output for .gd Files

```python
{
    "file": str,                    # res:// path
    "type": "gdscript",
    "class_name": str,              # "" if no class_name
    "extends": str,
    "is_autoload": bool,            # is this script registered as an autoload?
    "lines": int,                   # line count of the file
    "public_api": {
        "functions": [
            {
                "name": str,
                "args": [{"name": str, "type": str, "default": str}],
                "return_type": str,
                "is_static": bool,
                "line": int,
            }
        ],
        "signals": [
            {
                "name": str,
                "args": [{"name": str, "type": str}],
                "line": int,
            }
        ],
        "variables": [
            {
                "name": str,
                "type": str,
                "is_const": bool,
                "annotation": str,
                "line": int,
            }
        ],
        "enums": [
            {
                "name": str,
                "values": [str],
                "line": int,
            }
        ],
    },
    "signals_defined": [str],       # signal names this file defines
    "signals_emitted": [str],       # signal names this file emits
    "signals_connected": [str],     # signal sources this file connects to
    "depends_on": {
        "autoloads": [str],         # autoload singletons referenced
        "preloads": [str],          # preloaded/loaded res:// paths
        "scenes": [str],            # scenes this script is used in
    },
    "depended_by": [str],           # scripts that reference this file (via autoload, signals)
    "used_in_scenes": [str],        # scenes where this script is attached to a node
    "safety": {
        "is_instance_valid_count": int,
        "has_serialize": bool,      # has serialize() and/or deserialize() functions
    },
}
```

Data sources:
- `session.get_script(res_path)` for the GdScript
- `session._index.autoloads` to check is_autoload
- `gd.functions` filtered by `not is_private` for public_api (functions not starting with `_`)
- `gd.signals` for signals_defined
- `gd.signal_emissions` for signals_emitted
- `gd.signal_connections` for signals_connected
- `gd.autoload_refs` for depends_on.autoloads
- `gd.preload_refs` for depends_on.preloads
- `session._index.script_to_scenes` for used_in_scenes
- `session.get_autoload_dependents()` or reverse search for depended_by
- `gd.is_instance_valid_lines` for safety.is_instance_valid_count
- Check function names for "serialize"/"deserialize" for has_serialize

#### Output for .tscn Files

```python
{
    "file": str,                    # res:// path
    "type": "scene",
    "total_nodes": int,
    "root_type": str,               # type of the root node
    "ext_resources": [
        {
            "id": str,
            "type": str,
            "path": str,
        }
    ],
    "instances": [str],             # instanced sub-scene res:// paths
    "scripts_used": [str],          # script res:// paths attached to nodes
    "groups_used": [str],           # unique group names across all nodes
    "connections": [
        {
            "signal": str,
            "from": str,
            "to": str,
            "method": str,
        }
    ],
    "instanced_by": [str],         # scenes that instance this scene
}
```

Data sources:
- `session.get_scene(res_path)` for the TscnScene
- `TscnScene.nodes` for total_nodes count
- `TscnScene.root.type` for root_type
- `TscnScene.ext_resources` for ext_resources
- `TscnScene.get_all_instances()` for instances
- `TscnScene.get_all_scripts()` for scripts_used
- Iterate `.nodes` for unique groups
- `TscnScene.connections` for connections
- `session._index.scene_instance_of` for instanced_by

#### Error Handling

If the file is not found in the session:
```python
{
    "error": "File not found",
    "file": str,  # the path that was requested
}
```

### MCP Tool Registration

```python
@mcp.tool(
    name="godotiq_file_context",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": False,
    },
)
async def godotiq_file_context(
    file: str,
    ctx: Context = None,
) -> dict:
    """Deep context for a file. Call before editing to understand dependencies and risks.

    Works for both .gd scripts and .tscn scenes.

    Args:
        file: Path to file, either res:// path or relative path from project root.
    """
    session = ctx.request_context.lifespan_state["session"]
    if session is None:
        return {"error": "No Godot project loaded"}
    return _build_file_context(session, file)
```

---

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

```python
"""Tests for godotiq_file_context tool."""

from __future__ import annotations

import pytest

from godotiq.session import GodotIQSession
from godotiq.tools.memory import _build_file_context


class TestFileContextGdScript:
    def test_context_gd_file(self, session: GodotIQSession) -> None:
        """Basic .gd file returns expected structure."""
        result = _build_file_context(session, "res://scripts/autoloads/economy_manager.gd")
        assert result["type"] == "gdscript"
        assert result["class_name"] == "EconomyManagerClass"
        assert result["extends"] == "Node"
        assert "public_api" in result

    def test_context_gd_autoload(self, session: GodotIQSession) -> None:
        """Autoload script is correctly identified."""
        result = _build_file_context(session, "res://scripts/autoloads/economy_manager.gd")
        assert result["is_autoload"] is True

    def test_context_gd_non_autoload(self, session: GodotIQSession) -> None:
        """Non-autoload script is correctly identified."""
        result = _build_file_context(session, "res://scripts/entities/worker.gd")
        assert result["is_autoload"] is False

    def test_context_gd_dependencies(self, session: GodotIQSession) -> None:
        """Dependencies are correctly listed."""
        result = _build_file_context(session, "res://scripts/autoloads/progression_manager.gd")
        assert "depends_on" in result
        assert "GameManager" in result["depends_on"]["autoloads"]
        assert "EconomyManager" in result["depends_on"]["autoloads"]

    def test_context_gd_signals(self, session: GodotIQSession) -> None:
        """Signals are listed."""
        result = _build_file_context(session, "res://scripts/autoloads/economy_manager.gd")
        assert "cash_changed" in result["signals_defined"]
        assert "cash_changed" in result["signals_emitted"]

    def test_context_gd_public_api(self, session: GodotIQSession) -> None:
        """Public API lists non-private functions."""
        result = _build_file_context(session, "res://scripts/autoloads/economy_manager.gd")
        func_names = [f["name"] for f in result["public_api"]["functions"]]
        assert "add_cash" in func_names
        assert "spend_cash" in func_names
        # Private functions should not appear
        assert "_ready" not in func_names


class TestFileContextTscnScene:
    def test_context_tscn_file(self, session: GodotIQSession) -> None:
        """Basic .tscn file returns expected structure."""
        result = _build_file_context(session, "res://scenes/equipment/fdm_printer.tscn")
        assert result["type"] == "scene"
        assert result["root_type"] == "Node3D"
        assert result["total_nodes"] >= 4

    def test_context_tscn_scripts(self, session: GodotIQSession) -> None:
        """Scene lists attached scripts."""
        result = _build_file_context(session, "res://scenes/equipment/fdm_printer.tscn")
        assert "res://scripts/entities/printer.gd" in result["scripts_used"]

    def test_context_tscn_instances(self, session: GodotIQSession) -> None:
        """Scene lists instanced sub-scenes."""
        result = _build_file_context(session, "res://scenes/main.tscn")
        assert "res://scenes/equipment/fdm_printer.tscn" in result["instances"]


class TestFileContextPathHandling:
    def test_context_res_path(self, session: GodotIQSession) -> None:
        """Accepts res:// paths."""
        result = _build_file_context(session, "res://scripts/autoloads/economy_manager.gd")
        assert "error" not in result

    def test_context_relative_path(self, session: GodotIQSession) -> None:
        """Accepts relative paths from project root."""
        result = _build_file_context(session, "scripts/autoloads/economy_manager.gd")
        assert "error" not in result
        assert result["file"].startswith("res://")

    def test_context_missing_file(self, session: GodotIQSession) -> None:
        """Returns error dict for missing files."""
        result = _build_file_context(session, "res://nonexistent.gd")
        assert "error" in result
```

---

## Checklist

- [x] `_build_file_context` added to `src/godotiq/tools/memory/__init__.py`
- [x] Handles .gd files: class_name, extends, is_autoload, public_api, signals, dependencies
- [x] Handles .tscn files: total_nodes, root_type, instances, scripts_used, groups, connections
- [x] Path resolution: accepts both res:// and relative paths
- [x] Error handling: returns error dict for missing files + unsupported file types
- [x] MCP tool registered with correct annotations
- [x] `tests/test_tools/test_file_context.py` created with 16 tests
- [x] All 285 tests pass: `uv run pytest tests/ -v`

## Implementation Notes

- File path: `src/godotiq/tools/memory/__init__.py` (package, not `.py` module as plan stated)
- Added `GdScript` and `TscnScene` type annotations to helper functions per code review
- Added 4 extra tests from code review: safety fields, depended_by, groups_used, instanced_by
- Unsupported file types (e.g., .tres) return descriptive error instead of "File not found"
- Uses `lifespan_context` (not `lifespan_state`) matching existing project pattern
