# S04: Expand to Pipeline Stages 2-4

**Goal:** Extend the prompt optimization loop from stage-5-only to stages 2-4, with stage-appropriate scoring rubrics, fixture formats, and variant validation — so `optimize --stage N` works for any pipeline stage.
**Demo:** After this: Run `python -m pipeline.quality optimize --stage 3 --iterations 5` — optimizes extraction prompts with stage-appropriate scoring

## Tasks
- [x] **T01: Added STAGE_CONFIGS registry (stages 2-5) with per-stage rubrics, generalized ScoreResult to scores dict, and made variant generator stage-agnostic** — Build the STAGE_CONFIGS registry in scorer.py that maps each pipeline stage (2-5) to its scoring rubric, dimensions list, format markers, fixture key requirements, prompt file name, and output schema class. Generalize ScoreResult to use a `scores: dict[str, float]` field instead of 5 named fields (keep backward compat via properties). Add a `score_stage_output()` method to ScoreRunner that accepts arbitrary stage output + input and scores using the stage's rubric. Update variant_generator.py to accept format markers as a parameter rather than using the hardcoded `_FORMAT_MARKERS` list, and generalize the meta-prompt to work for any stage (not just synthesis).

## Context

The existing scorer has a hardcoded `SCORING_RUBRIC` with 5 stage-5 dimensions (structural, content_specificity, voice_preservation, readability, factual_fidelity). `ScoreResult` has these as named float fields. The variant generator has hardcoded `_FORMAT_MARKERS = ['SynthesisResult', '"pages"', 'body_sections', 'title', 'summary']` and a `VARIANT_META_PROMPT` that references 'synthesis prompt' language.

Stages 2-4 need different rubrics:
- Stage 2 (segmentation): coverage_completeness, topic_specificity, boundary_accuracy, summary_quality
- Stage 3 (extraction): moment_richness, timestamp_accuracy, content_type_correctness, summary_actionability, plugin_normalization  
- Stage 4 (classification): category_accuracy, tag_completeness, tag_specificity, coverage, no_overlap

Format markers per stage:
- Stage 2: `'segments'`, `'start_index'`, `'end_index'`, `'topic_label'`
- Stage 3: `'moments'`, `'content_type'`, `'raw_transcript'`, `'plugins'`
- Stage 4: `'classifications'`, `'moment_index'`, `'topic_category'`, `'topic_tags'`
- Stage 5: `'SynthesisResult'`, `'"pages"'`, `'body_sections'`, `'title'`, `'summary'` (existing)

Prompt files: `stage2_segmentation.txt`, `stage3_extraction.txt`, `stage4_classification.txt`, `stage5_synthesis.txt`
Schemas: `SegmentationResult`, `ExtractionResult`, `ClassificationResult`, `SynthesisResult` (all in `pipeline.schemas`)
  - Estimate: 1.5h
  - Files: backend/pipeline/quality/scorer.py, backend/pipeline/quality/variant_generator.py
  - Verify: cd backend && python -c "from pipeline.quality.scorer import STAGE_CONFIGS, ScoreResult, ScoreRunner, DIMENSIONS; assert 2 in STAGE_CONFIGS and 3 in STAGE_CONFIGS and 4 in STAGE_CONFIGS and 5 in STAGE_CONFIGS; r = ScoreResult(scores={'structural': 0.8, 'readability': 0.7}, composite=0.75); print('scorer ok')" && python -c "from pipeline.quality.variant_generator import PromptVariantGenerator; print('generator ok')"
- [x] **T02: Generalized OptimizationLoop to stages 2-5 with per-stage fixture validation, schema dispatch, and user prompt building; created stage 2-4 fixtures; removed stage-5 gate from CLI** — Make OptimizationLoop stage-aware: generalize _load_fixture() to validate stage-specific keys, generalize _score_variant() to call the correct prompt and parse the correct schema per stage, and pass stage-appropriate format markers to the variant generator. Create minimal fixture JSON files for stages 2-4. Remove the stage-5 gate in __main__.py's _run_optimize(), add validation for stages 2-5. Verify all stages import and CLI accepts them.

## Context

The optimizer currently has three stage-5-specific hardcodings:
1. `_load_fixture()` expects `creator_name` and `moments` keys — stages 2-4 have different input shapes
2. `_score_variant()` calls synthesis via `SynthesisResult` schema and formats output as a technique page for the scorer
3. The `run()` method loads `stage{N}_synthesis.txt` — stages 2-4 use different prompt file names

The CLI's `_run_optimize()` rejects `args.stage != 5` with an error.

Stage fixture shapes (from research):
- Stage 2: `{transcript_segments: [{index, start_time, end_time, text}]}` — segments of a transcript
- Stage 3: `{topic_label, segments: [{start_time, end_time, text}]}` — a topic group to extract moments from
- Stage 4: `{moments: [{title, summary, content_type, plugins}], taxonomy: "..."}` — moments to classify
- Stage 5: `{creator_name, moments: [...]}` (existing sample_moments.json)

The STAGE_CONFIGS registry from T01 provides the prompt filename, schema class, dimensions, and format markers per stage. This task uses that registry to dispatch correctly.

After changes, `OptimizationLoop.run()` should:
1. Load the prompt file from `STAGE_CONFIGS[stage]['prompt_file']`
2. Load and validate fixture using `STAGE_CONFIGS[stage]['fixture_keys']`
3. In `_score_variant()`, use the stage's schema to parse LLM output, then format it for the scorer's `score_stage_output()`
4. Pass stage-appropriate format markers to variant generator
  - Estimate: 2h
  - Files: backend/pipeline/quality/optimizer.py, backend/pipeline/quality/__main__.py, backend/pipeline/quality/fixtures/sample_segments.json, backend/pipeline/quality/fixtures/sample_topic_group.json, backend/pipeline/quality/fixtures/sample_classifications.json
  - Verify: cd backend && python -c "from pipeline.quality.optimizer import OptimizationLoop; print('optimizer ok')" && python -c "from pipeline.quality.__main__ import main; print('cli ok')" && python -m pipeline.quality optimize --stage 2 --iterations 1 --file pipeline/quality/fixtures/sample_segments.json --help 2>&1 | head -1 && python -m pipeline.quality optimize --stage 6 --file x 2>&1 | grep -q 'stage' && echo 'stage6 rejected ok'
