I now see the Sprint 0 test calls `parse_gd("")` which expects `NotImplementedError`. The plan says the signature will change to `parse_gd(path: str, ...)`. This means the Sprint 0 test will need to be updated or the behavior for an empty string path must raise `FileNotFoundError` (not `NotImplementedError`). The plan itself notes "Existing Sprint 0 tests reference parse_gd but only test that it exists and raises FileNotFoundError for bad paths -- they will work with the new path-based signature." But actually the test expects `NotImplementedError`, not `FileNotFoundError`. That's a concern for section-02 (the state machine core), not for this section.

Now I have complete understanding. Let me generate the section content.

# Section 03: Body Patterns -- IN_FUNCTION State and Cross-State Detection

## Overview

This section implements the body pattern detection logic that runs during the `IN_FUNCTION` state of the GDScript parser's state machine, plus the cross-state patterns (preload/load and is_instance_valid) that are scanned on every line regardless of state. It adds `.connect()` detection, `.emit()` detection, autoload reference detection, `is_instance_valid` detection, and `preload`/`load` detection to the parser.

**File to modify:** `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/parsers/gd_parser.py`

**Dependencies:** This section depends on section-01 (dataclasses and helpers) and section-02 (state machine core). It adds logic INTO the state machine built by section-02. Specifically:
- The dataclasses `GdSignalConnection`, `GdSignalEmission`, `GdAutoloadRef`, `GdPreloadRef` must exist (section-01).
- The helper functions `_find_matching_paren()`, `_split_args_depth_aware()`, `_count_emit_args()` must exist (section-01).
- The `_parse_gd_content()` function's `IN_FUNCTION` state must exist as a hook point (section-02).
- The `GdScript` container must have fields: `signal_connections`, `signal_emissions`, `autoload_refs`, `preload_refs`, `is_instance_valid_lines` (section-01).

## Tests -- Write These First

All tests go in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_parsers/test_gd_parser.py`. These tests call `_parse_gd_content()` directly with inline strings. They should be added alongside (not replacing) the tests from section-02.

### TestSignalConnections

```python
class TestSignalConnections:
    """Tests for .connect() detection inside function bodies."""

    def test_direct_connect(self) -> None:
        """Direct .connect() call: signal.connect(_handler) extracts signal_source and target."""
        # Input: func with "my_signal.connect(_handler)" in body
        # Assert: signal_connections[0].signal_source == "my_signal"
        # Assert: signal_connections[0].target == "_handler"
        # Assert: signal_connections[0].has_bind is False

    def test_connect_with_bind(self) -> None:
        """Connect with .bind(): signal.connect(_handler.bind(arg)) sets has_bind=True."""
        # Input: func with "my_signal.connect(_handler.bind(some_arg))" in body
        # Assert: has_bind is True
        # Assert: bind_args == "some_arg"

    def test_connect_with_dollar_node(self) -> None:
        """Connect on $Node reference: $Timer.timeout.connect(_on_timeout)."""
        # Input: func with "$Timer.timeout.connect(_on_timeout)" in body
        # Assert: signal_source == "$Timer.timeout"
        # Assert: target == "_on_timeout"

    def test_connect_with_lambda(self) -> None:
        """Connect with inline lambda: target starts with 'func'."""
        # Input: func with 'level_up.connect(func(_l, _u): fired[0] = true)' in body
        # Assert: target starts with "func"

    def test_depth_aware_connect_parsing(self) -> None:
        """Depth-aware parsing: trailing code after .connect() does not corrupt result."""
        # Input: func with "if signal.connect(handler) == OK:" in body
        # Assert: target == "handler" (not "handler) == OK:")

    def test_signal_source_captures_full_chain(self) -> None:
        """Signal source captures dotted chain: printer.print_started.connect(...)."""
        # Input: func with "printer.print_started.connect(_on_print_started)" in body
        # Assert: signal_source == "printer.print_started"

    def test_connect_line_number(self) -> None:
        """Line number on GdSignalConnection matches source line."""
        # Input: multi-line func with connect on a known line
        # Assert: signal_connections[0].line == expected_line
```

### TestSignalEmissions

```python
class TestSignalEmissions:
    """Tests for .emit() detection inside function bodies."""

    def test_emit_no_args(self) -> None:
        """Emit with no args: health_depleted.emit() has arg_count=0."""
        # Input: func with "health_depleted.emit()" in body
        # Assert: signal_emissions[0].signal_name == "health_depleted"
        # Assert: signal_emissions[0].arg_count == 0

    def test_emit_two_args(self) -> None:
        """Emit with 2 args: cash_changed.emit(cash, amount) has arg_count=2."""
        # Input: func with "cash_changed.emit(cash, amount)" in body
        # Assert: arg_count == 2

    def test_emit_three_args(self) -> None:
        """Emit with 3 args: any_print_failed.emit(id, product, reason) has arg_count=3."""
        # Input: func with "any_print_failed.emit(id, product, reason)" in body
        # Assert: arg_count == 3

    def test_emit_with_nested_parens(self) -> None:
        """Emit arg counting is depth-aware: nested parens don't inflate count."""
        # Input: func with "my_signal.emit(Vector2(1, 2), 3)" in body
        # Assert: arg_count == 2 (not 3)
```

### TestAutoloadRefs

```python
class TestAutoloadRefs:
    """Tests for autoload singleton reference detection."""

    def test_direct_autoload_with_known_autoloads(self) -> None:
        """Direct pattern: EconomyManager.spend_cash() detected when in known_autoloads."""
        # Input: func with "EconomyManager.spend_cash(100)" in body
        # Call _parse_gd_content with known_autoloads=["EconomyManager"]
        # Assert: autoload_refs[0].pattern == "direct"
        # Assert: autoload_refs[0].autoload_name == "EconomyManager"
        # Assert: autoload_refs[0].access == "spend_cash"

    def test_no_direct_detection_without_known_autoloads(self) -> None:
        """Direct refs not captured when known_autoloads is None."""
        # Input: func with "EconomyManager.spend_cash(100)" in body
        # Call _parse_gd_content with known_autoloads=None
        # Assert: no autoload_refs with pattern="direct"

    def test_has_node_pattern(self) -> None:
        """has_node('/root/ProgressionManager') detected as autoload ref."""
        # Input: func with 'has_node("/root/ProgressionManager")' in body
        # Assert: autoload_refs[0].pattern == "has_node"
        # Assert: autoload_refs[0].autoload_name == "ProgressionManager"

    def test_get_node_pattern(self) -> None:
        """get_node('/root/ProgressionManager') detected as autoload ref."""
        # Input: func with 'get_node("/root/ProgressionManager")' in body
        # Assert: autoload_refs[0].pattern == "get_node"

    def test_get_node_or_null_pattern(self) -> None:
        """get_node_or_null('...') detected as autoload ref."""
        # Input: func with 'get_tree().root.get_node_or_null("Main/Systems/PrinterSlotManager")' in body
        # Assert: autoload_refs[0].pattern == "get_node_or_null"

    def test_has_node_detected_without_known_autoloads(self) -> None:
        """has_node and get_node detected even when known_autoloads is None."""
        # Input: func with 'has_node("/root/SomeManager")' in body
        # Call _parse_gd_content with known_autoloads=None
        # Assert: still detected (structurally unambiguous)

    def test_multiple_autoload_refs_separate_entries(self) -> None:
        """Each autoload ref on a different line creates a separate GdAutoloadRef."""
        # Input: func with EconomyManager.foo() on line A and ResearchManager.bar() on line B
        # known_autoloads=["EconomyManager", "ResearchManager"]
        # Assert: len(autoload_refs) >= 2
```

### TestPreloadLoad

```python
class TestPreloadLoad:
    """Tests for preload() and load() reference detection."""

    def test_preload_detected(self) -> None:
        """preload('res://...') captured with is_preload=True."""
        # Input: 'const Scene = preload("res://scenes/player.tscn")'
        # Assert: preload_refs[0].path == "res://scenes/player.tscn"
        # Assert: preload_refs[0].is_preload is True

    def test_load_detected(self) -> None:
        """load('res://...') captured with is_preload=False."""
        # Input: 'var tex = load("res://data/items.tres")' inside a function body
        # Assert: preload_refs[0].is_preload is False

    def test_preload_in_const_creates_both(self) -> None:
        """preload in const line creates BOTH a GdVariable and a GdPreloadRef."""
        # Input: 'const MyScene = preload("res://scenes/player.tscn")'
        # Assert: variables has const named "MyScene"
        # Assert: preload_refs has entry with same path

    def test_preload_in_function_body(self) -> None:
        """preload inside a function body also captured."""
        # Input: func with 'var s = preload("res://foo.tscn")' in body
        # Assert: preload_refs not empty
```

### TestIsInstanceValid

```python
class TestIsInstanceValid:
    """Tests for is_instance_valid() detection."""

    def test_is_instance_valid_detected(self) -> None:
        """is_instance_valid() call line number recorded."""
        # Input: func with "if is_instance_valid(printer):" on line N
        # Assert: N in result.is_instance_valid_lines

    def test_multiple_is_instance_valid(self) -> None:
        """Multiple is_instance_valid calls record multiple line entries."""
        # Input: func with is_instance_valid on lines N and M
        # Assert: len(is_instance_valid_lines) >= 2
```

## Implementation Details

### Regex Patterns (Module-Level Constants)

The following regex patterns are compiled at module level (added to the patterns from section-01). These are used for body pattern detection.

**Connect pattern:** Does NOT use a single regex for the full match. Instead, find `.connect(` in the line text, then use `_find_matching_paren()` to depth-aware extract the argument content. The signal source is everything before `.connect(` on that line segment (stripped of leading whitespace and other non-identifier characters).

**Emit pattern:**
```python
RE_EMIT = re.compile(r"(\w+)\.emit\(")
```
Captures signal name as group 1. After matching, use `_find_matching_paren()` from the position of the opening `(` to extract the args string, then `_count_emit_args()` to count depth-aware arguments.

**Autoload direct pattern:** Compiled dynamically inside `_parse_gd_content()` when `known_autoloads` is provided and non-empty:
```python
if known_autoloads:
    alt = "|".join(re.escape(name) for name in known_autoloads)
    re_autoload_direct = re.compile(rf"\b({alt})\.(\w+)")
```
This creates a single alternation regex like `\b(EconomyManager|ResearchManager)\.(\w+)` compiled once per parse invocation.

**has_node pattern:**
```python
RE_HAS_NODE = re.compile(r'has_node\(\s*"(/root/([\w/]+))"\s*\)')
```

**get_node pattern:**
```python
RE_GET_NODE = re.compile(r'get_node\(\s*"(/root/([\w/]+))"\s*\)')
```
Important: this must NOT match `get_node_or_null`. The pattern specifically matches `get_node(` without `_or_null` following. One approach: use a negative lookahead or check the match is exactly `get_node(` and not `get_node_or_null(`.

**get_node_or_null pattern:**
```python
RE_GET_NODE_OR_NULL = re.compile(r'get_node_or_null\(\s*"([^"]+)"\s*\)')
```

**is_instance_valid pattern:**
```python
RE_IS_INSTANCE_VALID = re.compile(r"is_instance_valid\(")
```

**preload/load pattern:**
```python
RE_PRELOAD_LOAD = re.compile(r'(?:preload|load)\(\s*"([^"]+)"\s*\)')
```
This must also determine `is_preload` by checking which keyword matched. One approach: use two separate patterns or a named/numbered group. Simpler approach: check if `preload(` or `load(` appears in the matched substring.

```python
RE_PRELOAD = re.compile(r'preload\(\s*"([^"]+)"\s*\)')
RE_LOAD = re.compile(r'(?<!\w)load\(\s*"([^"]+)"\s*\)')
```
Using two separate patterns avoids ambiguity. The `load` pattern uses a negative lookbehind `(?<!\w)` to avoid matching `preload` as `load`.

### .connect() Detection Logic

Inside the `IN_FUNCTION` state handler, for each line:

1. Search for `.connect(` in the line.
2. If found at position `pos`:
   - Signal source: scan backwards from `pos` to extract the dotted expression before `.connect(`. This means taking the substring before `.connect(` and extracting the last dotted identifier chain (e.g., `printer.print_started` from `\tprinter.print_started.connect(...)`). Use stripping/regex to extract.
   - Use `_find_matching_paren(line, pos + len(".connect(") - 1)` to find the closing `)` of the connect call. The `-1` positions at the opening `(`.
   - Extract the content between the opening `(` and the matched closing `)`.
   - Check if `.bind(` appears in the extracted content to determine `has_bind`. If so, extract bind args.
   - The `target` is the first argument (before `.bind(` if present, or the whole content for simple cases).
3. Create `GdSignalConnection(signal_source=..., target=..., has_bind=..., bind_args=..., line=line_num)` and append to `result.signal_connections`.

**Depth-aware parsing is critical:** A naive regex like `\.connect\((.+)\)` would over-match when the line contains code after the connect call (e.g., `if signal.connect(handler) == OK:`). The `_find_matching_paren()` function tracks nesting depth to find the correct closing `)`.

### .emit() Detection Logic

1. Search for `RE_EMIT` match in the line.
2. If found:
   - Signal name from group 1.
   - Find the opening `(` position in the match.
   - Use `_find_matching_paren()` to find the matching `)`.
   - Extract the args string between the parens.
   - Use `_count_emit_args()` to count arguments at depth 0. Empty string means 0 args.
3. Create `GdSignalEmission(signal_name=..., arg_count=..., line=line_num)` and append.

### Autoload Reference Detection Logic

Inside the `IN_FUNCTION` state, for each line:

1. **Direct pattern** (only if `re_autoload_direct` was compiled):
   - Search for `re_autoload_direct` match.
   - If found: `autoload_name` from group 1, `access` from group 2.
   - Create `GdAutoloadRef(autoload_name=..., access=..., pattern="direct", line=line_num)`.

2. **has_node pattern:**
   - Search for `RE_HAS_NODE` match.
   - If found: extract the autoload name from the path (last segment of `/root/Name` path, i.e., group 2 which captures the name after `/root/`).
   - Create `GdAutoloadRef(autoload_name=..., access="", pattern="has_node", line=line_num)`.

3. **get_node pattern:**
   - Search for `RE_GET_NODE` match, ensuring it is NOT `get_node_or_null`.
   - If found: same extraction as has_node.
   - Create `GdAutoloadRef(autoload_name=..., access="", pattern="get_node", line=line_num)`.

4. **get_node_or_null pattern:**
   - Search for `RE_GET_NODE_OR_NULL` match.
   - If found: `access` is the full path argument string.
   - Autoload name: extract from the path if it follows `/root/` pattern, otherwise use the full path.
   - Create `GdAutoloadRef(autoload_name=..., access=..., pattern="get_node_or_null", line=line_num)`.

### Cross-State Pattern Detection (All Lines)

These patterns are scanned on EVERY line, regardless of whether the state machine is in `TOP_LEVEL`, `IN_FUNCTION`, `IN_ENUM`, `IN_MULTILINE_VALUE`, or `IN_INNER_CLASS`.

**preload/load detection:**
1. Search for `RE_PRELOAD` match in the line.
   - If found: `GdPreloadRef(path=group(1), is_preload=True, line=line_num)`.
2. Search for `RE_LOAD` match in the line.
   - If found: `GdPreloadRef(path=group(1), is_preload=False, line=line_num)`.
3. Both checks run independently -- a line could theoretically have both (unlikely but safe).

**is_instance_valid detection:**
1. Search for `RE_IS_INSTANCE_VALID` match in the line.
   - If found: append `line_num` to `result.is_instance_valid_lines`.

### Where This Code Lives in `_parse_gd_content()`

The `_parse_gd_content()` function (built in section-02) has a main loop that iterates over lines. The body pattern logic fits in as follows:

```python
def _parse_gd_content(content: str, known_autoloads: list[str] | None = None) -> GdScript:
    """Parse GDScript content string into structured representation."""
    result = GdScript()

    # Compile autoload direct regex if known_autoloads provided (section-03)
    re_autoload_direct = None
    if known_autoloads:
        alt = "|".join(re.escape(name) for name in known_autoloads)
        re_autoload_direct = re.compile(rf"\b({alt})\.(\w+)")

    lines = content.split("\n")
    state = "TOP_LEVEL"
    # ... other state variables from section-02 ...

    for line_num_0based, raw_line in enumerate(lines):
        line_num = line_num_0based + 1  # 1-based

        # === CROSS-STATE PATTERNS (section-03) ===
        # These run on EVERY line regardless of state
        # - preload/load detection -> append to result.preload_refs
        # - is_instance_valid detection -> append line_num to result.is_instance_valid_lines

        # ... comment stripping, blank line handling from section-02 ...

        if state == "TOP_LEVEL":
            # ... section-02 logic ...
            pass

        elif state == "IN_FUNCTION":
            # ... indentation check and dedent logic from section-02 ...

            # === IN_FUNCTION BODY PATTERNS (section-03) ===
            # - .connect() detection (depth-aware)
            # - .emit() detection with arg counting
            # - autoload direct pattern (if re_autoload_direct)
            # - has_node pattern
            # - get_node pattern
            # - get_node_or_null pattern

        # ... other states from section-02 ...

    return result
```

The cross-state patterns (preload/load and is_instance_valid) are placed BEFORE any state-specific logic, at the top of the per-line loop, so they execute for every line. The body patterns (.connect, .emit, autoloads) are placed inside the `IN_FUNCTION` state block, after the dedent check.

### Autoload Alternation Regex Compilation

The autoload direct-pattern regex is compiled ONCE at the start of `_parse_gd_content()`, not per-line. This is a performance consideration: building a regex per line would be wasteful. The alternation pattern `\b(Name1|Name2|Name3)\.(\w+)` efficiently matches any of the known autoload names in a single pass.

When `known_autoloads` is `None` or empty, `re_autoload_direct` remains `None` and the direct-pattern check is skipped entirely inside the loop.

### Extracting Signal Source from .connect()

The signal source is the dotted expression immediately before `.connect(`. For example:
- Line: `\tprinter.print_started.connect(_on_print_started)` 
- Find `.connect(` at some position.
- Everything before `.connect(` on that line, stripped and trimmed: `printer.print_started`.

A practical approach: take the substring before `.connect(`, strip whitespace, then extract the trailing identifier chain. A regex like `([\w.$]+)$` applied to the stripped prefix captures dotted identifiers including `$` for node references.

### Bind Detection Within .connect()

After extracting the full argument content of `.connect(...)`, check if `.bind(` appears:
- If present, `has_bind = True`.
- The target is the part before `.bind(`.
- The `bind_args` is the content inside `.bind(...)` (again, depth-aware extraction).
- Example: `_handler.bind(some_arg)` -> target=`_handler`, bind_args=`some_arg`.

### _count_emit_args() Behavior

This helper (defined in section-01) counts comma-separated arguments at depth 0:
- Empty string -> 0
- `"cash, amount"` -> 2
- `"Vector2(1, 2), 3"` -> 2 (comma inside parens is at depth > 0)
- Whitespace-only -> 0

The implementation scans the string character by character, tracking parenthesis/bracket/brace depth, and counts commas only at depth 0. If there is any non-whitespace content, the result is `comma_count + 1`.

## Error Handling

- If `.connect(` is found but `_find_matching_paren()` returns -1 (no matching close), skip that connect detection silently. Log at DEBUG level.
- If `RE_EMIT` matches but the arg extraction fails, skip silently. Log at DEBUG level.
- Autoload regex compilation is guarded: empty `known_autoloads` list results in no regex (avoids empty alternation `\b()\.(\w+)` which would be invalid).
- All pattern detection is best-effort: a malformed line never causes an exception. The parser continues to the next line.

## Summary of Changes to `gd_parser.py`

1. **Add module-level regex constants:** `RE_EMIT`, `RE_HAS_NODE`, `RE_GET_NODE`, `RE_GET_NODE_OR_NULL`, `RE_IS_INSTANCE_VALID`, `RE_PRELOAD`, `RE_LOAD`.
2. **Add autoload regex compilation** at the start of `_parse_gd_content()`.
3. **Add cross-state pattern scanning** (preload/load, is_instance_valid) at the top of the per-line loop.
4. **Add IN_FUNCTION body pattern scanning** (.connect, .emit, autoload patterns) inside the `IN_FUNCTION` state block.
5. No new dataclasses or helper functions are created in this section -- they come from section-01. No new states are created -- the state machine structure comes from section-02.

## Actual Implementation Notes

### Deviations from Plan
- **Cross-state patterns (preload/load, is_instance_valid)** were already implemented in section-02 as part of the main loop. Section 03 did not need to add them.
- **Replaced `_PRELOAD_LOAD_RE`** with separate `_PRELOAD_RE` + `_LOAD_RE` (with `(?<!\w)` lookbehind) to prevent false positives on words ending in `load` (e.g., `download()`). Code review fix.
- **Added inline comment stripping** (`_strip_comment()`) in IN_FUNCTION body before pattern matching. Prevents detecting patterns in inline comments.
- **Autoload direct pattern uses `finditer()`** instead of `search()` to capture multiple autoload refs on a single line.

### Files Modified
- `src/godotiq/parsers/gd_parser.py` — body pattern scanning in IN_FUNCTION state, autoload regex compilation, preload/load regex fix
- `tests/test_parsers/test_gd_parser.py` — 17 new tests across 5 test classes (TestSignalConnections, TestSignalEmissions, TestAutoloadRefs, TestPreloadLoad, TestIsInstanceValid)

### Test Results
- 79 gd_parser tests pass (62 existing + 17 new)
- 206 total tests pass (all Sprint 0/1/1BC tests unaffected)