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

# Section 3: State Machine Parser

## Overview

This section implements the core line-by-line state machine parser in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/tscn_parser.py`. The parser reads a `.tscn` file, classifies lines into sections, parses section headers and property lines, accumulates multiline values, and populates the dataclass structures defined in Section 2. It wires up the value parser from Section 1 for property value interpretation.

This is Pass 1 of the two-pass architecture. Pass 2 (tree reconstruction) and query methods are handled in Section 4.

## Dependencies

- **Section 1 (Value Parser Foundation):** The `parse_value` and `decompose_transform3d` functions from `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/value_parser.py` must be implemented. The state machine calls `parse_value()` on every property value and invokes `decompose_transform3d()` when a `Transform3D(...)` value is encountered on a node.
- **Section 2 (Dataclass Definitions):** The dataclasses `SceneHeader`, `ExtResource`, `SubResource`, `Connection`, extended `TscnNode`, and extended `TscnScene` must exist in `tscn_parser.py`. The `parse_tscn` signature must already be changed to `parse_tscn(path: str) -> TscnScene`.

## Tests First

Write these tests in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_tscn_parser.py` before implementing the parser. The fixture files live at `/Users/salvatorefarruggio/Desktop/godotiq/tests/fixtures/`. Note that `furniture_slot_placeholder.tscn` is created in Section 5; tests referencing it can be skipped until that fixture exists.

```python
# tests/test_parsers/test_tscn_parser.py

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

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


class TestHeaderParsing:
    """Verify the [gd_scene] header is correctly parsed."""

    def test_worker_header_format(self):
        """worker.tscn has format=3."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        assert scene.header.format == 3

    def test_main_header_format(self):
        """main.tscn has format=4."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        assert scene.header.format == 4

    def test_worker_header_uid(self):
        """worker.tscn header uid starts with 'uid://'."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        assert scene.header.uid is not None
        assert scene.header.uid.startswith("uid://")


class TestExtResourceParsing:
    """Verify [ext_resource] sections are parsed."""

    def test_fdm_printer_ext_resource_count(self):
        """fdm_printer.tscn has 3 ext_resources."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        assert len(scene.ext_resources) == 3

    def test_fdm_printer_ext_resource_contents(self):
        """fdm_printer.tscn ext_resource '1_iqbk5' is a Script."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        res = scene.ext_resources["1_iqbk5"]
        assert res.type == "Script"
        assert res.path == "res://scripts/entities/printer.gd"


class TestSubResourceParsing:
    """Verify [sub_resource] sections are parsed with their properties."""

    def test_worker_sub_resource_count(self):
        """worker.tscn has 1 sub_resource."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        assert len(scene.sub_resources) == 1

    def test_worker_sub_resource_properties(self):
        """worker.tscn sub_resource CapsuleShape3D has radius and height."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        sub = scene.sub_resources["CapsuleShape3D_qf3b7"]
        assert sub.type == "CapsuleShape3D"
        assert sub.properties["radius"] == 0.2
        assert sub.properties["height"] == 1.0


class TestBasicNodeParsing:
    """Verify [node] sections produce correct flat node lists."""

    def test_worker_node_count(self):
        """worker.tscn has 4 nodes."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        assert len(scene.nodes) == 4

    def test_worker_root_node(self):
        """worker.tscn root is 'Worker' of type 'CharacterBody3D'."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        root_nodes = [n for n in scene.nodes if n.parent == ""]
        assert len(root_nodes) == 1
        assert root_nodes[0].name == "Worker"
        assert root_nodes[0].type == "CharacterBody3D"

    def test_worker_root_has_script(self):
        """worker.tscn root node has a script reference."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        root = [n for n in scene.nodes if n.parent == ""][0]
        assert root.script == "1_qf3b7"


class TestGroupsParsing:
    """Verify groups attribute is parsed from [node] headers."""

    def test_fdm_printer_single_group(self):
        """fdm_printer.tscn FDMPrinter has group 'interactable'."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        node = [n for n in scene.nodes if n.name == "FDMPrinter"][0]
        assert "interactable" in node.groups

    def test_main_multiple_groups(self):
        """main.tscn filament_storage has groups 'material_store' and 'storage_points'."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        node = [n for n in scene.nodes if n.name == "filament_storage"][0]
        assert "material_store" in node.groups
        assert "storage_points" in node.groups


class TestTransformParsing:
    """Verify Transform3D values are decomposed into position/rotation/scale."""

    def test_worker_collision_shape_position(self):
        """worker.tscn CollisionShape3D position = (0, 0.5, 0)."""
        scene = parse_tscn(str(FIXTURES / "worker.tscn"))
        node = [n for n in scene.nodes if n.name == "CollisionShape3D"][0]
        assert node.position is not None
        assert node.position == pytest.approx((0.0, 0.5, 0.0))

    def test_fdm_printer_uniform_scale(self):
        """fdm_printer.tscn PrinterModel scale approx (0.35, 0.35, 0.35)."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        node = [n for n in scene.nodes if n.name == "PrinterModel"][0]
        assert node.scale is not None
        assert node.scale == pytest.approx((0.35, 0.35, 0.35))

    def test_main_non_uniform_scale(self):
        """main.tscn SouthWall_X2 has Y scale = 1.25."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        node = [n for n in scene.nodes if n.name == "SouthWall_X2"][0]
        assert node.scale is not None
        assert node.scale[1] == pytest.approx(1.25)

    def test_main_rotation_decomposition(self):
        """main.tscn wall with -90deg Y rotation (scientific notation in basis)."""
        # Transform3D(-4.371139e-08, 0, -1, 0, 1.25, 0, 1, 0, -4.371139e-08, ...)
        # This is a -90 degree Y rotation with Y scale 1.25
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        # Find one of the nodes with this transform pattern
        node = [n for n in scene.nodes if n.name == "SouthWall_X2_Window"][0]
        assert node.rotation is not None
        # Rotation should be approximately -pi/2 on Y axis
        import math
        assert node.rotation[1] == pytest.approx(-math.pi / 2, abs=1e-5)

    def test_main_scientific_notation_values(self):
        """Scientific notation values like -4.371139e-08 are cleaned properly."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        # A node with scientific notation in transform should have clean rotation
        node = [n for n in scene.nodes if n.name == "SouthWall_X2_Window"][0]
        # Z rotation should be 0 (not a tiny float noise value)
        assert node.rotation[2] == pytest.approx(0.0, abs=1e-6)


class TestInstanceAndScript:
    """Verify instance and script node attributes."""

    def test_fdm_printer_instance_node(self):
        """fdm_printer.tscn PrinterModel has instance ExtResource ref."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        node = [n for n in scene.nodes if n.name == "PrinterModel"][0]
        assert node.instance == "2_model"

    def test_fdm_printer_no_explicit_type(self):
        """fdm_printer.tscn PrinterModel has no explicit type (instanced)."""
        scene = parse_tscn(str(FIXTURES / "fdm_printer.tscn"))
        node = [n for n in scene.nodes if n.name == "PrinterModel"][0]
        assert node.type == ""


class TestPropertiesAndMetadata:
    """Verify custom properties and metadata/ prefix handling."""

    def test_main_locked_zone_properties(self):
        """main.tscn LockedZone (Storage) has zone_name, unlock_cost."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        node = [n for n in scene.nodes if n.name == "Storage"
                and n.parent == "Building/Furniture"][0]
        assert node.properties["zone_name"] == "Storage Zone"
        assert node.properties["unlock_cost"] == 2000

    def test_main_metadata_station_type(self):
        """main.tscn ResearchDesk has metadata/station_type = 'desk'."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        node = [n for n in scene.nodes if n.name == "ResearchDesk"][0]
        assert node.metadata["station_type"] == "desk"


class TestEdgeCases:
    """Verify edge cases: special names, stress counts."""

    def test_main_special_node_name(self):
        """main.tscn has node named '@CSGBox3D@106885'."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        node = [n for n in scene.nodes if n.name == "@CSGBox3D@106885"]
        assert len(node) == 1

    def test_main_node_count(self):
        """main.tscn has exactly 876 nodes."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        assert len(scene.nodes) == 876

    def test_main_ext_resource_count(self):
        """main.tscn has exactly 68 ext_resources."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        assert len(scene.ext_resources) == 68


class TestMultilineValues:
    """Verify multiline value accumulation works for sub_resources."""

    def test_main_sub_resource_with_surfaces(self):
        """main.tscn has sub_resources with multiline _surfaces arrays."""
        scene = parse_tscn(str(FIXTURES / "main.tscn"))
        # ArrayMesh sub_resources should have _surfaces parsed
        mesh_sub = scene.sub_resources.get("ArrayMesh_e1gva")
        assert mesh_sub is not None
        assert "_surfaces" in mesh_sub.properties
```

## Implementation Details

### File to Modify

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

### State Machine Design

The parser uses an enum-based state machine with the following states:

```python
import enum

class _ParserState(enum.Enum):
    INITIAL = "initial"
    FILE_HEADER = "file_header"
    EXT_RESOURCE = "ext_resource"
    SUB_RESOURCE = "sub_resource"
    NODE = "node"
    CONNECTION = "connection"
    EDITABLE = "editable"
```

State transitions happen whenever a new `[section_type ...]` header line is detected. The state tracks which section is currently being populated so that subsequent property lines are assigned to the correct data structure.

### Core Parser Function: `parse_tscn(path: str) -> TscnScene`

The `parse_tscn` function replaces the existing placeholder. It:

1. Opens the file at `path` with `encoding="utf-8"` and reads all lines.
2. Runs the state machine (Pass 1) to produce flat lists of parsed sections.
3. Returns a `TscnScene` with populated `header`, `ext_resources`, `sub_resources`, `nodes`, and `connections` fields.

Tree reconstruction (Pass 2, setting `root` and `children`) is deferred to Section 4. After Pass 1, `scene.root` remains `None` and `TscnNode.children` lists are empty.

### Section Header Detection

A section header is any line matching the pattern `[word ...]` or `[word]` where `word` is one of the known section types. Use a pre-compiled regex:

```python
import re

_SECTION_RE = re.compile(
    r"^\[(\w+)"  # captures section type keyword
)

_SECTION_TYPES = frozenset({
    "gd_scene", "gd_resource",
    "ext_resource", "sub_resource",
    "node", "connection", "editable",
})
```

A line starting with `[` is a section header only if the first word after `[` is one of `_SECTION_TYPES`. This is critical for multiline value accumulation: a line starting with `[` that does not match a known section type is a continuation line (e.g., array values in `_surfaces`).

### Section Header Attribute Parsing

After identifying the section type, parse the `key=value` pairs from the header line. The header line format is:

```
[section_type key1=value1 key2="value2" key3=123 ...]
```

Strip the leading `[` and trailing `]`, then the section type word. Parse the remaining string for `key=value` pairs. Attribute values can be:

- **Quoted strings:** `name="NodeName"` -- extract the content between quotes.
- **Bare integers:** `format=3`, `unique_id=12345` -- parse as `int`.
- **Array literals:** `groups=["enemy", "damageable"]` -- call the value parser to get a list.
- **ExtResource references:** `instance=ExtResource("2_model")` -- call the value parser, which returns the ref ID string.

Use a regex or manual scanning approach to extract key-value pairs. A practical approach is to use a regex that captures `key=` followed by either a quoted string, an array, a function call, or a bare value:

```python
_HEADER_ATTR_RE = re.compile(
    r'(\w+)='
    r'('
    r'"[^"]*"'           # quoted string
    r'|\[[^\]]*\]'       # array literal
    r'|\w+\([^)]*\)'     # function-like (ExtResource(...))
    r'|[^\s\]]*'         # bare value (int, etc.)
    r')'
)
```

### Property Line Parsing

Property lines appear below a section header and follow the format:

```
property_name = value
```

The delimiter is ` = ` (space-equals-space), not bare `=`. Split on the **first** occurrence of ` = ` to get the property name and value string. The value string is then passed to `parse_value()` from the value parser module.

Special handling for node properties:
- If the property name starts with `metadata/`, strip the prefix and store in `node.metadata` instead of `node.properties`.
- If the property name is `script`, call `parse_value()` on the value. The value parser returns the ExtResource ID string for `ExtResource("id")` values. Store this ID in `node.script`.
- If the property name is `transform` and the value starts with `Transform3D(`, call `decompose_transform3d()` from the value parser. Store the resulting position, rotation, and scale tuples on the node. Also store the raw value in `node.properties["transform"]` for completeness.
- All other properties go into `node.properties`.

### Multiline Value Accumulation

Some property values span multiple lines. The parser detects this through bracket/brace/parenthesis depth tracking:

1. After splitting a property line on ` = `, count the opening and closing brackets in the value portion: `(` `)`, `[` `]`, `{` `}`.
2. If depth is nonzero (more opens than closes), the value is incomplete. Continue reading subsequent lines, appending them to the accumulated value string.
3. Before appending a continuation line, check whether it is a new section header (starts with `[` and the first word is a known section type). If so, stop accumulating and process the new section header -- the multiline value was likely malformed, log a warning.
4. Once brackets are balanced (depth returns to zero), join all accumulated lines and pass the complete value to `parse_value()`.

This is essential for `main.tscn` which has 52+ sub_resource blocks with multiline `_surfaces` arrays like:

```
_surfaces = [{
"aabb": AABB(0, 0, -1, 1, 0.05, 1),
"format": 34896613377,
...
}]
```

### State Machine Loop

The main parsing loop iterates over all lines, maintaining:
- `state: _ParserState` -- current section type being parsed
- `current_sub_resource: SubResource | None` -- sub_resource being populated
- `current_node: TscnNode | None` -- node being populated
- `accumulating: bool` -- whether accumulating a multiline value
- `accum_lines: list[str]` -- accumulated lines for multiline value
- `accum_key: str` -- property key for the multiline value
- `depth: int` -- bracket/brace/paren nesting depth

The loop pseudocode:

```
for each line:
    strip trailing whitespace
    skip blank lines and comment lines (starting with ";")

    if accumulating multiline value:
        if line is a section header (starts with "[" + known section type):
            log warning about unfinished multiline value
            stop accumulating, process new section header
        else:
            append line to accum_lines
            update depth by scanning line for brackets
            if depth <= 0:
                join accum_lines, parse value, store on current section
                stop accumulating
            continue

    if line is a section header:
        extract section type and attributes
        transition state
        create new data object (ExtResource, SubResource, TscnNode, etc.)
        continue

    if line contains " = ":
        split on first " = " to get key and value_str
        count bracket depth in value_str
        if depth > 0:
            start multiline accumulation
            continue
        parse value_str with parse_value()
        store on current section object based on state
```

### Section-Specific Processing

**`gd_scene` (FILE_HEADER):**
Extract `format` (int), `load_steps` (int, optional), `uid` (string, optional) from header attributes. Create `SceneHeader`.

**`ext_resource` (EXT_RESOURCE):**
Extract `id`, `type`, `path`, `uid` from header attributes. Create `ExtResource` and add to `scene.ext_resources[id]`.

**`sub_resource` (SUB_RESOURCE):**
Extract `id`, `type` from header attributes. Create `SubResource`. Property lines below populate `sub_resource.properties`. Add to `scene.sub_resources[id]`.

**`node` (NODE):**
Extract `name`, `type` (default `""`), `parent` (default `""`), `instance`, `groups`, `unique_id` from header attributes. The `instance` value comes from `ExtResource("id")` in the header -- the value parser extracts just the ID string. The `groups` value comes from array parsing (e.g., `["interactable"]` becomes a list of strings). Create `TscnNode` with these values. Property lines are handled with the special rules described above (script, transform, metadata). Add to `scene.nodes`.

**`connection` (CONNECTION):**
Extract `signal`, `from` (mapped to `from_node`), `to` (mapped to `to_node`), `method`, `flags` (default 0), `binds` (default empty list) from header attributes. Create `Connection` and add to `scene.connections`. Connection sections typically have no property lines.

**`editable` (EDITABLE):**
These mark instanced scene nodes as editable. Extract the `path` attribute. No data structure is needed for these in the current implementation -- they can be silently skipped or stored if desired.

### Logging for Malformed Input

Use the `logging` module to issue warnings for malformed input rather than raising exceptions. The parser should never crash on unexpected input.

```python
import logging

logger = logging.getLogger(__name__)
```

Situations that trigger warnings:
- Unrecognized section type (skip the section)
- Property line without ` = ` delimiter (skip the line)
- Unfinished multiline value when a new section header is encountered
- Missing required header attributes (e.g., `ext_resource` without `id`)

### Bracket Depth Counting

A helper function counts bracket depth changes in a string:

```python
def _bracket_depth(text: str) -> int:
    """Return net bracket/brace/paren depth change for a line of text.

    Counts opening brackets ( [ { as +1, closing ) ] } as -1.
    Ignores brackets inside quoted strings.
    """
    ...
```

The function must ignore brackets inside quoted strings to avoid false depth changes from string values like `"name": "some[thing]"`. A simple approach: track whether currently inside a quoted string (toggle on `"`, handle `\"` escapes).

### Performance Considerations

- **PackedByteArray optimization:** Before applying any regex to a property value string, check if it starts with `Packed` (using `str.startswith`). If so, store the raw string without parsing. PackedByteArray values in `main.tscn` can be very long single lines; regex on these is wasteful.
- **Pre-compiled regexes:** All regex patterns used in the parser must be compiled once at module level, not inside the loop.
- **Target:** Parse `main.tscn` (4711 lines, 876 nodes) in under 200 ms.

### Import Structure

The parser module imports from the value parser:

```python
from godotiq.parsers.value_parser import parse_value, decompose_transform3d
```

### Key Implementation Notes

1. **The root node has no `parent` attribute in the `.tscn` file.** In the parsed `TscnNode`, this translates to `parent = ""` (empty string). All other nodes have a `parent` value -- `"."` means direct child of the root, `"A/B"` means child of the node at path `A/B` relative to root.

2. **The `type` field on nodes is optional.** Instanced nodes (those with an `instance` attribute) often omit the `type`. Default to empty string.

3. **The `unique_id` attribute in headers is a bare integer.** Parse it as `int`, not string.

4. **Header attribute `from` in connections conflicts with Python keyword.** When extracting connection attributes, map the header key `from` to the dataclass field `from_node`, and `to` to `to_node`.

5. **Array values in headers** (like `groups=["interactable", "research_stations"]`) need to be parsed by calling `parse_value()` on the array string, which returns a Python list. Do not attempt to parse these with a simple regex.

6. **The `format` in `gd_scene` headers can be 3 or 4** (or higher in future Godot versions). `worker.tscn` uses format 3, `main.tscn` uses format 4. The parser must handle both.

7. **Comments in `.tscn` files** start with `;` at the beginning of a line. These are rare but must be skipped.

8. **Encoding:** Always open files with `encoding="utf-8"`.

## Summary of What This Section Produces

After implementing this section, calling `parse_tscn(path)` on any `.tscn` file will:

- Parse the `[gd_scene]` header into `scene.header`
- Parse all `[ext_resource]` entries into `scene.ext_resources`
- Parse all `[sub_resource]` blocks (including multiline values) into `scene.sub_resources`
- Parse all `[node]` sections into `scene.nodes` as a flat list with correct `name`, `type`, `parent`, `instance`, `groups`, `unique_id`, `script`, `position`, `rotation`, `scale`, `metadata`, and `properties`
- Parse all `[connection]` entries into `scene.connections`
- Handle all multiline values correctly through bracket depth tracking
- Log warnings for malformed input without crashing

The `scene.root` field will remain `None` and `TscnNode.children` lists will be empty -- tree construction is Section 4's responsibility.