Now I have all the context I need. Let me generate the section content.

# Section 02 -- spatial_audit: Severity Filtering + Sibling Z-Fighting Downgrade

## Goal

Add a `min_severity` parameter to `godotiq_spatial_audit` that filters issues by severity level, and downgrade z-fighting severity from `"warning"` to `"info"` when both nodes share the same parent in the scene tree (sibling tiles). This eliminates 77% of noise from sibling tile pairs in the default output while still tracking them in summary counts.

## Background

The spatial audit tool at `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/spatial/__init__.py` currently runs 6 checks (floating_objects, scale_mismatch, z_fighting, empty_markers, overlapping, extreme_positions). Z-fighting detections between sibling nodes under the same parent (e.g., adjacent floor tiles) flood the output. Two changes fix this:

1. **Sibling z-fighting downgrade** -- When both nodes in a z-fighting pair share the same parent path (determined by splitting `full_path` on `/`), the issue severity is set to `"info"` instead of `"warning"`.
2. **min_severity parameter** -- A new parameter (`min_severity`, default `"warning"`) filters the issues list. Issues below the threshold are excluded from the `issues` list but still counted in a new `by_severity` summary dict.

## Dependencies

None. This section is fully independent. It modifies the spatial audit functions in `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/spatial/__init__.py` and adds tests to `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_spatial_audit.py`. Section 04 (scene_map) also modifies the same source file but touches different functions -- there is no overlap.

## Files to Modify

| File | Action |
|------|--------|
| `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/spatial/__init__.py` | Modify `_check_z_fighting`, `_build_spatial_audit`, `godotiq_spatial_audit` |
| `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_spatial_audit.py` | Add 4 new test classes |

## Tests (Write First)

All 4 tests go in `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_spatial_audit.py`. They use programmatic `WorldNode` construction, following the pattern already established by `TestSpatialAuditZFightingMeshOnly` (lines 239-306 of the existing test file). The required imports for the new tests are:

```python
from godotiq.parsers.tscn_parser import TscnNode
from godotiq.parsers.scene_resolver import WorldNode
from godotiq.tools.spatial import _check_z_fighting, _build_spatial_audit
```

The `session` fixture is available from `conftest.py` (loads `tests/fixtures/sample_project/`). The `GodotIQSession` import already exists in the test file.

### Test 1: `test_spatial_audit_min_severity_filter`

**Class:** `TestSpatialAuditMinSeverityFilter`

**Purpose:** Verify that calling `_build_spatial_audit` with the default `min_severity="warning"` excludes info-level issues from the `issues` list, but the `by_severity` dict still reports the full counts.

**Setup:** Use the existing session fixture and the `"res://scenes/equipment/fdm_printer.tscn"` scene which already produces both warning-level and (after the sibling downgrade) info-level issues.

**Assertions:**
- No issue in the `issues` list has `severity == "info"`
- `result["by_severity"]` is a dict
- `result["shown_issues"]` is an int and is less than or equal to `result["total_issues"]`

**Note:** If the fixture scene does not produce info-level issues after the z-fighting downgrade (e.g., no sibling z-fighting pairs exist), then construct WorldNode objects programmatically by directly calling `_check_z_fighting` to produce known info-level issues, and test the filtering logic at the `_build_spatial_audit` level by using a scene that has at least some mix. The implementer should verify against the fixture first and fall back to programmatic construction if needed.

### Test 2: `test_spatial_audit_min_severity_info`

**Class:** `TestSpatialAuditMinSeverityInfo`

**Purpose:** Verify that `min_severity="info"` returns all issues, including info-level ones.

**Setup:** Same scene as Test 1.

**Assertions:**
- `result["shown_issues"] == result["total_issues"]`
- If info-level issues exist (from by_severity), they appear in the `issues` list

### Test 3: `test_spatial_audit_sibling_z_fighting_downgraded`

**Class:** `TestSpatialAuditSiblingZFightingDowngraded`

**Purpose:** Two mesh nodes under the same parent at identical positions produce a z-fighting issue with severity `"info"` (not `"warning"`).

**Setup:** Construct two `WorldNode` objects programmatically:
- Node A: `type="MeshInstance3D"`, `full_path="Room/Floor/TileA"`, `world_position=(1.0, 0.0, 1.0)`
- Node B: `type="MeshInstance3D"`, `full_path="Room/Floor/TileB"`, `world_position=(1.0, 0.0, 1.0)`

Both share parent path `"Room/Floor"`.

**Call:** `_check_z_fighting([node_a, node_b])`

**Assertions:**
- Exactly 1 issue returned
- `issue["severity"] == "info"`
- `issue["check"] == "z_fighting"`

**Edge case for root-level siblings:** Also test two nodes with no `/` in their full_path (e.g., `full_path="TileA"` and `full_path="TileB"`). These share an empty parent and should also be downgraded to `"info"`.

### Test 4: `test_spatial_audit_non_sibling_z_fighting_stays_warning`

**Class:** `TestSpatialAuditNonSiblingZFightingWarning`

**Purpose:** Two mesh nodes under different parents at identical positions produce a z-fighting issue that stays at severity `"warning"`.

**Setup:** Construct two `WorldNode` objects programmatically:
- Node A: `type="MeshInstance3D"`, `full_path="Room/Floor/TileA"`, `world_position=(1.0, 0.0, 1.0)`
- Node B: `type="MeshInstance3D"`, `full_path="Room/Rugs/RugB"`, `world_position=(1.0, 0.0, 1.0)`

They have different parent paths (`"Room/Floor"` vs `"Room/Rugs"`).

**Call:** `_check_z_fighting([node_a, node_b])`

**Assertions:**
- Exactly 1 issue returned
- `issue["severity"] == "warning"`
- `issue["check"] == "z_fighting"`

## Implementation Details

### Part A: Severity Ordering Constant

Add a module-level constant near the top of `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/spatial/__init__.py` (after the existing threshold constants, around line 386):

```python
_SEVERITY_ORDER: dict[str, int] = {"critical": 0, "warning": 1, "info": 2}
```

Lower value means higher severity. This is consistent with the existing `severity_order` dict used in the sort (line 598), which already uses `{"warning": 0, "info": 1}`. The new constant adds `"critical": 0` and shifts the others. The existing inline sort dict should be replaced with this constant.

### Part B: Sibling Z-Fighting Downgrade in `_check_z_fighting`

Add a helper function:

```python
def _parent_path(full_path: str) -> str:
    """Extract parent path from full_path (e.g., 'A/B/C' -> 'A/B').

    Root-level nodes (no '/' in path) return empty string.
    """
```

Implementation: if `"/"` is in `full_path`, return everything before the last `"/"`. Otherwise return `""`.

Modify `_check_z_fighting` (currently at line 469): after detecting a z-fighting pair (the `if dist < _Z_FIGHTING_DISTANCE and a.node.name != b.node.name` block), determine severity based on whether the two nodes share a parent:

- If `_parent_path(a.full_path) == _parent_path(b.full_path)` then `severity = "info"`
- Otherwise `severity = "warning"`

Use the computed `severity` variable in the issue dict instead of the hardcoded `"warning"`.

The existing `_check_z_fighting` function at lines 469-492 currently hardcodes `"severity": "warning"` on line 488. Change that single line to use the dynamically computed severity.

### Part C: `min_severity` Parameter in `_build_spatial_audit`

Modify the `_build_spatial_audit` function signature (currently at line 555) to accept a new parameter:

```python
def _build_spatial_audit(
    session: GodotIQSession,
    scene: str,
    checks: list[str] | None = None,
    min_severity: str = "warning",
) -> dict:
```

Add input validation for `min_severity`: if the value is not in `_SEVERITY_ORDER`, fall back to `"warning"`.

After all checks run and `all_issues` is fully built (after the current line 595 where `by_check` is populated), add the following logic:

1. **Compute `by_severity`** -- Count issues by severity level BEFORE any filtering or capping:
   ```python
   by_severity: dict[str, int] = {}
   for issue in all_issues:
       sev = issue["severity"]
       by_severity[sev] = by_severity.get(sev, 0) + 1
   ```

2. **Sort** -- Replace the inline `severity_order` dict with `_SEVERITY_ORDER`:
   ```python
   all_issues.sort(key=lambda i: _SEVERITY_ORDER.get(i["severity"], 99))
   ```

3. **Filter by min_severity** -- Keep only issues at or above the threshold:
   ```python
   threshold = _SEVERITY_ORDER.get(min_severity, 1)
   all_issues = [i for i in all_issues if _SEVERITY_ORDER.get(i["severity"], 99) <= threshold]
   ```

4. **Cap** -- Apply `_MAX_AUDIT_ISSUES` AFTER filtering (this is the existing cap, just moved after the filter):
   ```python
   all_issues = all_issues[:_MAX_AUDIT_ISSUES]
   ```

5. **Build result** -- Update the return dict to include new fields:
   ```python
   return {
       "scene": scene,
       "total_issues": sum(by_check.values()),  # pre-filter count (unchanged semantics)
       "shown_issues": len(all_issues),           # post-filter, post-cap count
       "filtered_by": min_severity,               # echo back the filter
       "by_severity": by_severity,                # full severity breakdown
       "issues": all_issues,                      # filtered + capped list
       "by_check": by_check,                      # unchanged
       "checks_run": list(checks_to_run),         # unchanged
   }
   ```

### Part D: Update MCP Tool Wrapper

Modify the `godotiq_spatial_audit` async function (currently at line 622) to accept and pass through `min_severity`:

```python
async def godotiq_spatial_audit(
    scene: str,
    checks: list[str] | None = None,
    min_severity: str = "warning",
    ctx: Context = None,
) -> dict:
```

Update the docstring to document the new parameter. Pass `min_severity` through to `_build_spatial_audit`.

### Existing Test Compatibility

The existing tests in `test_spatial_audit.py` call `_build_spatial_audit` without `min_severity`, which defaults to `"warning"`. The return dict gains new keys (`shown_issues`, `filtered_by`, `by_severity`) but all existing assertions check keys that are still present (`total_issues`, `issues`, `by_check`, `checks_run`). No existing assertions need to change.

The severity sort test (`TestSpatialAuditSeverityOrder`, line 184) asserts that warnings appear before info in the issues list. With the default `min_severity="warning"`, info issues are filtered out entirely, so the list contains only warnings -- the ordering assertion still holds trivially. If `min_severity="info"` were used, the sort still places warnings before info, so the assertion would also hold.

### Key Invariants

- `total_issues` always equals `sum(by_check.values())` -- this is the pre-filter count and maintains backward compatibility
- `shown_issues <= total_issues` -- always true
- `by_severity` sums to `total_issues` -- it counts ALL issues before filtering
- The `_MAX_AUDIT_ISSUES` cap of 50 applies to the filtered list, not the raw list
- Invalid `min_severity` values silently fall back to `"warning"`
- The `"critical"` severity level exists in `_SEVERITY_ORDER` but no current check produces it (future-proofing)

---

## Implementation Summary

**Status:** Complete
**Tests:** 17 passing (13 existing + 4 new across 3 classes)

### Files Modified
- `src/godotiq/tools/spatial/__init__.py` — Added `_SEVERITY_ORDER` constant, `_parent_path` helper, sibling z-fighting downgrade in `_check_z_fighting`, `min_severity` parameter with filtering in `_build_spatial_audit`, updated MCP wrapper
- `tests/test_tools/test_spatial_audit.py` — Added 3 new test classes with 5 test methods total

### Code Review Fixes Applied
- Strengthened `test_min_severity_info_shows_all` with conditional info-presence assertion

### Deviations from Plan
- Test 2 (`TestSpatialAuditMinSeverityInfo`) was merged into `TestSpatialAuditMinSeverityFilter` as a second method — functionally equivalent, reduces boilerplate