# Sprint 1B+1C — Complete Specification

## Overview

Two new modules that build on Sprint 1A's raw .tscn parser to provide scene-level and project-level analysis capabilities for GodotIQ.

**1B — Scene Resolver** (`src/godotiq/parsers/scene_resolver.py`): Resolves instanced scenes recursively, applies property overrides, calculates world-space transforms.

**1C — Project Index** (`src/godotiq/parsers/project_index.py`): Scans entire Godot project, builds cross-reference maps, parses project.godot for autoloads.

---

## Prerequisites
- Sprint 1A raw .tscn parser complete (81 tests passing)
- Sprint 0 FileCache available in `src/godotiq/cache/file_cache.py`
- Test fixtures available in `tests/fixtures/`

## Constraints
- Zero external dependencies (stdlib only)
- Type hints on all public APIs, docstrings on all public functions
- Do NOT modify Sprint 0 or Sprint 1A code
- Do NOT implement MCP tools — parser/resolver/index only
- Performance: project scan < 500ms (100 files), scene resolve < 200ms

---

## Module 1B: Scene Resolver

### Purpose
Given a raw-parsed TscnScene + project root path, resolve all `instance=ExtResource(...)` references recursively, producing an expanded tree with world-space coordinates.

### Data Model

```python
@dataclass
class ResolvedScene:
    source_path: str                    # original .tscn path
    root: TscnNode                      # root with expanded children
    total_nodes: int                    # count after expansion
    instance_sources: dict[str, str]    # node_path → source .tscn path
    unresolved: list[str]               # paths that couldn't be found

@dataclass
class WorldNode:
    node: TscnNode
    world_position: tuple[float, float, float]
    world_rotation: tuple[float, float, float]
    world_scale: tuple[float, float, float]
    full_path: str                      # "Equipment/FDMPrinter_1/WorkPoint"
    depth: int
    is_instance_root: bool
    instance_source: str | None
```

### Functions

1. **`resolve_scene(parsed, project_root, max_depth=10, resolve_instances=True) -> ResolvedScene`**
   - Walks all nodes in parsed scene
   - For each node with `instance` set: look up ExtResource, get res:// path, convert to filesystem path
   - If path is .tscn: parse it and insert its nodes as children
   - Apply property overrides from parent scene onto instance root
   - Recurse into instanced scene's instances (up to max_depth)
   - Track visited paths to detect circular references

2. **`calculate_world_transforms(resolved) -> list[WorldNode]`**
   - Walk tree top-down from root
   - Accumulate transforms: `child_world = parent_world_basis * child_local_origin + parent_world_origin`
   - Compose basis matrices for rotation/scale: standard 3x3 column-major multiply
   - Decompose final world transform into position/rotation/scale for each node
   - Return flat list of WorldNode

3. **`find_nodes_by_type(world_nodes, node_type) -> list[WorldNode]`**
4. **`find_nodes_by_group(world_nodes, group) -> list[WorldNode]`**
5. **`find_nodes_in_radius(world_nodes, center, radius) -> list[WorldNode]`**
   - Euclidean distance from center to world_position

### res:// Resolution
```python
def _resolve_res_path(res_path: str, project_root: Path) -> Path:
    """Convert res://x/y/z.tscn to project_root/x/y/z.tscn"""
    if res_path.startswith("res://"):
        return project_root / res_path[6:]  # strip "res://"
    return project_root / res_path
```

### Transform Composition
Work with raw column-major 3x3 basis + origin (not Euler angles):

```python
# Basis multiplication: C = A * B (column-major)
# C_col_i = A * B_col_i

# Point transform: world_pos = basis * local_pos + origin
# = col0*pos.x + col1*pos.y + col2*pos.z + origin
```

After full world basis is computed for each node, decompose to Euler angles using existing `decompose_transform3d()` logic.

### Instance Resolution Rules
1. Only .tscn files are recursively resolved
2. .glb/.gltf references are noted in instance_sources but not expanded
3. Property overrides: parent scene's node properties replace instanced scene root's properties
4. Transform override: parent's transform replaces instanced root's transform entirely
5. Missing files → added to `unresolved`, no crash
6. Circular references → depth limit protects

### Edge Cases
- Instance pointing to non-existent file → unresolved list, continue
- Circular A→B→A → max_depth=10 prevents infinite recursion
- GLB/GLTF as instance → noted but not expanded
- No transform on node → identity (position=0,0,0, rotation=0,0,0, scale=1,1,1)

---

## Module 1C: Project Index

### Purpose
Scan entire Godot project filesystem, parse all .tscn files, build dependency maps, and provide cross-reference queries.

### Data Model

```python
@dataclass
class ProjectIndex:
    project_root: Path
    project_name: str                          # from project.godot

    # File inventories
    scenes: dict[str, TscnScene]               # res://path → parsed scene
    scripts: list[str]                         # res://paths of .gd files
    assets: dict[str, dict]                    # res://path → {type, size_bytes, used_by: []}

    # Cross-reference maps
    scene_instances: dict[str, list[str]]      # scene → [scenes it instantiates]
    scene_instance_of: dict[str, list[str]]    # scene → [scenes that instantiate it]
    script_to_scenes: dict[str, list[str]]     # script path → [scenes using it]
    resource_usage: dict[str, list[str]]       # resource path → [scenes referencing it]

    # Autoloads
    autoloads: dict[str, str]                  # name → script/scene path

    # Stats
    total_scenes: int
    total_scripts: int
    total_assets: int
    scan_time_ms: float
```

### Functions

1. **`scan_project(project_root, cache=None) -> ProjectIndex`**
   - Validate project_root contains project.godot
   - Walk filesystem collecting .tscn, .gd, .glb/.gltf/.png/.jpg/.tres files
   - Skip `.godot/` and `addons/` directories
   - Parse all .tscn files (using cache if provided)
   - Build cross-reference maps from parsed scenes
   - Parse project.godot for autoloads and project name

2. **`rescan_changed(index, cache) -> ProjectIndex`**
   - Use FileCache.is_stale() to find changed files
   - Re-parse only changed .tscn files
   - Rebuild affected cross-reference entries

3. **`find_asset_usage(index, asset_path) -> list[str]`**
4. **`find_unused_assets(index) -> list[str]`**
5. **`get_scene_dependency_chain(index, scene_path) -> list[str]`**
   - Recursive: follow scene_instances transitively

### project.godot Parsing

```ini
[autoload]
GameManager="*res://scripts/managers/game_manager.gd"
```

- Parse section headers `[section_name]`
- Parse key="value" pairs
- For `[autoload]`: strip `*` prefix, strip quotes → {name: path}
- For `[application]`: extract `config/name` value

### Cross-Reference Building
For each parsed scene:
1. Extract all ext_resource paths → populate resource_usage
2. Filter ext_resources by type=="PackedScene" with .tscn path → populate scene_instances
3. Filter ext_resources by type=="Script" → populate script_to_scenes
4. Build inverse maps (scene_instance_of from scene_instances)

### Asset Detection
File extensions → types:
- `.glb`, `.gltf` → "model"
- `.png`, `.jpg`, `.jpeg` → "texture"
- `.tres` → "resource"
- `.wav`, `.ogg`, `.mp3` → "audio"

### Filesystem Scanning Rules
- Skip directories: `.godot/`, `addons/`, `.*` (hidden dirs)
- Use os.walk() for traversal
- Convert absolute paths to res:// format: `"res://" + relative_to_project_root`

---

## Test Fixtures

### Existing fixtures (reuse):
- `tests/fixtures/main.tscn` — large scene with instances
- `tests/fixtures/fdm_printer.tscn` — instanced scene
- `tests/fixtures/worker.tscn` — no instances

### New fixture: `tests/fixtures/sample_project/`
```
sample_project/
├── project.godot              # minimal with autoloads
├── scenes/
│   ├── main.tscn              # minimal scene instancing fdm_printer
│   └── equipment/
│       └── fdm_printer.tscn   # minimal scene with child nodes
├── scripts/
│   └── entities/
│       ├── printer.gd         # minimal/empty
│       └── worker.gd          # minimal/empty
└── assets/
    └── models/
        └── printers/
            ├── printer_standard.glb   # empty file
            └── unused_model.glb       # never referenced
```

---

## Test Plan

### 1B Tests (Scene Resolver)
1. `test_resolve_simple_no_instances` — worker.tscn unchanged
2. `test_resolve_with_instance` — main.tscn FDMPrinter_1 expanded
3. `test_property_override_transform` — parent transform overrides instance
4. `test_world_position_root` — root world_pos = local_pos
5. `test_world_position_nested` — child world_pos calculated correctly
6. `test_world_position_deep_nesting` — 4+ levels deep
7. `test_find_nodes_by_group` — printer_slots → 6 results
8. `test_find_nodes_by_type` — Marker3D → N results
9. `test_find_nodes_in_radius` — spatial query works
10. `test_unresolved_instances` — missing file → unresolved, no crash
11. `test_max_depth_limit` — depth respected

### 1C Tests (Project Index)
1. `test_scan_finds_all_tscn` — fixture project → all .tscn found
2. `test_scan_finds_all_scripts` — .gd files listed
3. `test_scene_instances_map` — main→fdm_printer mapped
4. `test_script_to_scenes_map` — printer.gd → fdm_printer.tscn
5. `test_resource_usage` — printer_standard.glb → fdm_printer.tscn
6. `test_autoloads_parsed` — project.godot fixture → autoloads extracted
7. `test_unused_assets` — unreferenced asset found
8. `test_incremental_rescan` — only changed files re-parsed
9. `test_scan_skips_godot_dir` — .godot/ ignored
10. `test_scan_skips_addons` — addons/ ignored
