# BUILD SPEC: Visual Validation Integration

## Overview
Wire the existing visual validators (RefImageCritic, StartFrameCritic, VideoFrameCritic) into the pipeline at three hook points. Minimal code changes — the validators already work.

---

## Phase 1: elements.py — Validate refs before element assembly

### File: `lib/elements.py` (MODIFY)

Add optional ref validation in `_build_element_entry()`.

**Changes:**

1. Add constructor param `validate_refs: bool = True` to `ElementManager.__init__()`.

2. In `_build_element_entry()`, before converting refs to data URIs, validate the first ref (frontal/hero) with RefImageCritic. If it fails HARD checks, log a warning with the specific failures and return `None` (skip this element). If it passes, proceed normally.

3. Import lazily to avoid circular imports:
```python
def _validate_ref(self, ref_path: Path, element_id: str) -> bool:
    """Validate a ref image before use as an element. Returns True if OK."""
    if not self.validate_refs:
        return True
    try:
        from lib.critics.ref_image_critic import RefImageCritic
        critic = RefImageCritic(character_type="human")  # Default — could be enhanced with character metadata
        _, result = critic.run(str(ref_path))
        if not result.passed:
            failed = [f"{d.name}: {d.message}" for d in result.failed_dimensions]
            logger.warning(
                "Ref validation FAILED for %s (%s): %s",
                element_id, ref_path.name, "; ".join(failed),
            )
            return False
        logger.info("Ref validation passed for %s (%s)", element_id, ref_path.name)
        return True
    except Exception as e:
        logger.warning("Ref validation error (non-blocking): %s", e)
        return True  # Fail-open on validator errors
```

4. Call `_validate_ref()` in `_build_element_entry()` on `refs[0]` (the frontal/hero ref). If it returns False, return None.

5. Also validate each additional angle ref in `refs[1:4]` — skip any that fail, but don't skip the whole element.

**Do NOT change:**
- The _to_data_uri function
- The build_fal_elements function signature
- Any existing ref resolution logic

### Validation: `cd /Users/joeturnerlin/Dropbox/CLAUDE_PROJECTS/starsend && python3 -c "from lib.elements import ElementManager; print('OK')"`

---

## Phase 2: step_runner.py — Validate start frames before submission

### File: `orchestrator/step_runner.py` (MODIFY)

Add StartFrameCritic validation before the Kling API call.

**Changes:**

1. Add `validate_frames: bool = True` param to `StepRunner.__init__()`, stored as `self._validate_frames`.

2. In the video generation flow (the section where `start_frame` is prepared, around line 200), BEFORE the API submission (line 220), add:

```python
# Validate start frame before submission
if self._validate_frames and start_frame is not None:
    try:
        from lib.critics.start_frame_critic import StartFrameCritic
        critic = StartFrameCritic(expected_background="scene")
        _, sf_result = critic.run(str(start_frame))
        if not sf_result.passed:
            failed = [f"{d.name}: {d.message}" for d in sf_result.failed_dimensions]
            logger.warning(
                "Start frame validation FAILED for %s: %s",
                shot_id, "; ".join(failed),
            )
            # Store the validation failure but don't block — let the user decide
            self._store.update_shot(
                shot_id,
                validation_notes=f"Start frame failed: {'; '.join(failed)}",
            )
        else:
            logger.info("Start frame validation passed for %s", shot_id)
    except Exception as e:
        logger.warning("Start frame validation error (non-blocking): %s", e)
```

**Do NOT change:**
- The API submission logic
- The payload construction
- The StepResult dataclass
- Any existing gate logic

### Validation: `cd /Users/joeturnerlin/Dropbox/CLAUDE_PROJECTS/starsend && python3 -c "from orchestrator.step_runner import StepRunner; print('OK')"`

---

## Phase 3: step_runner.py — Validate video after generation

### File: `orchestrator/step_runner.py` (MODIFY)

Add VideoFrameCritic as a post-generation validation step, integrated with the existing gate system.

**Changes:**

1. After the video is saved (line 248: `video_path = self._save_video(shot_id, result)`) and BEFORE the existing gates run (line 253), add video frame validation:

```python
# Video frame validation (runs before other gates)
if self._validate_frames and video_path:
    try:
        from lib.critics.video_frame_critic import VideoFrameCritic
        vf_critic = VideoFrameCritic(
            character_type="human",
            expected_style="",  # Could be enriched from project config
            num_frames=3,  # 3 frames for speed, 5 for thorough
        )
        _, vf_result = vf_critic.run(str(video_path))
        if not vf_result.passed:
            failed = [f"{d.name}: {d.message}" for d in vf_result.failed_dimensions]
            logger.warning(
                "Video validation FAILED for %s: %s",
                shot_id, "; ".join(failed),
            )
            self._store.update_shot(
                shot_id,
                validation_notes=f"Video failed: {'; '.join(failed)}",
            )
        else:
            logger.info("Video validation passed for %s", shot_id)
    except Exception as e:
        logger.warning("Video validation error (non-blocking): %s", e)
```

**Do NOT change:**
- The existing gate system (lines 253-297)
- The video saving logic
- The cost tracking

### Validation: `cd /Users/joeturnerlin/Dropbox/CLAUDE_PROJECTS/starsend && python3 -c "from orchestrator.step_runner import StepRunner; print('OK')"`

---

## Phase 4: Tests

### File: `tests/orchestrator/test_validation_hooks.py` (NEW)

Test the integration hooks with mocked validators:

1. **test_element_ref_validation_skips_bad_ref** — Mock RefImageCritic to fail, verify _build_element_entry returns None
2. **test_element_ref_validation_passes_good_ref** — Mock RefImageCritic to pass, verify element is built
3. **test_element_ref_validation_disabled** — Set validate_refs=False, verify no validation runs
4. **test_start_frame_validation_logs_warning** — Mock StartFrameCritic to fail, verify warning logged
5. **test_video_validation_logs_warning** — Mock VideoFrameCritic to fail, verify warning logged

All tests mock the critics — no real Gemini API calls.

### Validation: `cd /Users/joeturnerlin/Dropbox/CLAUDE_PROJECTS/starsend && python3 -m pytest tests/orchestrator/test_validation_hooks.py -v`

---

## Phase 5: Simplify

Run `/simplify` on modified files:
- `lib/elements.py`
- `orchestrator/step_runner.py`
- `tests/orchestrator/test_validation_hooks.py`
