Now I have all the context needed. Let me produce the section content.

# Section 4: Tree Reconstruction and Query Methods

## Overview

This section implements the second pass of the TSCN parser -- the tree reconstruction phase -- plus all query methods on `TscnScene`. After Pass 1 (the state machine parser from Section 3) produces a flat list of `TscnNode` objects with `parent` path attributes, Pass 2 builds the parent-child hierarchy by linking each node into its parent's `children` list. Additionally, five query methods are added to `TscnScene` that downstream MCP tools depend on.

## Dependencies

- **Section 2 (Dataclasses):** `TscnNode` must have the `parent`, `children`, `name`, `type`, `groups`, `script`, `instance` fields. `TscnScene` must have `root`, `nodes`, `ext_resources` fields.
- **Section 3 (State Machine Parser):** Pass 1 must produce a flat list of `TscnNode` objects with `parent` set from the `[node]` section header attribute. Each node's `name` must be populated. The `ext_resources` dict must be populated with `ExtResource` objects keyed by ID string.

## File to Modify

`/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py`

All tree reconstruction logic and query methods live in this file.

---

## Tests (Write First)

These tests go in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_tscn_parser.py`. They validate the tree hierarchy built by Pass 2 and all five query methods. The tests assume that the fixture files are fully parseable (Sections 1-3 are complete).

### Fixture files used

- `/Users/salvatorefarruggio/Desktop/godotiq/tests/fixtures/worker.tscn` -- 19 lines, 4 nodes (Worker root with 3 direct children)
- `/Users/salvatorefarruggio/Desktop/godotiq/tests/fixtures/fdm_printer.tscn` -- 30 lines, 6 nodes (FDMPrinter root; StaticBody3D has child CollisionShape3D)
- `/Users/salvatorefarruggio/Desktop/godotiq/tests/fixtures/main.tscn` -- 4711 lines, 876 nodes (deep nesting, groups, scripts, instances)

### Test stubs

```python
# tests/test_parsers/test_tscn_parser.py
# (These tests may be added alongside or after Section 3 tests in the same file.)

import pytest
from pathlib import Path
from godotiq.parsers.tscn_parser import parse_tscn

FIXTURES = Path(__file__).resolve().parent.parent / "fixtures"


# --- Hierarchy (tree reconstruction) ---

def test_fdm_printer_root_children_count():
    """fdm_printer.tscn root (FDMPrinter) has 5 direct children:
    StaticBody3D, PrinterModel, FilamentSpool, WorkPoint, plus one more."""
    scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
    # FDMPrinter is root with parent="" (no parent attribute in header)
    assert scene.root is not None
    assert scene.root.name == "FDMPrinter"
    # StaticBody3D, PrinterModel, FilamentSpool, WorkPoint = 4 direct children
    # (StaticBody3D has its own child CollisionShape3D, but that is not a direct child of root)
    assert len(scene.root.children) == 4


def test_fdm_printer_nested_child():
    """CollisionShape3D is a child of StaticBody3D, not of root."""
    scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
    static_body = next(c for c in scene.root.children if c.name == "StaticBody3D")
    assert len(static_body.children) == 1
    assert static_body.children[0].name == "CollisionShape3D"


def test_main_deep_nesting_reachable():
    """A 6+ level deep node is reachable via the children chain.
    Path: Main -> Building -> LockedZones -> PostProcessingZone -> PostProcessingFloor -> FloorTile_0_0
    """
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    node = scene.root
    for name in ["Building", "LockedZones", "PostProcessingZone", "PostProcessingFloor", "FloorTile_0_0"]:
        node = next((c for c in node.children if c.name == name), None)
        assert node is not None, f"Could not find child '{name}'"


def test_all_flat_nodes_reachable_via_tree():
    """Every node in the flat list should be reachable via tree traversal from root."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    # Collect all names via tree walk
    visited = set()
    stack = [scene.root]
    while stack:
        n = stack.pop()
        visited.add(n.name)
        stack.extend(n.children)
    flat_names = {n.name for n in scene.nodes}
    assert visited == flat_names


# --- find_node ---

def test_find_node_direct_child():
    """find_node('CollisionShape3D') on worker.tscn returns the correct node."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    node = scene.find_node("CollisionShape3D")
    assert node is not None
    assert node.name == "CollisionShape3D"
    assert node.type == "CollisionShape3D"


def test_find_node_deep_path():
    """find_node('Building/LockedZones/PostProcessingZone') on main.tscn."""
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    node = scene.find_node("Building/LockedZones/PostProcessingZone")
    assert node is not None
    assert node.name == "PostProcessingZone"
    assert node.type == "Node3D"


def test_find_node_nonexistent_returns_none():
    """find_node('nonexistent') returns None."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    assert scene.find_node("nonexistent") is None


def test_find_node_root_by_empty_string():
    """find_node('') returns the root node."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    node = scene.find_node("")
    assert node is not None
    assert node.name == "Worker"


def test_find_node_root_by_name():
    """find_node('Worker') returns the root node (root name match)."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    node = scene.find_node("Worker")
    assert node is not None
    assert node.name == "Worker"


# --- get_nodes_by_group ---

def test_get_nodes_by_group_interactable():
    """fdm_printer.tscn FDMPrinter node has group 'interactable'."""
    scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
    nodes = scene.get_nodes_by_group("interactable")
    assert len(nodes) >= 1
    assert any(n.name == "FDMPrinter" for n in nodes)


def test_get_nodes_by_group_printer_slots():
    """main.tscn has 6 printer_slots group members."""
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    nodes = scene.get_nodes_by_group("printer_slots")
    assert len(nodes) == 6


def test_get_nodes_by_group_empty_result():
    """Querying a nonexistent group returns an empty list."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    assert scene.get_nodes_by_group("nonexistent_group") == []


# --- get_nodes_by_type ---

def test_get_nodes_by_type_marker3d():
    """main.tscn contains 35 Marker3D nodes."""
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    nodes = scene.get_nodes_by_type("Marker3D")
    assert len(nodes) == 35


def test_get_nodes_by_type_no_match():
    """Querying a type not present returns an empty list."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    assert scene.get_nodes_by_type("Camera3D") == []


# --- get_all_scripts ---

def test_get_all_scripts_returns_res_paths():
    """main.tscn get_all_scripts returns unique res:// paths."""
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    scripts = scene.get_all_scripts()
    assert len(scripts) > 0
    for s in scripts:
        assert s.startswith("res://")
    # Should be unique
    assert len(scripts) == len(set(scripts))


def test_get_all_scripts_worker():
    """worker.tscn has exactly one script: res://scripts/entities/worker.gd."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    scripts = scene.get_all_scripts()
    assert scripts == ["res://scripts/entities/worker.gd"]


def test_get_all_scripts_handles_missing_ext_resource():
    """If a node references a script ExtResource ID not in ext_resources,
    get_all_scripts skips it gracefully (no KeyError)."""
    scene = parse_tscn(str(FIXTURES / "worker.tscn"))
    # Artificially remove the ext_resource to test graceful handling
    scene.ext_resources.clear()
    scripts = scene.get_all_scripts()
    assert scripts == []


# --- get_all_instances ---

def test_get_all_instances_returns_res_paths():
    """main.tscn get_all_instances returns unique res:// paths."""
    scene = parse_tscn(str(FIXTURES / "main.tscn"))
    instances = scene.get_all_instances()
    assert len(instances) > 0
    for i in instances:
        assert i.startswith("res://")
    assert len(instances) == len(set(instances))


def test_get_all_instances_fdm_printer():
    """fdm_printer.tscn has 2 instanced scenes (printer model and spool)."""
    scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
    instances = scene.get_all_instances()
    assert len(instances) == 2
    assert "res://assets/models/printers/printer_standard.glb" in instances
    assert "res://assets/models/printers/filament_spool.glb" in instances


def test_get_all_instances_handles_missing_ext_resource():
    """If an instance references an ExtResource ID not in ext_resources,
    get_all_instances skips it gracefully."""
    scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
    scene.ext_resources.clear()
    instances = scene.get_all_instances()
    assert instances == []
```

---

## Implementation Details

### Pass 2: Tree Reconstruction

After Pass 1 populates `scene.nodes` (flat list) and `scene.ext_resources`, add a tree reconstruction step inside `parse_tscn()`. This is called at the end of the function, after the state machine loop has finished.

#### Algorithm

1. **Identify the root node.** The root is the node whose `parent` attribute is an empty string `""`. In `.tscn` files, the root node's `[node]` header has no `parent=` attribute at all. During Pass 1, such a node should have `parent=""` assigned.

2. **Build a path-to-node lookup.** Walk the flat node list and compute each node's full path:
   - The root node's path is simply its `name` (e.g., `"Main"`).
   - For a non-root node with `parent="."`, its full path is `{root_name}/{node_name}` (e.g., `"Main/Building"`).
   - For a non-root node with `parent="A/B"`, its full path is `{root_name}/{A/B}/{node_name}` (e.g., `"Main/Building/Floors/ReceptionFloor"`).
   - Store these in a `dict[str, TscnNode]` mapping full path to node.

3. **Link children to parents.** For each non-root node, compute the parent's full path:
   - If `parent == "."`, the parent path is `{root_name}`.
   - If `parent == "A/B"`, the parent path is `{root_name}/{A/B}`.
   - Look up the parent node in the path map and append the current node to `parent_node.children`.

4. **Set `scene.root`** to the root node.

5. **Handle edge cases:**
   - If no root node is found (no node with `parent==""`) log a warning and leave `scene.root` as `None`.
   - If a parent path is not found in the lookup, log a warning and skip that node (do not crash).

#### Code location

This logic should be a private function (e.g., `_build_tree(nodes: list[TscnNode]) -> TscnNode | None`) called at the end of `parse_tscn()`, or inlined directly. The result is assigned to `scene.root`.

### Query Methods on TscnScene

Add these five methods to the `TscnScene` dataclass. They are instance methods that operate on the parsed data.

#### `find_node(path: str) -> TscnNode | None`

Traverse the tree from root to find a node by slash-separated path.

- **Empty string or root name:** Return `self.root`.
- **Single segment** (e.g., `"CollisionShape3D"`): Search `self.root.children` for a child with that name.
- **Multi-segment** (e.g., `"Building/LockedZones/PostProcessingZone"`): Split by `"/"`. Start at root. For each segment, search the current node's `children` list for a child whose `name` matches. If any segment fails to match, return `None`.
- **Special characters** in node names like `@CSGBox3D@106885` need no special handling -- just match by name string equality.

```python
def find_node(self, path: str) -> TscnNode | None:
    """Find a node by slash-separated path from the root.

    Args:
        path: Node path like 'Building/LockedZones/PostProcessingZone'.
              Empty string or root node name returns root.

    Returns:
        The matching TscnNode, or None if not found.
    """
    ...
```

#### `get_nodes_by_group(group: str) -> list[TscnNode]`

Filter the flat `self.nodes` list for nodes whose `groups` list contains the given group string.

```python
def get_nodes_by_group(self, group: str) -> list[TscnNode]:
    """Return all nodes that belong to the given group.

    Args:
        group: Group name to filter by.

    Returns:
        List of matching nodes (may be empty).
    """
    ...
```

#### `get_nodes_by_type(type_name: str) -> list[TscnNode]`

Filter the flat `self.nodes` list for nodes whose `type` field equals `type_name`.

```python
def get_nodes_by_type(self, type_name: str) -> list[TscnNode]:
    """Return all nodes of the given Godot type.

    Args:
        type_name: Godot class name like 'Marker3D', 'Node3D', etc.

    Returns:
        List of matching nodes (may be empty).
    """
    ...
```

#### `get_all_scripts() -> list[str]`

Collect unique script resource paths (`res://...`) from all nodes that have a `script` field set.

- For each node where `node.script is not None`, look up `self.ext_resources[node.script]`.
- If the ID exists, take `.path` from the `ExtResource`.
- If the ID does not exist in `ext_resources`, **skip silently** (do not raise `KeyError`).
- Return a deduplicated list of paths. Preserve first-occurrence order.

```python
def get_all_scripts(self) -> list[str]:
    """Collect unique script resource paths from all nodes.

    Returns:
        List of unique res:// paths for scripts. Missing ext_resource
        IDs are silently skipped.
    """
    ...
```

#### `get_all_instances() -> list[str]`

Collect unique instanced scene paths from all nodes that have an `instance` field set.

- Same pattern as `get_all_scripts`: look up `self.ext_resources[node.instance]`, skip missing IDs.
- Return a deduplicated list of `res://` paths in first-occurrence order.

```python
def get_all_instances(self) -> list[str]:
    """Collect unique instanced scene resource paths from all nodes.

    Returns:
        List of unique res:// paths for instanced scenes. Missing
        ext_resource IDs are silently skipped.
    """
    ...
```

### Graceful Error Handling

All query methods must handle missing data without raising exceptions:

- `find_node` returns `None` if `self.root is None` or if any path segment fails to match.
- `get_nodes_by_group` and `get_nodes_by_type` simply return empty lists if nothing matches.
- `get_all_scripts` and `get_all_instances` use `self.ext_resources.get(id)` instead of `self.ext_resources[id]` to avoid `KeyError`. If the resource is not found, that node is skipped.

### Deduplication Pattern for Scripts and Instances

Use a `seen` set alongside a result list to preserve insertion order while deduplicating:

```python
seen: set[str] = set()
result: list[str] = []
for node in self.nodes:
    if node.script is not None:
        resource = self.ext_resources.get(node.script)
        if resource is not None and resource.path not in seen:
            seen.add(resource.path)
            result.append(resource.path)
return result
```

This ensures deterministic ordering (file order) and uniqueness.

---

## Key Data from Fixtures (for test validation)

These are the concrete values an implementer can use to verify correctness:

### worker.tscn (4 nodes)
- Root: `Worker` (type `CharacterBody3D`), 3 children: `CollisionShape3D`, `WorkerModel`, `CarryPoint`
- `Worker` has script = `ExtResource("1_qf3b7")`, which maps to `res://scripts/entities/worker.gd`
- No instances in this scene

### fdm_printer.tscn (6 nodes)
- Root: `FDMPrinter` (type `Node3D`), groups: `["interactable"]`
- 4 direct children of root: `StaticBody3D`, `PrinterModel`, `FilamentSpool`, `WorkPoint`
- `StaticBody3D` has 1 child: `CollisionShape3D`
- `PrinterModel` has `instance=ExtResource("2_model")` mapping to `res://assets/models/printers/printer_standard.glb`
- `FilamentSpool` has `instance=ExtResource("3_spool")` mapping to `res://assets/models/printers/filament_spool.glb`
- `PrinterModel` has no explicit `type` attribute (instanced nodes often omit type)

### main.tscn (876 nodes)
- Root: `Main` (type `Node3D`)
- 68 ext_resources, 19 nodes with scripts, 343 instance references
- 6 nodes in group `"printer_slots"`, 35 nodes of type `Marker3D`
- Deep nesting example: `Main/Building/LockedZones/PostProcessingZone/PostProcessingFloor/FloorTile_0_0` (5 levels below root)
- Node `@CSGBox3D@106885` has special characters in name but is a valid direct child of root
- `ResearchDesk` node (in `RDLabZone`) has `metadata/station_type = "desk"` and groups `["interactable", "research_stations"]`

---

## Checklist

1. Write all test stubs in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_tscn_parser.py` (the hierarchy and query tests listed above).
2. Implement `_build_tree()` (or equivalent) in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py`. Call it at the end of `parse_tscn()` after the state machine loop.
3. Add `find_node()` method to `TscnScene`.
4. Add `get_nodes_by_group()` method to `TscnScene`.
5. Add `get_nodes_by_type()` method to `TscnScene`.
6. Add `get_all_scripts()` method to `TscnScene`.
7. Add `get_all_instances()` method to `TscnScene`.
8. Run `pytest tests/test_parsers/test_tscn_parser.py` and verify all hierarchy and query tests pass.