# Sprint 1 — TSCN Parser Research

## Part 1: Codebase Analysis

### Project Structure
```
src/godotiq/
├── __init__.py              # Package docstring
├── server.py                # FastMCP server with godotiq_ping tool
├── config.py                # .godotiq.json loader (GodotIQConfig dataclass)
├── parsers/
│   ├── __init__.py
│   ├── tscn_parser.py       # PLACEHOLDER: TscnNode, TscnScene, parse_tscn()
│   ├── gd_parser.py         # PLACEHOLDER: GdSignal, GdFunction, GdScript, parse_gd()
│   └── tres_parser.py       # PLACEHOLDER: parse_tres()
├── cache/
│   └── file_cache.py        # FileCache with CacheEntry, is_stale/get/put/invalidate/clear
└── tools/                   # 9 empty category packages
```

### Existing Parser Placeholders (tscn_parser.py)
```python
@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")
```

**Key pattern**: `parse_tscn(content: str)` takes raw text, not file path. Uses `from __future__ import annotations`.

### Import Conventions
- `from godotiq.parsers.tscn_parser import TscnNode, TscnScene, parse_tscn`
- Package installed via `pip install -e ".[dev]"`
- Python 3.10+, only runtime dep: `mcp>=1.0.0`

### FileCache Integration Pattern
```python
cache = FileCache()
cached = cache.get(file_path)
if cached is None:
    content = open(file_path).read()
    parsed = parse_tscn(content)
    cache.put(file_path, parsed)
```

### Testing Setup
- pytest + pytest-asyncio, `asyncio_mode = "auto"`
- conftest.py has `project_root` and `tmp_config` fixtures
- `tests/test_parsers/` ready (empty, has `__init__.py`)
- `tests/fixtures/` contains 4 .tscn files: worker.tscn, fdm_printer.tscn, filament_spool_item_new.tscn, main.tscn (4711 lines)

### Existing Test Expectations (Sprint 0)
The integration test (`test_scaffolding_int.py`) imports `TscnNode, TscnScene, parse_tscn` — any changes to these names/signatures would break Sprint 0 tests.

---

## Part 2: Godot .tscn File Format Specification

### Sources
- [TSCN file format — Godot 4.4 docs](https://docs.godotengine.org/en/4.4/contributing/development/file_formats/tscn.html)
- [Godot variant.h source](https://github.com/godotengine/godot/blob/master/core/variant/variant.h)
- [resource_format_text.cpp](https://github.com/godotengine/godot/blob/master/scene/resources/resource_format_text.cpp)
- [godot-resource-parser (TypeScript)](https://github.com/fernforestgames/godot-resource-parser)

### File Structure Overview

Five ordered section types. Headers are `[section_type key=value ...]`. Properties follow as `key = value` lines until next `[` header. Comments start with `;`.

### Format Versions
| Format | Godot Version | IDs |
|--------|--------------|-----|
| `format=2` | Godot 3.x | Integer IDs: `id=1`, refs as `ExtResource(1)` |
| `format=3` | Godot 4.0+ | String IDs: `id="SphereShape3D_abc"`, refs as `ExtResource("id")` |

**Note**: As of Godot 4.4, `format=3` is still current. The user's spec mentions `format=4` in main.tscn — this may be a custom/future version or the user's terminology for a variant.

### Section 1: File Header `[gd_scene]`
```
[gd_scene load_steps=4 format=3 uid="uid://cecaux1sm7mo0"]
```
- `format`: Required. 3 for Godot 4.x
- `load_steps`: Optional. Total resources + 1
- `uid`: Optional. `uid://` prefix string

### Section 2: External Resources `[ext_resource]`
```
[ext_resource type="Script" uid="uid://abc" path="res://script.gd" id="1_abc"]
```
- `type`: Required (Script, PackedScene, Texture2D, etc.)
- `path`: Required (`res://` path)
- `id`: Required (string reference ID)
- `uid`: Optional

### Section 3: Sub Resources `[sub_resource]`
```
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_abc"]
radius = 0.5
height = 3.0
```
- `type`: Required
- `id`: Required
- Followed by property key=value pairs

### Section 4: Nodes `[node]`
```
[node name="Player" type="CharacterBody3D"]
[node name="Child" type="Node3D" parent="."]
[node name="Instanced" parent="." instance=ExtResource("scene_id")]
```
- `name`: Required
- `type`: Conditional (absent if instanced scene root)
- `parent`: Absent for root node, `"."` for direct children, path like `"Arm/Hand"`
- `instance`: Optional, `ExtResource("id")`
- `groups`: Optional, `["group1", "group2"]`
- `unique_id`: Optional integer
- `owner`, `index`, `instance_placeholder`: Optional

### Section 5: Connections `[connection]`
```
[connection signal="pressed" from="Button" to="." method="_on_pressed" flags=0 binds=[42]]
```
- `signal`, `from`, `to`, `method`: Required
- `flags`, `binds`, `unbinds`: Optional

### Section 6: Editable Instances `[editable]`
```
[editable path="EnemyInstance"]
```
- Only `path` attribute

### Complete Godot 4 Value Types

**Atomic**: `null`, `bool` (true/false), `int`, `float`, `String` (double-quoted)

**Math types**:
| Type | Format |
|------|--------|
| `Vector2(x, y)` | 2 floats |
| `Vector2i(x, y)` | 2 ints |
| `Vector3(x, y, z)` | 3 floats |
| `Vector3i(x, y, z)` | 3 ints |
| `Vector4(x, y, z, w)` | 4 floats |
| `Color(r, g, b, a)` | 4 floats RGBA |
| `Rect2(x, y, w, h)` | 4 floats |
| `AABB(x, y, z, w, h, d)` | 6 floats |
| `Transform2D(...)` | 6 floats |
| `Transform3D(...)` | 12 floats |
| `Basis(...)` | 9 floats |
| `Plane(x, y, z, d)` | 4 floats |
| `Quaternion(x, y, z, w)` | 4 floats |

**References**: `ExtResource("id")`, `SubResource("id")`

**String-like**: `StringName(&"name")`, `NodePath("path")`

**Collections**: `Array(...)`, typed `Array[int](...)`, `Dictionary(...)`

**Packed arrays**: `PackedByteArray(...)`, `PackedFloat32Array(...)`, `PackedVector2Array(...)`, etc.

### Groups Encoding
Single-line format (Godot 4+):
```
[node name="Enemy" type="Node3D" parent="." groups=["enemy", "damageable"]]
```

---

## Part 3: Transform3D Euler Angle Decomposition

### Sources
- [Godot basis.cpp source](https://github.com/godotengine/godot/blob/master/core/math/basis.cpp)
- [Godot basis.h source](https://github.com/godotengine/godot/blob/master/core/math/basis.h)
- [Fix Basis::get_euler PR #102144](https://github.com/godotengine/godot/pull/102144)

### Transform3D Memory Layout
```
Transform3D(b00, b10, b20, b01, b11, b21, b02, b12, b22, ox, oy, oz)
```
- First 9 values: 3x3 basis matrix stored **by columns** (col0, col1, col2)
- Last 3 values: origin/translation (x, y, z)

The basis matrix in row-major form:
```
| b00  b01  b02 |
| b10  b11  b12 |
| b20  b21  b22 |
```

### Godot's YXZ Convention — CONFIRMED
Default rotation order: **YXZ intrinsic** (compose as Y * X * Z).
`get_euler(EulerOrder::YXZ)` returns `(x_angle, y_angle, z_angle)` in radians.

### Exact Decomposition Algorithm (from basis.cpp)

```python
import math

EPSILON = 0.00000025  # ~3 ULPs for float32 (per PR #102144)

def basis_to_euler_yxz(rows):
    """rows[row][col] — 3x3 rotation matrix, row-major."""
    m12 = rows[1][2]
    if m12 < (1.0 - EPSILON):
        if m12 > -(1.0 - EPSILON):
            x = math.asin(-m12)
            y = math.atan2(rows[0][2], rows[2][2])
            z = math.atan2(rows[1][0], rows[1][1])
        else:
            # Gimbal lock: pitch ~= +90°
            x = math.pi * 0.5
            y = math.atan2(rows[0][1], rows[0][0])
            z = 0.0
    else:
        # Gimbal lock: pitch ~= -90°
        x = -math.pi * 0.5
        y = -math.atan2(rows[0][1], rows[0][0])
        z = 0.0
    return (x, y, z)
```

### Scale Extraction
Scale = length of each column vector of the basis matrix:
```python
sx = sqrt(b00² + b10² + b20²)  # column 0 length
sy = sqrt(b01² + b11² + b21²)  # column 1 length
sz = sqrt(b02² + b12² + b22²)  # column 2 length
```

To get pure rotation: divide each column by its scale, then apply Euler decomposition.

Check determinant sign for negative scale detection (odd number of negatively-scaled axes).

### Floating Point Noise
- Values like `-3.059797e-08` should be treated as `0`
- Clean threshold: `abs(v) < 1e-7` → `0.0` (for Godot's float32 precision)
- Apply cleanup to basis matrix **before** decomposition
- Post-decomposition: snap angles near 0 to 0 (threshold ~`1e-10` radians)

---

## Part 4: Python State-Machine Parser Patterns

### Sources
- [godot-resource-parser (TypeScript)](https://github.com/fernforestgames/godot-resource-parser)
- [godot-parser (Python)](https://pypi.org/project/godot-parser/)
- [Python FSM tutorial](https://brianray-7981.medium.com/tutorial-write-a-finite-state-machine-to-parse-a-custom-language-in-pure-python-1c11ade9bd43)

### Recommended Architecture

Line-by-line state machine with pre-compiled regex for value tokenization.

```python
from enum import Enum, auto

class ParserState(Enum):
    INITIAL = auto()
    FILE_HEADER = auto()
    EXT_RESOURCE = auto()
    SUB_RESOURCE = auto()
    NODE = auto()
    CONNECTION = auto()
    EDITABLE = auto()
```

### Regex vs String Methods

| Method | Speed | Use for |
|--------|-------|---------|
| `str.startswith('[')` | Fastest | Section header detection |
| `str.split('=', 1)` | Fast | Key-value separation |
| Pre-compiled `re.match()` | Moderate | Value type detection (`Vector3(...)`, `Color(...)`) |
| `re.split()` | Slower | Avoid in hot loops |

**Pre-compile all patterns at module load time:**
```python
RE_VECTOR3 = re.compile(r'^Vector3\(\s*([^)]+)\)$')
RE_SECTION_HEADER = re.compile(r'^\[(\w+)\s*(.*)\]$')
RE_HEADER_ATTR = re.compile(r'(\w+)=("(?:[^"\\]|\\.)*"|[^\s]+)')
```

### Key Best Practices
1. Parse line-by-line for headers and properties
2. Handle multiline values (unclosed brackets/parentheses)
3. Separate tokenization from interpretation (two-pass)
4. Track line numbers for error messages
5. Large `PackedByteArray` lines: avoid regex, use specialized fast paths
6. `current_section` variable + handler dispatch dict is sufficient
