# Build Spec: Format Registry — Validation Dispatcher + Agent Parameterization

> Phases 2-3 of the format registry integration.
> Phase 1 (directory structure) is complete.
> DO NOT modify existing V12 behavior — all changes must be additive or conditional.

## Pre-Conditions

The format registry structure exists at `recoil/formats/`:
- `formats/kill_box/FORMAT.md`, `CONSTANTS.md`, `CONTEXT.md`
- `formats/kill_box_micro/FORMAT.md`, `CONSTANTS.md`, `CONTEXT.md`
- `formats/puzzle_box/CONSTANTS.md`, `CONTEXT.md` (FORMAT.md in progress)
- `formats/_registry.md`

## Phase 1: Validation Dispatcher

### 1.1 — Move V12 validator to format directory

**File:** `formats/kill_box/validate.py`
- Copy the validation logic from `tools/episode_metrics.py` into `formats/kill_box/validate.py`
- Refactor to export two functions:
  ```python
  def validate_episode(episode_path: str, constants: dict = None) -> dict:
      """Returns {valid: bool, errors: [], warnings: [], metrics: {}}"""

  def validate_batch(episode_paths: list, constants: dict = None) -> dict:
      """Returns {valid: bool, episode_results: [...]}"""
  ```
- The internal logic (Kill Box section regex, word count checks, pattern distribution) stays identical
- If constants are not passed, load from `formats/kill_box/CONSTANTS.md`
- **DO NOT delete `tools/episode_metrics.py` yet** — keep it as a fallback during transition

### 1.2 — Create Puzzle Box validator stub

**File:** `formats/puzzle_box/validate.py`
- Same interface as kill_box validator
- Validate:
  - Two-layer script structure (narrative layer + pipeline layer present)
  - Narrative word count: 40-60 (warn outside, fail >80)
  - Pipeline word count: 120-160 (warn outside)
  - Total word count: 160-220 (fail outside)
  - Beat sections present: ENTRY IMAGE, VOICE, LINGER (+ ORACLE on Exposure-final episodes)
  - Ending type annotation present (one of: RHYME, WITHHOLD, DISSONANCE, OBJECT, ABSENCE)
  - Fragment linkage annotation present (for episodes 5+)
  - VO orthogonality: warn if VO text overlaps with pipeline visual description (basic keyword check)
  - Rhythm tag present and valid (SUSPENDED, LAYERED, KINETIC, DRIFT)
- For now, implement what's possible from CONSTANTS.md. Some checks (like fragment linkage validation against prior episodes) can be stubs that always pass.

### 1.3 — Create Kill Box Micro validator stub

**File:** `formats/kill_box_micro/validate.py`
- Same interface
- Validate based on kill_box_micro/CONSTANTS.md:
  - Beat sections: CONSEQUENCE, PIVOT, FREEZE, VOTE
  - Word counts: 120-180 total, ≤40 spoken
  - Rhythm tag valid (Frenetic, Measured, Fluid)
  - Cliffhanger type present (Reveal, Reversal, Clock, Dilemma)

### 1.4 — Create validation dispatcher

**File:** `tools/validate_episode.py`
```python
"""Thin dispatcher — reads format from project config, routes to format-specific validator."""
import sys
import json
import importlib.util
from pathlib import Path

def detect_format(project_path: str) -> str:
    """Read format from project_config.json. Default to kill_box."""
    config_path = Path(project_path) / "project_config.json"
    if config_path.exists():
        config = json.loads(config_path.read_text())
        return config.get("format", "kill_box")
    return "kill_box"

def load_validator(format_name: str):
    """Dynamically load format-specific validator."""
    recoil_root = Path(__file__).parent.parent
    validator_path = recoil_root / "formats" / format_name / "validate.py"
    if not validator_path.exists():
        raise FileNotFoundError(f"No validator for format: {format_name} at {validator_path}")
    spec = importlib.util.spec_from_file_location(f"formats.{format_name}.validate", validator_path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod

def main():
    # Support both old interface (episode_path only) and new (episode_path + project_path)
    if len(sys.argv) < 2:
        print("Usage: validate_episode.py <episode_path> [project_path] [--json]")
        sys.exit(2)

    episode_path = sys.argv[1]
    project_path = sys.argv[2] if len(sys.argv) > 2 and not sys.argv[2].startswith("--") else None
    json_output = "--json" in sys.argv

    format_name = detect_format(project_path) if project_path else "kill_box"
    validator = load_validator(format_name)
    result = validator.validate_episode(episode_path)

    if json_output:
        print(json.dumps(result, indent=2))
    else:
        status = "PASS" if result["valid"] else "FAIL"
        print(f"{status}: {episode_path}")
        for err in result.get("errors", []):
            print(f"  ERROR: {err}")
        for warn in result.get("warnings", []):
            print(f"  WARN: {warn}")

    sys.exit(0 if result["valid"] else 1)

if __name__ == "__main__":
    main()
```

### 1.5 — Update validate_batch hook

**File:** `.claude/hooks/validate_batch.py`
- Add format detection at the top
- Route to format-specific `validate_batch()` instead of calling `episode_metrics.py` directly
- Keep backward compatibility: if no project_config.json found, default to kill_box

### Validation Gate (Phase 1)

Run the Kill Box validator against an existing V12 episode:
```bash
python3 formats/kill_box/validate.py [existing_episode.md] --json
```
Output must match `python3 tools/episode_metrics.py [same_episode.md] --json`.

Run the dispatcher against the same episode with a kill_box project:
```bash
python3 tools/validate_episode.py [existing_episode.md] [project_path] --json
```
Output must match.

---

## Phase 2: Format-Aware Engine Constants

### 2.1 — Update engine_constants.py

**File:** `tools/engine_constants.py` (or `lib/engine_constants.py` — find the actual location)

Add a `load_format_constants(format_name: str) -> dict` function:
- Reads `formats/{format_name}/CONSTANTS.md`
- Parses markdown tables into a dict
- Caches after first load
- Falls back to root `CONSTANTS.md` if format-specific file not found (backward compat)

Keep the existing `load_constants()` function working for backward compatibility — it should continue reading the root CONSTANTS.md. The new function is additive.

### 2.2 — Update thread tools

**Files:** `tools/track_threads.py`, `tools/verify_thread_continuity.py`
- Where STALE_THRESHOLD is hardcoded (15), replace with:
  ```python
  threshold = constants.get("STALE_THRESHOLD", 15)
  ```
- Where MIN_THREAD_COUNT is hardcoded (6), replace with:
  ```python
  min_threads = constants.get("MIN_THREAD_COUNT", 6)
  ```
- Accept format_name as optional parameter; if provided, load format-specific constants

### Validation Gate (Phase 2)

Run `verify_thread_continuity.py` against an existing V12 project. Output must be unchanged.

---

## Phase 3: Format-Aware State Init

### 3.1 — Update init_orchestrator_state.py

**File:** `tools/init_orchestrator_state.py`

Add format awareness:
- Read format from project_config.json
- Load format-specific CONSTANTS.md for beat schedule, thread thresholds, checkpoint schedule
- Initialize `format_state` section in orchestrator_state.json:
  - For kill_box: emotional_beat_map (11 beats), pattern_state (hooks/cliffhangers), checkpoints at batches 3/6/9/12
  - For puzzle_box: emotional_beat_map (6 beats), resonance_state, eruption_tracker, rhythm_distribution, vo_tracker, checkpoints at exposures 1/2/3/4
- Thread tracker stays at top level (format-agnostic)
- Add `"format": "{format_name}"` to the state envelope

Keep backward compatibility: if no project_config.json, initialize as kill_box.

### Validation Gate (Phase 3)

Initialize state for an existing V12 project. The resulting orchestrator_state.json must match what the current tool produces (plus the new `format` and `format_state` fields).

---

## Phase 4: Smoke Test

Create a minimal test project:
```bash
mkdir -p projects/_format_test/state
echo '{"format": "puzzle_box"}' > projects/_format_test/project_config.json
```

Run:
1. `python3 tools/init_orchestrator_state.py projects/_format_test` — should produce puzzle_box state
2. `python3 tools/validate_episode.py [any_file] projects/_format_test --json` — should route to puzzle_box validator

Clean up test project after verification.

---

## Phase 5: Simplify

Run `/simplify` on all new and modified files. Check for:
- Redundant code paths
- Unused imports
- Overly complex conditionals that could be simplified
- Any copy-paste duplication between validators
- Shared utilities that should be extracted (e.g., markdown table parsing, word counting)

Fix anything found. Re-run the Phase 4 smoke test after simplification to ensure nothing broke.

---

## Files Created/Modified Summary

### NEW FILES
- `formats/kill_box/validate.py`
- `formats/kill_box_micro/validate.py`
- `formats/puzzle_box/validate.py`
- `tools/validate_episode.py`

### MODIFIED FILES (minimal, backward-compatible changes)
- `tools/engine_constants.py` or `lib/engine_constants.py` — add format-aware loader
- `tools/track_threads.py` — parameterize STALE_THRESHOLD
- `tools/verify_thread_continuity.py` — parameterize STALE_THRESHOLD, MIN_THREAD_COUNT
- `tools/init_orchestrator_state.py` — format-aware init
- `.claude/hooks/validate_batch.py` — route through dispatcher

### NOT MODIFIED (leave for later)
- `agents/batch_agent.md` — agent parameterization is a separate build
- `agents/treatment_agent.md` — same
- `/load-context` skill — same
- `tools/episode_metrics.py` — kept as fallback, not deleted
