# Plan: Replace `sequential` with `fast`

## Context
Sequential optimization (N+2 DSPy compile runs) is the current default and is slow. The user wants to invert the mental model: **`fast=True` = single-pass with speed-optimised kwargs (fewer demos, num_threads fix); `fast=False` (default) = sequential field-by-field for maximum quality**. The `sequential` parameter is removed and replaced entirely by `fast`. Progress callbacks are added to both modes. After implementation, `compare_optimization.py` is updated and run.

---

## Semantic change

| Old | New |
|-----|-----|
| `sequential=True` (default) | `fast=False` (default) — sequential field-by-field |
| `sequential=False` | `fast=True` — single-pass + fast kwargs |

---

## Changes

### 1. `src/dspydantic/types.py` — Add `FieldOptimizationProgress`

Add after `OptimizationResult` (line 38):

```python
@dataclass
class FieldOptimizationProgress:
    phase: str           # "baseline" | "fields" | "system_prompt" | "instruction_prompt" | "complete"
    score_before: float
    score_after: float
    improved: bool
    total_fields: int
    field_path: str | None = None    # None for non-field phases
    field_index: int | None = None   # 1-based, None for non-field phases
    elapsed_seconds: float = 0.0
```

---

### 2. `src/dspydantic/__init__.py`

Add `FieldOptimizationProgress` to import from `dspydantic.types` and to `__all__`.

---

### 3. `src/dspydantic/optimizer.py` — Core changes

#### A. Module-level fast-mode constants (after imports, ~line 17)

```python
_FAST_MODE_KWARGS: dict[str, dict] = {
    "bootstrapfewshot": {"max_bootstrapped_demos": 1},
    "bootstrapfewshotwithrandomsearch": {"max_bootstrapped_demos": 1, "num_candidates": 4},
    "miprov2": {"auto": "light", "max_bootstrapped_demos": 1},
    "miprov2zeroshot": {"auto": "light"},
}
```

#### B. `PydanticOptimizer.__init__` — replace `sequential` with `fast`, add `on_progress`

Remove `sequential: bool = True` (~line 195). Add in its place:
```python
fast: bool = False,
on_progress: Callable[[FieldOptimizationProgress], None] | None = None,
```

In the body, replace `self.sequential = sequential` with:
```python
self.fast = fast
self.on_progress = on_progress
# fast=False → sequential field-by-field; fast=True → single-pass + fast kwargs
self.sequential = not fast
if self.fast and self.custom_optimizer is None:
    fast_defaults = _FAST_MODE_KWARGS.get(self.optimizer_type, {})
    self.optimizer_kwargs = {**fast_defaults, **self.optimizer_kwargs}  # user kwargs win
```

#### C. Fix `num_threads` bug in `_create_teleprompter` (~line 834)

Currently `BootstrapFewShot` only gets `{"metric": metric}`. Fix by checking the optimizer class signature:

```python
default_kwargs = {"metric": metric}
if "num_threads" in inspect.signature(optimizer_class.__init__).parameters:
    default_kwargs["num_threads"] = self.num_threads
if self.optimizer_type == "bootstrapfewshot" and len(self.examples) < 5:
    default_kwargs["max_bootstrapped_demos"] = max(1, min(2, len(self.examples) - 1))
return optimizer_class(**{**default_kwargs, **self.optimizer_kwargs})
```

`inspect` is already imported at the top of `optimizer.py`.

#### D. Progress callbacks in `_optimize_sequential` (~line 1087)

Add at start of method (move `import time` to top-level imports):
```python
_t0 = time.perf_counter()
total_fields = len(sorted_fields)  # already computed later — move up

def _emit(phase, score_before, score_after, field_path=None, field_index=None):
    if self.on_progress is None:
        return
    try:
        self.on_progress(FieldOptimizationProgress(
            phase=phase, score_before=score_before, score_after=score_after,
            improved=score_after > score_before, total_fields=total_fields,
            field_path=field_path, field_index=field_index,
            elapsed_seconds=time.perf_counter() - _t0,
        ))
    except Exception:
        pass  # never abort optimization due to callback error
```

Emit at 5 points:
1. `_emit("baseline", baseline_avg, baseline_avg)` after baseline is computed
2. Capture `score_before = current_score` before each field; `_emit("fields", score_before, new_score, field_path, idx)` after
3. `_emit("system_prompt", score_before_sp, current_score)` after system prompt step
4. `_emit("instruction_prompt", score_before_ip, current_score)` after instruction prompt step
5. `_emit("complete", baseline_avg, current_score)` just before `return OptimizationResult(...)`

For the single-pass path in `optimize()` (`sequential=False`): emit `"baseline"` before `.compile()` and `"complete"` after final scoring.

#### E. Update verbose output — remove references to `sequential`, refer to `fast`

In `optimize()` (~line 1248–1269), replace `sequential` references in print statements with `fast`.

---

### 4. `src/dspydantic/prompter.py`

In `Prompter.optimize()` signature, replace `sequential: bool = True` with:
```python
fast: bool = False,
on_progress: Callable[[FieldOptimizationProgress], None] | None = None,
```

Pass both through to `PydanticOptimizer(...)`. Import `FieldOptimizationProgress` from `dspydantic.types`.

---

### 5. `compare_optimization.py` — Update for new API + show progress

- Replace `sequential=True/False` with `fast=False/True`
- Add an `on_progress` callback that prints field-level progress
- Update the theoretical flow descriptions to match new semantics
- Add `fast` comparison to output labels

---

## Files to Modify

| File | Change |
|------|--------|
| `src/dspydantic/types.py` | Add `FieldOptimizationProgress` dataclass |
| `src/dspydantic/__init__.py` | Export `FieldOptimizationProgress` |
| `src/dspydantic/optimizer.py` | Replace `sequential` with `fast`, fast kwargs, num_threads fix, progress callbacks |
| `src/dspydantic/prompter.py` | Replace `sequential` with `fast`, pass `on_progress` |
| `compare_optimization.py` | Update to use `fast=` and `on_progress=` |

---

## Usage After Change

```python
from dspydantic import Prompter, FieldOptimizationProgress

def show_progress(p: FieldOptimizationProgress):
    if p.phase == "fields":
        status = "✓" if p.improved else "–"
        print(f"  [{p.field_index}/{p.total_fields}] {p.field_path}: {p.score_before:.0%} → {p.score_after:.0%} {status}")
    elif p.phase == "complete":
        print(f"Done in {p.elapsed_seconds:.1f}s: {p.score_before:.0%} → {p.score_after:.0%}")

# Default: sequential (quality mode)
result = prompter.optimize(examples=examples, on_progress=show_progress)

# Fast: single-pass with minimal demos
result = prompter.optimize(examples=examples, fast=True, on_progress=show_progress)
```

---

## Verification

1. Run `pytest tests/unit/ -v` — confirm no regressions
2. Run `/Users/davidberenstein/.venv/bin/python3 compare_optimization.py` — shows side-by-side fast vs default with live progress
