# Phase Shift Requirements: Pipeline Output Quality (Phase 15)

**Trigger:** First successful end-to-end pipeline run (Phase 14) produced 40 missions across 7 categories, but output has critical data quality issues: 0/40 missions available, all chain-mission links broken, validation fixes computed but discarded before persistence.

**Inserted after:** Phase 14 (Provider-Agnostic LLM Factory)

## What's Blocked Without This Work

- **TUI is useless** — 0 missions available means the TUI has nothing to display or let users interact with
- **Chain navigation broken** — chains reference missions by titles that don't match, so chain progression doesn't work
- **Validation is wasted compute** — validation runs, fixes data, then the unfixed data gets persisted anyway

## Scope

### Bug 1: Validated data not used for persistence (Critical)

**Root cause:** `pipeline.py:167` stores raw LLM response in `result.category_missions`. `pipeline.py:168` stores validated results in `result.category_validations`. `persister.py:180` reads from `category_missions` — the raw, unvalidated data. Validation fixes (removed bad prerequisites, clamped line numbers, dropped bad chains) are computed then discarded.

**Fix:** Wire validated missions and chains into persistence. Either:
- Option A: Replace `result.category_missions[category_name]` with validated data after validation runs
- Option B: Have `persist_pipeline_data` consume `category_validations` instead of `category_missions`
- Option C: Have pipeline replace the missions/chains in the response object with validated versions

**Affected files:**
- `src/unfamiliar/llm/missions/pipeline.py` — result storage
- `src/unfamiliar/campaign/persister.py` — data consumption (may need signature change)

### Bug 2: Chain `mission_titles` uses Film Noir titles instead of `title_plain`

**Root cause:** The LLM generates chain `mission_titles` using Film Noir themed titles ("The Factory Floor") instead of `title_plain` values ("Understand Factory Pattern"). The schema field description says "Ordered list of title_plain values" but the LLM (especially via JSON text fallback with GLM) doesn't follow this consistently.

**Consequence:** `persister.py:354` looks up `mission_id_map.get(title)` where `mission_id_map` is keyed on `title_plain`. Every lookup fails → all chain links skipped → all "Chain references unknown mission" warnings.

**Fix approach:**
1. **Prompt reinforcement** — Add explicit instruction in `MISSION_PROMPT` that `mission_titles` in chains must use the exact `title_plain` string, not the Film Noir `title`
2. **Defensive matching in persister** — Build a secondary lookup map from `title` → mission ID as fallback when `title_plain` lookup fails
3. **Validation-layer fix** — Add chain title normalization to `_validate_chains()` that maps Film Noir titles to `title_plain` values before validation

**Affected files:**
- `src/unfamiliar/llm/missions/prompts.py` — prompt text
- `src/unfamiliar/campaign/persister.py` — chain linking logic
- `src/unfamiliar/llm/missions/validation.py` — chain validation

### Bug 3: JSON truncation recovery drops all chain data

**Root cause:** `_recover_truncated_json()` in `structured.py:593-694` recovers truncated JSON by finding the last complete mission object, then appends `', "chains": []}'`. All chain data from the LLM response is lost.

**Why it matters:** Even with perfect prompts and validated data, if the LLM response gets truncated (common with GLM), chains are silently wiped to an empty array.

**Fix approach:**
1. **Improve recovery** — After finding last complete mission, also attempt to recover complete chain objects from the remaining text
2. **Validate truncation impact** — If chains were lost during recovery, log a warning so the category can potentially be re-requested
3. **Accept partial chains** — If some chain objects are complete, keep them even if the array is truncated

**Affected files:**
- `src/unfamiliar/llm/structured.py` — `_recover_truncated_json()`

### Validation Warning Volume

Not a bug per se, but the volume of validation warnings (clamped lines, nonexistent files, dropped chains) suggests the prompts could be tighter. The validation layer is doing its job — the question is whether we can reduce the noise at the source.

**Approach:** Review prompt instructions for file references, line ranges, and chain construction. Add concrete constraints where the LLM is consistently getting things wrong. This is secondary to the three bugs above.

**Affected files:**
- `src/unfamiliar/llm/missions/prompts.py`

## Phase 14 Review Fold-In

Phase 14 review is ongoing. Any remaining review items not already addressed (the `str.append()` fix, persister schema fix, campaign overwrite fix are done) should be checked during this phase's review cycle. Specifically:
- Verify Phase 14 test assertions are correct for new defaults (glm-5, api.z.ai URLs)
- Verify `supports_tool_calling` flag routes correctly through `create_structured_chain`
- Check for any remaining dead code from the `ChatZhipuAI` removal

## Does This Change the Data Model?

**No.** The DB schema is correct. Missions, chains, prerequisites, and chain-mission links all have the right columns. The problem is entirely upstream: wrong/unvalidated data reaches the persister.

## Does This Affect Already-Built Phases?

- **Phase 7** (mission generation): Prompt changes affect `prompts.py`
- **Phase 10** (pipeline stabilization): Pipeline result storage changes in `pipeline.py`; truncation recovery changes in `structured.py`
- **Phase 11** (interface contracts): Contracts for `persist_pipeline_data` may need updating if signature changes
- **Phase 13** (campaign orchestration): No changes needed — orchestrator calls persist correctly, it's the data that's wrong

## Success Criteria

1. Re-running `unfamiliar investigate` on a test repo produces missions where:
   - Root missions (no prerequisites) have status=AVAILABLE
   - Chain-mission links are successfully created (no "unknown mission" warnings)
   - Validated data (clamped lines, cleaned prerequisites) is what gets persisted
2. Validation warnings are reduced (not eliminated — LLM output will always need some fixing)
3. JSON truncation recovery preserves chain data when possible
4. All existing tests pass (888+)
