# Sprint 5f — Spatial Validation in node_ops

## Objective

Add `validate: true` parameter to node_ops. When set, the tool uses scene_map (Phase 1 filesystem analysis) to check the target position BEFORE executing the operation. If the position is problematic, it returns a WARNING with a suggested alternative instead of blindly executing.

This is THE differentiating feature vs GDAI. GDAI moves a node inside a wall and nobody notices. GodotIQ says "blocked — 0.2m inside Wall_East, try [1.5, 0, -0.5] instead."

After this sprint: **30 MCP tools** (same count, node_ops upgraded with intelligence).

---

## Architecture

The validation is **entirely Python-side**. No GDScript changes needed.

```
Agent calls node_ops(operations=[{op:"move", node:"Shelf", position:[1.8,0,-0.5], validate:true}])
    │
    ▼
Python node_ops tool sees validate:true
    │
    ▼
Python calls scene_map internally (Phase 1 filesystem parser)
    → Gets: all node positions, types, nearby nodes, anomalies
    │
    ▼
Python runs validation checks:
    - Distance to nearest StaticBody3D (walls) → too close?
    - Overlap with existing nodes at same position → collision?
    - Distance to similar nodes → outlier?
    - Position within room bounds (if defined in .godotiq.json)?
    │
    ▼
If OK → send to WebSocket → execute → return {status: "OK", validation: "..."}
If WARNING → execute anyway but warn → return {status: "WARNING", validation: "..."}
If BLOCKED → do NOT execute → return {status: "BLOCKED", validation: "...", suggestion: {...}}
```

**Key insight**: scene_map works from filesystem (Phase 1 parsers). No addon or running game needed. Validation works even offline.

---

## DO NOT TOUCH:
- `godot-addon/` — no GDScript changes at all
- `src/godotiq/parsers/` — use them, don't modify
- `src/godotiq/resolvers/` — use them, don't modify
- Existing test files

## Files to MODIFY:
- `src/godotiq/tools/bridge/node_ops.py` — add validation logic
- `src/godotiq/prompts/godot_development.md` — document validate parameter

## Files to CREATE:
- `src/godotiq/tools/bridge/spatial_validator.py` — validation logic (extracted for testability)
- `tests/test_bridge/test_spatial_validator.py` — tests

---

## Implementation

### Step 1: Create `src/godotiq/tools/bridge/spatial_validator.py`

This module contains the pure validation logic. It takes scene_map data and a proposed operation, returns validation results.

```python
"""Spatial validation for node_ops.

Uses scene_map data (Phase 1 filesystem analysis) to validate
proposed node operations before execution.

Checks:
- Proximity to walls (StaticBody3D nodes)
- Overlap with existing nodes
- Distance from similar siblings (outlier detection)
- Room bounds (if configured)
"""

from __future__ import annotations
import math
from typing import Any


# Minimum distances (meters)
MIN_WALL_DISTANCE = 0.2       # Closer than this → BLOCKED
WARN_WALL_DISTANCE = 0.5      # Closer than this → WARNING
OVERLAP_THRESHOLD = 0.1        # Positions within this → overlap
SCALE_OUTLIER_FACTOR = 3.0     # 3x larger/smaller than siblings → WARNING


def validate_move(
    target_position: list[float],
    node_name: str,
    scene_data: dict,
) -> dict:
    """Validate a move operation against spatial context."""
    if len(target_position) < 3:
        return _result("BLOCKED", "Invalid position: need [x, y, z]")

    tx, ty, tz = target_position[0], target_position[1], target_position[2]
    checks = []
    worst_severity = "OK"
    suggestion = None

    nodes = _extract_nodes(scene_data)
    if not nodes:
        return _result("OK", "No spatial data available — executing without validation",
                       checks=[{"check": "data", "result": "No scene_map data"}])

    # Check 1: Distance to walls (StaticBody3D)
    wall_check = _check_wall_distance(tx, ty, tz, nodes, node_name)
    checks.append(wall_check)
    if wall_check["severity"] == "BLOCKED":
        worst_severity = "BLOCKED"
        suggestion = wall_check.get("suggestion")
    elif wall_check["severity"] == "WARNING" and worst_severity == "OK":
        worst_severity = "WARNING"

    # Check 2: Overlap with existing nodes
    overlap_check = _check_overlap(tx, ty, tz, nodes, node_name)
    checks.append(overlap_check)
    if overlap_check["severity"] == "BLOCKED":
        worst_severity = "BLOCKED"
    elif overlap_check["severity"] == "WARNING" and worst_severity != "BLOCKED":
        worst_severity = "WARNING"

    # Check 3: Distance from siblings (outlier detection)
    sibling_check = _check_sibling_distance(tx, ty, tz, node_name, nodes)
    checks.append(sibling_check)
    if sibling_check["severity"] == "WARNING" and worst_severity == "OK":
        worst_severity = "WARNING"

    message = _build_summary(worst_severity, checks, tx, ty, tz)
    return _result(worst_severity, message, checks=checks, suggestion=suggestion)


def validate_scale(
    target_scale: list[float],
    node_name: str,
    scene_data: dict,
) -> dict:
    """Validate a scale operation against similar nodes in the scene."""
    if len(target_scale) < 3:
        return _result("BLOCKED", "Invalid scale: need [x, y, z]")

    sx, sy, sz = target_scale[0], target_scale[1], target_scale[2]
    checks = []
    nodes = _extract_nodes(scene_data)

    sibling_scales = _find_sibling_scales(node_name, nodes)

    if sibling_scales:
        avg_scale = sum(s[0] for s in sibling_scales) / len(sibling_scales)
        ratio = sx / avg_scale if avg_scale > 0.001 else 0

        if ratio > SCALE_OUTLIER_FACTOR or (ratio > 0 and ratio < 1.0 / SCALE_OUTLIER_FACTOR):
            checks.append({
                "check": "scale_outlier",
                "severity": "WARNING",
                "detail": f"Scale {sx:.2f} is {ratio:.1f}x vs avg {avg_scale:.2f} of similar nodes",
            })
            return _result("WARNING",
                f"Scale [{sx},{sy},{sz}] is {ratio:.1f}x different from similar nodes (avg {avg_scale:.2f})",
                checks=checks)

    return _result("OK", f"Scale [{sx},{sy},{sz}] is consistent with scene", checks=checks)
```

### Step 2: Integrate into `src/godotiq/tools/bridge/node_ops.py`

Add validation logic that intercepts `validate: true` in operations, calls scene_map internally (Phase 1 filesystem parser), and runs spatial checks before executing.

### Step 3: Update MCP tool description and prompt documentation

---

## Tests

11+ tests covering:
- Empty scene data → OK
- Valid position far from walls → OK
- Too close to wall → BLOCKED
- Warning distance from wall → WARNING
- Overlapping position → BLOCKED
- Self-node excluded from checks → OK
- Invalid position format → BLOCKED
- Sibling outlier distance → WARNING
- Consistent scale → OK
- Outlier scale → WARNING
- Name prefix extraction

---

## Verification

1. `pytest tests/test_bridge/test_spatial_validator.py -v` → all pass
2. `pytest tests/ -v` → 662+ tests, zero regressions
3. Live test with PFT project
