I now have all the context I need. Let me produce the section content.

# Section 2: Dataclass Definitions

## Overview

This section extends `src/godotiq/parsers/tscn_parser.py` with all the dataclass definitions needed by the TSCN parser. It adds four new dataclasses (`SceneHeader`, `ExtResource`, `SubResource`, `Connection`), extends the existing `TscnNode` with many new fields, replaces the existing `TscnScene` structure, and changes the `parse_tscn` signature from content-based to path-based. No parsing logic is implemented here -- just the data model and the updated function signature stub.

## Dependencies

- **No dependencies on other sections.** This section can be implemented in parallel with section-01 (value parser).
- **Blocks:** section-03 (state machine), section-04 (tree/queries), section-05 (fixtures/unit tests).

## File to Modify

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

This file currently contains minimal placeholder dataclasses (`TscnNode`, `TscnScene`) and a `parse_tscn` stub that raises `NotImplementedError`. All changes go in this single file.

## Tests First

Write these tests before implementing the dataclasses. Place them in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_dataclasses.py`.

```python
# /Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_dataclasses.py
"""Tests for TSCN parser dataclass definitions."""

import pytest

from godotiq.parsers.tscn_parser import (
    SceneHeader,
    ExtResource,
    SubResource,
    Connection,
    TscnNode,
    TscnScene,
    parse_tscn,
)


class TestTscnNodeDefaults:
    """TscnNode can be instantiated with all new fields having sensible defaults."""

    def test_tscn_node_all_new_fields_have_defaults(self) -> None:
        """A TscnNode created with only name/type should have all other fields
        defaulted: parent='', instance=None, unique_id=None, groups=[],
        script=None, position=None, rotation=None, scale=None, metadata={},
        properties={}, children=[]."""
        node = TscnNode(name="TestNode", type="Node3D")
        assert node.name == "TestNode"
        assert node.type == "Node3D"
        assert node.parent == ""
        assert node.instance is None
        assert node.unique_id is None
        assert node.groups == []
        assert node.script is None
        assert node.position is None
        assert node.rotation is None
        assert node.scale is None
        assert node.metadata == {}
        assert node.properties == {}
        assert node.children == []

    def test_tscn_node_groups_defaults_to_empty_list(self) -> None:
        """groups field defaults to an empty list, not None."""
        node = TscnNode()
        assert isinstance(node.groups, list)
        assert node.groups == []

    def test_tscn_node_metadata_defaults_to_empty_dict(self) -> None:
        """metadata field defaults to an empty dict."""
        node = TscnNode()
        assert isinstance(node.metadata, dict)
        assert node.metadata == {}


class TestSceneHeader:
    """SceneHeader can be instantiated with format, load_steps, uid."""

    def test_scene_header_instantiation(self) -> None:
        header = SceneHeader(format=3, load_steps=5, uid="uid://abc123")
        assert header.format == 3
        assert header.load_steps == 5
        assert header.uid == "uid://abc123"

    def test_scene_header_optional_fields_default_none(self) -> None:
        header = SceneHeader(format=3)
        assert header.load_steps is None
        assert header.uid is None


class TestExtResource:
    """ExtResource can be instantiated with id, type, path, uid."""

    def test_ext_resource_instantiation(self) -> None:
        res = ExtResource(id="1_abc", type="Script", path="res://player.gd")
        assert res.id == "1_abc"
        assert res.type == "Script"
        assert res.path == "res://player.gd"
        assert res.uid is None

    def test_ext_resource_with_uid(self) -> None:
        res = ExtResource(id="2", type="Texture2D", path="res://icon.png", uid="uid://xyz")
        assert res.uid == "uid://xyz"


class TestSubResource:
    """SubResource can be instantiated with id, type, properties."""

    def test_sub_resource_instantiation(self) -> None:
        sub = SubResource(id="BoxShape3D_1", type="BoxShape3D", properties={"size": (1.0, 2.0, 3.0)})
        assert sub.id == "BoxShape3D_1"
        assert sub.type == "BoxShape3D"
        assert sub.properties == {"size": (1.0, 2.0, 3.0)}

    def test_sub_resource_properties_default_empty(self) -> None:
        sub = SubResource(id="1", type="SomeType")
        assert sub.properties == {}


class TestConnection:
    """Connection can be instantiated with signal, from_node, to_node, method."""

    def test_connection_instantiation(self) -> None:
        conn = Connection(signal="pressed", from_node="Button", to_node=".", method="_on_button_pressed")
        assert conn.signal == "pressed"
        assert conn.from_node == "Button"
        assert conn.to_node == "."
        assert conn.method == "_on_button_pressed"
        assert conn.flags == 0
        assert conn.binds == []

    def test_connection_flags_and_binds_defaults(self) -> None:
        conn = Connection(signal="s", from_node="a", to_node="b", method="m")
        assert conn.flags == 0
        assert isinstance(conn.binds, list)
        assert conn.binds == []


class TestTscnScene:
    """TscnScene has ext_resources, sub_resources, root, nodes, connections fields."""

    def test_tscn_scene_has_all_fields(self) -> None:
        scene = TscnScene()
        assert isinstance(scene.ext_resources, dict)
        assert isinstance(scene.sub_resources, dict)
        assert scene.root is None
        assert isinstance(scene.nodes, list)
        assert isinstance(scene.connections, list)
        # header should have a default
        assert scene.header is not None


class TestParseTscnSignature:
    """parse_tscn raises FileNotFoundError for nonexistent path (new path-based API)."""

    def test_parse_tscn_raises_file_not_found_for_bad_path(self) -> None:
        with pytest.raises(FileNotFoundError):
            parse_tscn("/nonexistent/path/to/scene.tscn")
```

Make sure `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/__init__.py` exists (it does, from Sprint 0).

## Implementation Details

### Current State of the File

The existing `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py` contains:

```python
from __future__ import annotations
from dataclasses import dataclass, field

@dataclass
class TscnNode:
    name: str = ""
    type: str = ""
    properties: dict[str, object] = field(default_factory=dict)
    children: list[TscnNode] = field(default_factory=list)

@dataclass
class TscnScene:
    nodes: list[TscnNode] = field(default_factory=list)
    resources: dict[str, object] = field(default_factory=dict)

def parse_tscn(content: str) -> TscnScene:
    raise NotImplementedError("tscn parser not yet implemented")
```

### Changes Required

#### 1. Add New Dataclasses

Add the following four new dataclasses before the existing `TscnNode` class. Each uses `@dataclass` from the stdlib `dataclasses` module, with `field(default_factory=...)` for mutable defaults.

**`SceneHeader`** -- Parsed `[gd_scene]` line:
- `format: int` -- scene format version (3 or higher)
- `load_steps: int | None = None` -- optional load steps count
- `uid: str | None = None` -- optional scene UID

**`ExtResource`** -- One `[ext_resource]` entry:
- `id: str` -- reference ID used by `ExtResource("id")` in property values
- `type: str` -- resource class name (e.g., "Script", "PackedScene", "Texture2D")
- `path: str` -- `res://` path to the resource file
- `uid: str | None = None` -- optional resource UID

**`SubResource`** -- One `[sub_resource]` block:
- `id: str` -- reference ID used by `SubResource("id")` in property values
- `type: str` -- resource class name (e.g., "BoxShape3D", "StandardMaterial3D")
- `properties: dict[str, object] = field(default_factory=dict)` -- parsed property key-value pairs

**`Connection`** -- One `[connection]` entry:
- `signal: str` -- signal name
- `from_node: str` -- node path of the emitter
- `to_node: str` -- node path of the receiver
- `method: str` -- method name called on the receiver
- `flags: int = 0` -- connection flags (default 0)
- `binds: list[object] = field(default_factory=list)` -- bound arguments (default empty)

#### 2. Extend `TscnNode`

Preserve the existing fields (`name`, `type`, `properties`, `children`) and add these new fields with defaults:

- `parent: str = ""` -- parent path string ("." for root children, "" for root itself)
- `instance: str | None = None` -- ExtResource ID if this node is an instanced scene
- `unique_id: int | None = None` -- unique node ID within the scene
- `groups: list[str] = field(default_factory=list)` -- group memberships
- `script: str | None = None` -- ExtResource ID if the node has a script attached
- `position: tuple[float, float, float] | None = None` -- from Transform3D decomposition
- `rotation: tuple[float, float, float] | None = None` -- YXZ Euler angles in radians
- `scale: tuple[float, float, float] | None = None` -- per-axis scale factors
- `metadata: dict[str, object] = field(default_factory=dict)` -- properties with `metadata/` prefix, stored without the prefix

The field order in the dataclass should place required-like fields first (`name`, `type`) and optional/defaulted fields after. Since all fields have defaults, any order works, but group logically: identity fields, then hierarchy fields, then spatial fields, then data fields.

#### 3. Replace `TscnScene`

Remove the old `resources: dict[str, object]` field. The new `TscnScene` has:

- `header: SceneHeader = field(default_factory=lambda: SceneHeader(format=3))` -- default to format 3
- `ext_resources: dict[str, ExtResource] = field(default_factory=dict)` -- keyed by resource ID string
- `sub_resources: dict[str, SubResource] = field(default_factory=dict)` -- keyed by resource ID string
- `root: TscnNode | None = None` -- the tree root (node without parent)
- `nodes: list[TscnNode] = field(default_factory=list)` -- flat list of all nodes in file order
- `connections: list[Connection] = field(default_factory=list)` -- signal connections

Query methods (`find_node`, `get_nodes_by_group`, `get_nodes_by_type`, `get_all_scripts`, `get_all_instances`) will be added in section-04. For now, `TscnScene` is a plain dataclass with only data fields.

#### 4. Change `parse_tscn` Signature

The function signature changes from:

```python
def parse_tscn(content: str) -> TscnScene:
```

to:

```python
def parse_tscn(path: str) -> TscnScene:
```

The stub implementation should raise `FileNotFoundError` when the file does not exist, rather than `NotImplementedError`. The minimal stub for this section is:

```python
def parse_tscn(path: str) -> TscnScene:
    """Parse a .tscn file from disk into a TscnScene.

    Args:
        path: Filesystem path to the .tscn file.

    Returns:
        Parsed scene representation.

    Raises:
        FileNotFoundError: If the file does not exist.
    """
    import os
    if not os.path.isfile(path):
        raise FileNotFoundError(f"Scene file not found: {path}")
    # Actual parsing implemented in section-03
    raise NotImplementedError("tscn parser not yet implemented")
```

This provides the new path-based API while still raising `NotImplementedError` for the actual parsing (which section-03 will implement). The `FileNotFoundError` check comes first so that the test in this section passes.

### Important: Backward Compatibility Impact

The signature change from `parse_tscn(content: str)` to `parse_tscn(path: str)` breaks the existing Sprint 0 test in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_server.py`:

```python
class TestParserStubs:
    def test_parser_stubs_raise(self) -> None:
        with pytest.raises(NotImplementedError):
            parse_tscn("")  # This will now raise FileNotFoundError, not NotImplementedError
```

This test must be updated in section-06 (Integration Tests & Sprint 0 Fixes). Until that section is implemented, this test will fail. This is expected and documented. The other two stubs (`parse_gd`, `parse_tres`) remain unchanged.

The import in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_scaffolding_int.py` (`from godotiq.parsers.tscn_parser import TscnNode, TscnScene, parse_tscn`) will continue to work since we are not removing any exported names -- we are extending them.

### Mutable Default Safety

All mutable default values (`list`, `dict`) must use `field(default_factory=...)` to avoid the shared-mutable-default pitfall. This applies to:
- `TscnNode.properties`, `TscnNode.children`, `TscnNode.groups`, `TscnNode.metadata`
- `SubResource.properties`
- `Connection.binds`
- `TscnScene.ext_resources`, `TscnScene.sub_resources`, `TscnScene.nodes`, `TscnScene.connections`
- `TscnScene.header` (uses `default_factory=lambda: SceneHeader(format=3)`)

### Complete File Structure After This Section

The resulting `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py` should have this structure (showing signatures and docstrings only, not full implementations):

```python
"""Parser for Godot .tscn scene files.

Extracts node hierarchy, transforms, resources, and signal connections
from Godot's text scene format.
"""

from __future__ import annotations

import os
from dataclasses import dataclass, field


@dataclass
class SceneHeader:
    """Parsed [gd_scene] header line."""
    format: int = 3
    load_steps: int | None = None
    uid: str | None = None


@dataclass
class ExtResource:
    """One [ext_resource] entry referencing an external file."""
    id: str = ""
    type: str = ""
    path: str = ""
    uid: str | None = None


@dataclass
class SubResource:
    """One [sub_resource] block with inline properties."""
    id: str = ""
    type: str = ""
    properties: dict[str, object] = field(default_factory=dict)


@dataclass
class Connection:
    """One [connection] signal wiring entry."""
    signal: str = ""
    from_node: str = ""
    to_node: str = ""
    method: str = ""
    flags: int = 0
    binds: list[object] = field(default_factory=list)


@dataclass
class TscnNode:
    """A single node in a Godot scene tree."""
    # Identity
    name: str = ""
    type: str = ""
    # Hierarchy
    parent: str = ""
    children: list[TscnNode] = field(default_factory=list)
    # Scene instancing
    instance: str | None = None
    unique_id: int | None = None
    # Groups and script
    groups: list[str] = field(default_factory=list)
    script: str | None = None
    # Spatial (from Transform3D decomposition)
    position: tuple[float, float, float] | None = None
    rotation: tuple[float, float, float] | None = None
    scale: tuple[float, float, float] | None = None
    # Data
    properties: dict[str, object] = field(default_factory=dict)
    metadata: dict[str, object] = field(default_factory=dict)


@dataclass
class TscnScene:
    """Parsed representation of a .tscn file."""
    header: SceneHeader = field(default_factory=lambda: SceneHeader(format=3))
    ext_resources: dict[str, ExtResource] = field(default_factory=dict)
    sub_resources: dict[str, SubResource] = field(default_factory=dict)
    root: TscnNode | None = None
    nodes: list[TscnNode] = field(default_factory=list)
    connections: list[Connection] = field(default_factory=list)
    # Query methods added in section-04


def parse_tscn(path: str) -> TscnScene:
    """Parse a .tscn file from disk into a TscnScene.

    Args:
        path: Filesystem path to the .tscn file.

    Returns:
        Parsed scene representation.

    Raises:
        FileNotFoundError: If the file does not exist.
    """
    if not os.path.isfile(path):
        raise FileNotFoundError(f"Scene file not found: {path}")
    # Actual parsing implemented in section-03
    raise NotImplementedError("tscn parser not yet implemented")
```

### Exports

The following names must be importable from `godotiq.parsers.tscn_parser`:

- `SceneHeader` (new)
- `ExtResource` (new)
- `SubResource` (new)
- `Connection` (new)
- `TscnNode` (extended, existing name preserved)
- `TscnScene` (restructured, existing name preserved)
- `parse_tscn` (signature changed, existing name preserved)

No changes are needed to `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/__init__.py` since individual imports from the module path work directly.

## Verification

After implementing this section, run:

```bash
cd /Users/salvatorefarruggio/Desktop/godotiq
python -m pytest tests/test_parsers/test_dataclasses.py -v
```

All 12 tests in `test_dataclasses.py` should pass. The existing Sprint 0 test `test_parser_stubs_raise` in `tests/test_server.py` will fail for `parse_tscn` (it expects `NotImplementedError` but now gets `FileNotFoundError` for empty string path). This is expected and will be fixed in section-06.