# Sprint 3e — Hardening: Complete Specification

## Objective
Fix 5 concrete problems found during real-world testing on Print Farm Tycoon (86 scripts, 41 scenes, 876 nodes, 28 autoloads). These are correctness fixes, noise reduction, and UX improvements — not feature additions.

## Codebase Context

GodotIQ MCP server. Sprint 3d complete: 425 tests, 13 tools. Python 3.10+, FastMCP API.

### Session API (unchanged)
```python
session._index: ProjectIndex
    .autoloads: dict[str, str]       # name -> res:// path
    .scenes: dict[str, TscnScene]
    .scripts: list[str]
session._gd_scripts: dict[str, GdScript]
session.get_script(res_path) -> GdScript | None
session.resolve_scene_spatial(scene_path) -> tuple[ResolvedScene, list[WorldNode]] | None
```

### WorldNode
```python
@dataclass
class WorldNode:
    node: TscnNode
    world_position: tuple[float, float, float]
    world_scale: tuple[float, float, float]
    full_path: str          # "/" separated, e.g., "Building/Floor/Tile"
    depth: int
    is_instance_root: bool
    instance_source: str | None
```

---

## Fix 1 — signal_map: Eliminate Events Bus False Positives (CRITICAL)

**File:** `src/godotiq/tools/code/__init__.py`

### Problem
`find="missing"` flags 52 false positives on PFT. Signals emitted via the Events bus autoload (e.g., `Events.order_accepted.emit()`) are defined in `events.gd`, not in the emitting file. Current check only looks in the same file.

### Current Implementation (lines 393-404)
```python
for emitter_path in unique_emitters:
    emitter_gd = session.get_script(emitter_path)
    emitter_defs = {s.name for s in emitter_gd.signals}
    if name not in emitter_defs:
        missing_list.append(...)
```

### Fix
Build project-wide set of all defined signal names once. Check emissions against this set instead of per-file.

```python
all_defined_signals: set[str] = set()
for gd in session._gd_scripts.values():
    for sig in gd.signals:
        all_defined_signals.add(sig.name)

# In missing check:
if emission.signal_name not in all_defined_signals:
    flag as missing
```

### Interview Decision
- Project-wide name check is sufficient — no need to resolve target object class hierarchy
- Both Godot 3 style (`emit_signal("name")`) and Godot 4 style (`signal.emit()`) are handled by the existing GD parser

### Tests
- `test_signal_map_events_bus_not_missing`: Signal defined in events.gd, emitted in another file → NOT missing
- `test_signal_map_truly_missing`: Signal emitted but not defined anywhere → IS missing

### Fixtures Needed
```
tests/fixtures/scripts/events_bus.gd      # signal order_accepted\nsignal day_started
tests/fixtures/scripts/bus_emitter.gd     # Events.order_accepted.emit()
tests/fixtures/scripts/broken_emitter.gd  # emit_signal("totally_fake_signal")
```

---

## Fix 2 — spatial_audit: Reduce Z-Fighting Noise (HIGH)

**File:** `src/godotiq/tools/spatial/__init__.py`

### Problem
336/435 issues (77%) are z-fighting between sibling floor tiles/walls at Y=0. Real problems buried.

### Current Implementation
- Z-fighting: all get severity "warning"
- No severity filtering — all issues returned, capped at 50
- Severity ordering: `{"warning": 0, "info": 1}`

### Fix 2a: Add `min_severity` parameter
- Default: `"warning"` (filters out info, which is the new z-fighting severity for siblings)
- `by_severity` counts show ALL issues (agent sees info exists)
- `issues` list only includes at/above threshold
- New output fields: `total_issues`, `shown_issues`, `filtered_by`

Severity ordering: `{"critical": 3, "warning": 2, "info": 1}`

### Fix 2b: Downgrade sibling z-fighting to "info"
- Extract parent from `full_path` (split on `/`, take all but last)
- Same parent → "info" (modular kit tiles)
- Different parent → "warning" (likely real issue)

### Interview Decision
- "critical" level is future-proofing only — no existing checks elevated

### Tests
- `test_spatial_audit_min_severity_filter`: warning filter excludes info, counts show all
- `test_spatial_audit_min_severity_info`: info filter shows everything
- `test_spatial_audit_sibling_z_fighting_downgraded`: same parent → info
- `test_spatial_audit_non_sibling_z_fighting_stays_warning`: different parent → warning

---

## Fix 3 — suggest_scale: Work Without Model Path (MEDIUM)

**File:** `src/godotiq/tools/assets/__init__.py`

### Problem
Agent wants "what scale do printers use?" but doesn't have a model file yet. Tool requires `model`.

### Current Implementation
`model` is required. 3-tier matching system. Scale = median per axis.

### Fix
Make `model` optional. When None, enter **reference mode**:
1. Require non-empty `intended_use`
2. Search all world nodes: name/instance_source/ext_resource contains query (case-insensitive)
3. Return matched nodes with scales
4. `recommended_scale` = median per axis (consistent with model mode)

Output includes: `mode: "reference"`, `query`, `matches_found`, `references[]`, `scale_range`, `recommended_scale`, `note`

### Interview Decision
- Median per-axis for recommended_scale (consistent with existing model-mode behavior)

### Tests
- `test_suggest_scale_reference_mode`: printer query finds matching nodes → returns scales
- `test_suggest_scale_reference_mode_no_match`: no matches → 0 matches, helpful message
- `test_suggest_scale_both_empty_error`: no model, no intended_use → error
- `test_suggest_scale_model_still_works`: existing behavior unchanged

---

## Fix 4 — scene_map: Reduce Token Bloat (MEDIUM)

**File:** `src/godotiq/tools/spatial/__init__.py`

### Problem
10.1k tokens for single call with `radius=8, detail="full"`. Context window killer.

### Current Implementation
- `_MAX_NODES_WITH_FOCUS = 30` for all detail levels
- `_MAX_NODES_NO_FOCUS = 50`
- Detail levels already differentiate fields (brief/normal/full)

### Fix 4a: Detail-dependent caps (focus mode only)
```python
MAX_NEARBY = {"brief": 10, "normal": 20, "full": 30}
```
Non-focus mode stays at 50 (used for scene overview).

### Fix 4b: Compact nearby output
Verify current implementation:
- `"brief"`: should only have name, type, distance, direction
- `"normal"`: should have name, type, distance, direction, groups, has_script
- `"full"`: all properties

Research shows brief already omits most fields. Normal may include too much. Verify and fix.

### Fix 4c: Token hint fields
```python
output["token_hint"] = detail
output["nearby_count"] = len(nearby)
output["nearby_capped"] = len(nearby) >= MAX_NEARBY[detail]
output["nearby_total_in_radius"] = total_before_cap  # only when capped
```

### Interview Decision
- Only cap focus mode. Non-focus mode keeps _MAX_NODES_NO_FOCUS = 50.

### Tests
- `test_scene_map_nearby_cap_brief`: 15+ nodes in radius → only 10 returned
- `test_scene_map_nearby_cap_normal`: caps at 20
- `test_scene_map_brief_compact_output`: only name, type, distance, direction
- `test_scene_map_capped_shows_total`: nearby_total_in_radius present when capped

---

## Fix 5 — project_summary: Add Architectural Context (MEDIUM)

**File:** `src/godotiq/tools/memory/__init__.py`

### Problem
Summary lacks signal health and autoload importance metrics. Agent needs more context to start working.

### Current Implementation
- `"full"` already has `autoload_analysis` with signals list and `referenced_by`
- No signal_health or project-wide health metrics

### Fix 5a: Autoload enrichment (detail="full" only)
Add to existing autoload output: `signals_defined` count, `signals_emitted` count, `referenced_by_count`

### Fix 5b: Signal health summary (detail="full" only)
```python
"signal_health": {
    "total_defined": int,
    "total_emissions": int,
    "total_connections": int,
    "orphan_count": int,      # emitted but 0 connections
    "top_signals": [...]       # top 5 most connected
}
```

### Fix 5c: Project health (detail="full" only)
```python
"health": {
    "orphan_signals": int,
    "scripts_without_class_name": int,  # non-autoload scripts without class_name
    "note": str
}
```

### Interview Decision
- orphan_count = signals emitted but never connected-to (not "defined but never emitted")

### Tests
- `test_project_summary_full_has_autoload_details`: autoloads have signal/reference counts
- `test_project_summary_full_has_signal_health`: signal_health present with totals
- `test_project_summary_full_has_health`: health with orphan count
- `test_project_summary_normal_no_extras`: signal_health/health absent
- `test_project_summary_brief_unchanged`: unchanged

---

## Constraints
- NO parser modifications
- NO session.py changes unless absolutely necessary
- NO existing test assertion changes — only ADD new tests
- NO external dependencies
- All changes additive — backward compatible (new params have old-behavior defaults)
- Target: 425 existing + ~19 new ≈ 444+ passing tests
