# Debug Round 1 Findings — Canonical Refs Refactor

**Date**: 2026-04-06
**Reviewer**: Gemini 3.1 Pro (via consult.py)
**Cost**: $0.029

## Summary

- **Issues found by Gemini**: 8
- **Real bugs confirmed**: 7
- **Bugs fixed**: 7
- **False positives / deferred**: 1 (Dropbox race condition — not actionable)

## Issues Found and Fixed

### Issue 1: Extension Masking in promote_grid (HIGH)
- **File**: `lib/ref_resolver.py` — `promote_grid()`
- **Bug**: When writing panels to `_canonical/`, old files with different extensions were not cleaned up. A stale `hero.jpg` could be masked by a newly written `hero.png`, and reappear if the `.png` was later removed.
- **Fix**: Added `for old_file in canonical_out.parent.glob(f"{role}.*"): old_file.unlink()` before saving.

### Issue 2: Phase Validation Hardcodes .png (HIGH)
- **File**: `lib/ref_resolver.py` — `validate_refs_for_shot()`
- **Bug**: Phase check only tested for `.png` extension: `hero_{phase}.png`. A phase hero saved as `.jpg` would be falsely reported as missing.
- **Fix**: Changed to loop over all image extensions (`.png`, `.jpg`, `.jpeg`).

### Issue 3: resolve_previz_character_refs Missing .jpeg (MEDIUM)
- **File**: `lib/previz_context.py` — `resolve_previz_character_refs()`
- **Bug**: Hardcoded checks for `hero.png` then `hero.jpg`, missing `.jpeg` support. Other resolvers loop over all three extensions.
- **Fix**: Replaced the two-line if/else with a loop over `(".png", ".jpg", ".jpeg")`.

### Issue 4: _canonicalize_hero Not Writing to _canonical/ (HIGH)
- **File**: `api/routes/casting.py` — `_canonicalize_hero()`
- **Bug**: Only wrote to the legacy `output/refs/heroes/` flat folder. Did NOT write to `_canonical/{type}s/{slug}/hero.{ext}`. This meant `casting_select_location_hero` and `_update_casting_hero` (via grid lock-hero) never populated `_canonical/`.
- **Fix**: Added canonical folder write with extension cleanup. Now returns the `_canonical/` relative path instead of the legacy path.

### Issue 5: casting_select_hero Missing _canonical/ Promotion (HIGH)
- **File**: `api/routes/casting.py` — `casting_select_hero()` (FastAPI version)
- **Bug**: Set `hero_path` to the raw grid image path and never promoted to `_canonical/` or legacy hero path. The review_server.py version had this promotion, but the FastAPI port was missing it entirely.
- **Fix**: Added full promotion logic: legacy copy, canonical copy with extension cleanup, hero_path update to canonical path.

### Issue 6: populate_canonical.py Missing Extension Cleanup (HIGH)
- **File**: `tools/populate_canonical.py` — `populate_project()`
- **Bug**: Migration script copied files to `_canonical/` without cleaning up old extensions. Running the migration twice with different source extensions would leave both files, causing extension masking.
- **Fix**: Added `for old_hero in canonical_dir.glob("hero.*"): old_hero.unlink()` before the copy for both characters and locations.

### Issue 7: review_server.py hero_path Not Updated to Canonical (HIGH)
- **File**: `editors/review_server.py` — `_api_casting_select_hero()`
- **Bug**: The hero_path in casting_state was saved as the raw grid image path, then the canonical promotion happened in a try/except, but hero_path was never updated to the canonical location. Also, the `_save_casting_state` call happened BEFORE the promotion, so even if the path was updated, it wouldn't be persisted.
- **Fix**: Moved `_save_casting_state` to AFTER the promotion. Updated `hero_path` to the canonical relative path after successful promotion. Also fixed hardcoded `.png` extension for legacy hero copy.

## Deferred / Not Fixed

### Dropbox Race Condition (MEDIUM — Not Actionable)
- **Finding**: Thumbnail generation reads a file immediately after writing. Gemini flagged this as a potential Dropbox sync issue.
- **Assessment**: Dropbox does not lock files this way on macOS. The read is from the same process that wrote the file, and the OS caches the write. No fix needed.

## Files Modified

1. `lib/ref_resolver.py` — 2 fixes (extension masking in promote_grid, phase validation hardcoded .png)
2. `lib/previz_context.py` — 1 fix (hardcoded .png/.jpg in previz hero resolution)
3. `api/routes/casting.py` — 2 fixes (_canonicalize_hero missing _canonical/ write, casting_select_hero missing promotion)
4. `tools/populate_canonical.py` — 1 fix (missing extension cleanup)
5. `editors/review_server.py` — 1 fix (hero_path not updated to canonical, save ordering)

## Syntax Validation

All Python files pass `ast.parse()` validation.
