Now I have comprehensive understanding of the codebase. Let me generate the section content.

# Section 03 — suggest_scale: Reference Mode Without Model

**Status:** IMPLEMENTED

## Overview

This section adds a "reference mode" to the `godotiq_suggest_scale` tool. Currently the tool requires a `model` parameter (a `res://` path to a model asset). Reference mode makes `model` optional: when `model` is `None`, the tool searches the scene for existing nodes matching the `intended_use` query string and returns their scale statistics. This covers the real use case where an AI agent does not yet have a model file but wants to know what scale existing similar objects use.

**Priority:** MEDIUM -- UX improvement, independent file.

## Implementation Summary

**Files modified:**
- `src/godotiq/tools/assets/__init__.py` — Added `_reference_mode()` helper, updated `_build_suggest_scale` signature (`model: str | None = None`), added `"mode": "model"` to existing code path, added defensive `intended_use=None` guard, updated MCP tool signature and docstring
- `tests/test_tools/test_suggest_scale.py` — Added 5 new tests in `TestSuggestScaleReferenceMode` class

**Deviations from plan:**
- `_reference_mode` does NOT take `session` parameter (plan specified it, but it's unused — cleaner without)
- Added defensive `if intended_use is None: intended_use = ""` guard (from code review)
- Added extra test `test_suggest_scale_reference_mode_missing_scene` (from code review)

**Tests:** 16 total (11 existing + 5 new), all passing

**Files modified:**
- `src/godotiq/tools/assets/__init__.py` (implementation)
- `tests/test_tools/test_suggest_scale.py` (4 new tests)

**Dependencies:** None. This section modifies a file not touched by any other section.

---

## Tests (Write First)

**File:** `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_suggest_scale.py`

Add a new test class at the end of the existing file. All 4 tests use the existing `session` fixture from `tests/test_tools/conftest.py`, which loads the sample project at `tests/fixtures/sample_project/`.

### Test 1: test_suggest_scale_reference_mode

Verifies that calling `_build_suggest_scale` with `model=None` and a matching `intended_use` enters reference mode and returns matching nodes with correct scale statistics.

```python
class TestSuggestScaleReferenceMode:
    """Reference mode: model=None, search by intended_use."""

    def test_suggest_scale_reference_mode(self, session: GodotIQSession) -> None:
        """Reference mode finds nodes matching intended_use and returns scale stats."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="printer",
        )
        assert result["mode"] == "reference"
        assert result["matches_found"] > 0
        assert result["recommended_scale"] is not None
        assert len(result["recommended_scale"]) == 3
        assert "references" in result
        assert len(result["references"]) > 0
```

The sample project scene `fdm_printer.tscn` contains a node named "PrinterModel". The case-insensitive substring match on "printer" should find it. The `recommended_scale` should be the median per axis of all matched nodes' scales.

### Test 2: test_suggest_scale_reference_mode_no_match

Verifies that reference mode with a query that matches nothing returns `matches_found: 0` and `recommended_scale: None`.

```python
    def test_suggest_scale_reference_mode_no_match(self, session: GodotIQSession) -> None:
        """Reference mode with no matching nodes returns matches_found=0."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="dragon",
        )
        assert result["mode"] == "reference"
        assert result["matches_found"] == 0
        assert result["recommended_scale"] is None
```

### Test 3: test_suggest_scale_both_empty_error

Verifies that calling with `model=None` AND empty `intended_use` returns an error dict.

```python
    def test_suggest_scale_both_empty_error(self, session: GodotIQSession) -> None:
        """model=None and intended_use='' returns an error."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model=None,
            intended_use="",
        )
        assert "error" in result
```

### Test 4: test_suggest_scale_model_still_works

Verifies that the existing model-based behavior is preserved. When `model` is provided, the output should include `mode: "model"` and behave exactly as before.

```python
    def test_suggest_scale_model_still_works(self, session: GodotIQSession) -> None:
        """Existing model-based behavior is unchanged, now includes mode='model'."""
        result = _build_suggest_scale(
            session,
            "res://scenes/equipment/fdm_printer.tscn",
            model="res://assets/models/printers/printer_standard.glb",
        )
        assert result["mode"] == "model"
        assert result["recommended_scale"] is not None
        assert result["match_tier"] == "exact_filename"
```

---

## Implementation

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

### Change 1: Update `_build_suggest_scale` signature

The `model` parameter changes from `str` to `str | None = None`. The existing signature is:

```python
def _build_suggest_scale(
    session: GodotIQSession,
    scene: str,
    model: str,
    near_node: str | None = None,
    intended_use: str = "",
) -> dict:
```

Change to:

```python
def _build_suggest_scale(
    session: GodotIQSession,
    scene: str,
    model: str | None = None,
    near_node: str | None = None,
    intended_use: str = "",
) -> dict:
```

### Change 2: Add guard for both-empty case

At the top of `_build_suggest_scale`, before any scene resolution, add:

```python
if model is None and not intended_use:
    return {"error": "Either 'model' or 'intended_use' must be provided"}
```

### Change 3: Add reference mode branch

After the guard, add a branch: if `model is None`, enter reference mode. Otherwise, run the existing code path. The structure becomes:

```python
def _build_suggest_scale(session, scene, model=None, near_node=None, intended_use=""):
    if model is None and not intended_use:
        return {"error": "Either 'model' or 'intended_use' must be provided"}

    world_nodes, ok = _collect_world_nodes_recursive(session, scene)
    if not ok:
        return {"error": "Scene not found", "scene": scene}

    if model is None:
        return _reference_mode(session, scene, intended_use, world_nodes)

    # ... existing model-based code path (unchanged) ...
```

### Change 4: Implement `_reference_mode` helper

Add a new private function above `_build_suggest_scale`:

```python
def _reference_mode(
    session: GodotIQSession,
    scene: str,
    intended_use: str,
    world_nodes: list,
) -> dict:
    """Search scene for nodes matching intended_use query, return scale statistics."""
```

**Matching logic:** For each WorldNode in the scene, check two conditions (case-insensitive):
1. `intended_use` appears as a substring in `wn.node.name`
2. `intended_use` appears as a substring in the filename portion of `wn.instance_source` (when not None)

If either matches, include the node.

**Output construction for matches found:**

```python
{
    "scene": scene,
    "mode": "reference",
    "intended_use": intended_use,
    "matches_found": len(matched),
    "references": [
        {
            "name": wn.node.name,
            "scale": list(wn.world_scale),
            "position": list(wn.world_position),
            "instance_source": wn.instance_source,
        }
        for wn in matched
    ],
    "scale_range": {
        "min": [min of each axis],
        "max": [max of each axis],
    },
    "recommended_scale": [median_x, median_y, median_z],
    "note": f"Found {len(matched)} node(s) matching '{intended_use}' ...",
}
```

The median-per-axis calculation reuses the same `statistics.median` approach already used in the model code path (lines 413-419 of the current file).

**Output construction for zero matches:**

```python
{
    "scene": scene,
    "mode": "reference",
    "intended_use": intended_use,
    "matches_found": 0,
    "references": [],
    "recommended_scale": None,
    "note": f"No nodes matching '{intended_use}' found. Try a broader query.",
}
```

### Change 5: Add `"mode": "model"` to existing code path

In the existing model-based code path, add `"mode": "model"` to the `result` dict. This is the dict constructed around line 435-442 of the current file. Simply add the key:

```python
result: dict = {
    "scene": scene,
    "model": model,
    "mode": "model",  # <-- NEW
    "recommended_scale": recommended_scale,
    "match_tier": match_tier,
    "reference_models": reference_models,
    "reasoning": reasoning,
}
```

### Change 6: Update the MCP tool function signature

The `godotiq_suggest_scale` async function (the `@mcp.tool()` decorated entry point) must also make `model` optional:

Current signature:
```python
async def godotiq_suggest_scale(
    scene: str,
    model: str,
    near_node: str | None = None,
    intended_use: str = "",
    ctx: Context = None,
) -> dict:
```

Change to:
```python
async def godotiq_suggest_scale(
    scene: str,
    model: str | None = None,
    near_node: str | None = None,
    intended_use: str = "",
    ctx: Context = None,
) -> dict:
```

Update the docstring to describe reference mode:

```python
"""Suggest scale and position for a model based on similar models in a scene.

When model is provided: finds similar models and suggests scale based on them.
When model is omitted: searches for nodes matching intended_use and returns
their scale statistics (reference mode).

Args:
    scene: Path to .tscn file (res:// or relative).
    model: Path to the model asset (res://). Optional — omit for reference mode.
    near_node: Optional node name to suggest position near.
    intended_use: Hint about intended use. Required when model is omitted.
"""
```

Since FastMCP generates the JSON schema from the function signature, making `model: str | None = None` will correctly mark it as optional in the MCP protocol.

### Edge Cases

- **Zero matches:** Return `matches_found: 0`, `recommended_scale: None`, note suggesting a broader query.
- **One match:** Return that single node's scale as `recommended_scale` (median of 1 element is the element itself).
- **All matches identical scale:** `scale_range` min equals max, `recommended_scale` equals that scale.
- **Root nodes only (no instance_source):** Matching still works via node name substring check; `instance_source` will be `None` in the references list entry.

### What Does NOT Change

- The 3-tier matching system when `model` is provided (exact filename, same directory, group)
- Position suggestion logic (the `near_node` handling)
- Scale calculation method (median per axis)
- All existing parameter defaults
- The `_collect_world_nodes_recursive` function
- The `_find_ext_resource_refs` function
- The `_build_asset_registry` function and its MCP tool
- All existing tests continue to pass without modification (the only change to the model path is adding `"mode": "model"` to its output, which existing tests do not assert against negatively)