# Design: ReaScript-First Built-in Plugin Configuration

## Technical Approach

**Hybrid pipeline**: RPPBuilder writes skeleton `.rpp` (VST3 + MIDI + audio only). Built-in plugins (ReaEQ, ReaComp, etc.) are deferred to ReaScript which inserts via `TrackFX_AddByName` and configures via `TrackFX_SetParam`. `ReaScriptGenerator._build_script()` produces dispatch-based `main()` that iterates `cmd["action"]` (now `str | list[str]`) and calls per-action functions conditionally. Backward compat: `action="calibrate"` still works.

## Architecture Decisions

### Multi-Action Dispatch Model

| Option | Tradeoff | Decision |
|--------|----------|----------|
| `action: str \| list[str]` | Normalization in Python; trivial backward compat | **Chosen** |
| Pipeline object with stages | Type-safe but adds dataclass overhead; overkill for 5 actions | Rejected |
| Separate command per action | Multiple script invocations (5x slower); breaks single-script model | Rejected |

**Rationale**: `_build_script()` normalizes `str → [str]`. Generated `main()` loops actions and calls per-action functions (`add_plugins()`, `configure_fx_params()`, `verify_fx()`, `calibrate()`, `render()`). Each function is emitted only if its action appears in the list.

### Built-in FX Parameter Mapping

| Option | Tradeoff | Decision |
|--------|----------|----------|
| Index-based (`dict[int, float]`) | Stable across REAPER versions; matches existing `PluginDef.params` | **Chosen** |
| Name-based (`dict[str, float]`) | User-friendly but fragile (band labels change); requires lookup table | Rejected |
| Hybrid index + name aliases | Too complex for v1; doubles code | Rejected |

**Rationale**: Codebase already uses `PluginDef.params: dict[int, float]` (schema.py:169) and RPPBuilder populates param slots by index (lines 1828-1831). JSON uses `{"2": 200.0}` for transport (string keys). No mapping layer needed.

### Command JSON Protocol Evolution

| Option | Tradeoff | Decision |
|--------|----------|----------|
| Extend existing dataclasses with optional fields, keep v=1 | Zero breaking changes; all new fields optional | **Chosen** |
| New schema v2 | Clear boundary but breaks 298 existing tests | Rejected |
| Separate plugin command file | Coordination complexity between two JSON files | Rejected |

**Rationale**: `plugins_to_add` and `added_plugins` are additive optional fields. `write_command` / `read_result` serialize them only when present. Old scripts without `plugins_to_add` produce identical JSON.

## Data Flow

```
compose.py ──→ SongDefinition ──→ RPPBuilder.write()
                 │  PluginDef.builtin=True        │
                 │  (index-based params)          ▼
                 │                    skeleton .rpp (VST3 only)
                 │                         │
                 │                         ▼
                 │                  run_in_reaper.py
                 │                  │ builds ReaScriptCommand
                 │                  │ ▸ action=["add_plugins","configure_fx_params",
                 │                  │           "verify_fx","calibrate","render"]
                 │                  │ ▸ plugins_to_add=[{track_name, fx_name, params}]
                 │                  ▼
                 │                  ReaScriptGenerator.generate()
                 │                  │ adaptive check_api(actions)
                 │                  │ dispatch loop: add_plugins→configure→verify→calibrate→render
                 │                  ▼
                 │                  REAPER executes → rendered .wav
                 │                                         │
                 ▼                                         ▼
           final .rpp                            ReaScriptResult JSON
                                               ▸ added_plugins, lufs
```

## File Changes

| File | Action | Description |
|------|--------|-------------|
| `src/reaper_scripting/__init__.py` | Modify | Add `_add_plugins_src()`, `_configure_fx_params_src()`, `_action_dispatch_src()`. Refactor `_build_script()` to per-action functions + dispatch loop. Adaptive `check_api(actions)`. |
| `src/reaper_scripting/commands.py` | Modify | `ReaScriptCommand`: `action: str \| list[str]`, `plugins_to_add: list[dict]`. `ReaScriptResult`: `added_plugins: list[dict]`. Update `write_command`/`read_result`. |
| `scripts/run_in_reaper.py` | Modify | Accept `--plugins-config` flag; derive `plugins_to_add` from SongDefinition + RPPBuilder built-in set. Normalize action to list. |
| `src/reaper_builder/__init__.py` | Modify | Add `REAPER_BUILTINS: frozenset[str]`. Expose `get_builtin_plugins(song) → list[dict]`. Skip VST element generation for built-in plugins in `_build_fx_chain()`. |
| `src/core/schema.py` | Modify | Add `builtin: bool = False` to `PluginDef`. |
| `tests/test_reaper_scripting.py` | Modify | Tests: multi-action dispatch output, `plugins_to_add` block, adaptive `check_api`, backward compat string action. |

## Interfaces / Contracts

**PluginDef** (new field):
```python
builtin: bool = False  # True → deferred to ReaScript, not written to .rpp
```

**ReaScriptCommand** (modified):
```python
action: str | list[str] = "calibrate"  # string normalized to list in _build_script
plugins_to_add: list[dict] = field(default_factory=list)
# Each dict: {"track_name": str, "fx_name": str, "params": {"0": float, ...}}
```

**ReaScriptResult** (modified):
```python
added_plugins: list[dict] = field(default_factory=list)
# Each dict: {"fx_name": str, "instance_id": int, "track_name": str, "status": str}
```

**RPPBuilder** (new export):
```python
REAPER_BUILTINS: frozenset[str]  # {"ReaEQ", "ReaComp", "ReaDelay", ...}
def get_builtin_plugins(song: SongDefinition) -> list[dict]: ...
```

## Testing Strategy

| Layer | What | Approach |
|-------|------|----------|
| Unit | Multi-action dispatch generates correct code blocks | Parametrize action list variants; assert source contains expected per-action functions |
| Unit | Adaptive `check_api` requires FX APIs only when relevant | Test with/without `add_plugins`; grep generated source for required API names |
| Unit | `plugins_to_add` serialization round-trip | JSON write → read → assert optional fields survive |
| Integration | Built-in plugin detection in RPPBuilder | Add `builtin=True` to PluginDef; assert skipped in .rpp output, listed in command |
| Regression | All 298 existing tests pass unchanged | Run `pytest` before merge; string action backward compat verified |

## Migration / Rollout

No data migration required. All new fields are optional with defaults. `action="calibrate"` produces identical script output. Rollback: revert `commands.py` and `__init__.py` changes; existing tests remain green.
