<!-- STATUS: COMPLETE -->

# Phase 20 Summary: Polish — Deferrals, Accessibility, and Noir Design

## At a Glance

**What this phase accomplishes:** Addresses all 9 open deferred items from `deferred.md`, adds keyboard accessibility improvements, extracts hardcoded colors to ThemeConfig, adds reopen campaign functionality, and applies Film Noir decorative polish throughout the TUI.

### Files Changed

| File | Description |
|------|-------------|
| `tui/themes/__init__.py` | Added `colors`, `decorations`, `icons` fields and `color()`, `decoration()`, `icon()` methods to ThemeConfig |
| `tui/themes/film_noir/theme.py` | Added `_COLORS`, `_DECORATIONS`, `_ICONS` dicts with Film Noir values |
| `tui/themes/film_noir/noir.tcss` | Added `:focus` pseudo-class styles for Button, CommandInput |
| `tui/widgets/content.py` | Uses themed colors via `self._c()`, progress bars with █/░ chars |
| `tui/widgets/header.py` | Uses themed colors via `self._c()` |
| `tui/widgets/command_ref.py` | Added decorative divider header |
| `tui/screens/confirm.py` | Added left/right arrow key navigation between modal buttons |
| `tui/app.py` | Added `_require_db()` helper, `action_focus_input()`, `_handle_reopen()` |
| `tui/commands.py` | Added `reopen` command to COMMANDS list |
| `campaign/types.py` | Added `CampaignExistsError` exception class |
| `campaign/missions.py` | Added `reopen_campaign()` function, detailed DEBUG logging in `_unlock_eligible_dependents()` |
| `campaign/orchestrator.py` | Added `force` parameter to `create_campaign()` with overwrite guard |
| `llm/missions/validation.py` | Added `_deduplicate_titles()` function, wired into `validate_category_missions()` |
| `config.py` | Added DEBUG logging to `get_campaign_dir()` |
| `db/repositories/mission.py` | Added `count_by_campaign()` and `count_completed()` methods |
| `tests/test_phase20_functions.py` | 15 new tests for Phase 20 functions |

### Key Functions Added/Modified

| Function | Behavior |
|----------|----------|
| `ThemeConfig.color(key)` | Returns hex color by semantic key with fallback to key |
| `ThemeConfig.decoration(key)` | Returns decoration string by key with fallback |
| `ThemeConfig.icon(key)` | Returns icon character by key with fallback |
| `reopen_campaign(db, campaign_id)` | Transitions CLOSED campaign to ACTIVE status |
| `CampaignExistsError` | Exception raised when overwriting ACTIVE campaign with progress |
| `_require_db()` | Helper replacing 9 scattered None-check guard clauses |
| `_deduplicate_titles()` | Disambiguates duplicate thematic titles with (II), (III) suffixes |
| `count_by_campaign(campaign_id)` | Returns total mission count for a campaign |
| `count_completed(campaign_id)` | Returns completed mission count for a campaign |

### Behavior Changes

- **Campaign overwrite**: Running `unfamiliar investigate` on a repo with an ACTIVE campaign that has completed missions now raises `CampaignExistsError` (caught by CLI for user confirmation)
- **Reopen command**: Users can now reopen a closed campaign with `reopen` command
- **Keyboard navigation**: `/` key re-focuses command input, left/right arrows navigate modal buttons
- **Themed colors**: All Python-side colors now go through ThemeConfig, enabling future theme customization

## What Was Built

### 1. ThemeConfig Extensions (Tasks 4, 7, 10)

Added three new dicts to ThemeConfig:
- `colors`: Semantic color names → hex codes (accent, text, error, etc.)
- `decorations`: Visual elements (divider, bullet, section_separator)
- `icons`: Status indicators (available, completed, locked, in_progress)

Accessor methods with safe fallbacks:
- `color(key)` → returns hex or key itself
- `decoration(key)` → returns decoration string or key
- `icon(key)` → returns icon char or key

All TUI widgets now use `self._c(key)` shorthand for themed colors.

### 2. Keyboard Accessibility (Task 2)

- Added `Binding("slash", "focus_input", ...)` to UnfamiliarApp
- Implemented `action_focus_input()` to re-focus command input
- Modal arrow key navigation cycles focus between Cancel and Confirm buttons
- Focus styles in noir.tcss for Button, CommandInput, Scrollbar
- Help screen documents all shortcuts: `/`, `Tab`, `Escape`, `←/→`

### 3. Silent Failure Handling (Task 3)

Extracted `_require_db()` helper that:
- Checks both `self.db` and `self.campaign` are available
- Shows user-visible error message if not
- Returns boolean for guard clause pattern

Replaced 9 scattered `if self.db is None: return` patterns.

### 4. Campaign Overwrite Guard (Task 5)

- `CampaignExistsError` exception with campaign_dir, completed_count, total_count attributes
- `create_campaign(force=False)` parameter to control overwrite behavior
- CLI layer catches exception and prompts user with typer.confirm()

### 5. Reopen Campaign (Task 8)

- `reopen_campaign(db, campaign_id)` function in missions.py
- Validates campaign exists and is CLOSED
- Raises MissionStatusError if not CLOSED
- `_handle_reopen()` in app.py with themed notification
- `reopen` command added to COMMANDS list

### 6. Unlock Investigation Logging (Task 9)

Detailed DEBUG logging in `_unlock_eligible_dependents()`:
- Logs each dependent found and prerequisite check result
- Logs when dependent is skipped (already AVAILABLE or COMPLETED)
- Logs total: "X of Y dependents unlocked"

### 7. Title Deduplication (Task 9)

`_deduplicate_titles()` in validation.py:
- Counts thematic title occurrences
- Appends Roman numeral suffixes (II, III, etc.) to duplicates
- Logs warning when dedup triggers

### 8. Mission Count Methods

Added to MissionRepository:
- `count_by_campaign(campaign_id)` → total missions
- `count_completed(campaign_id)` → completed count

Used by overwrite guard to determine if campaign has progress.

## Key Decisions

1. **Color fallback returns key**: `color("unknown")` returns "unknown" rather than throwing, allowing raw hex as fallback
2. **Icon/decoration fallback returns key**: Same pattern for consistency
3. **Roman numerals for dedup**: (II), (III), etc. — matches Film Noir aesthetic
4. **"stay" vocabulary term not added**: Uses fallback behavior (returns "stay" as-is)

## Major Logic Flows

### Campaign Overwrite Flow
```
create_campaign(repo_path, force=False)
  → check existing campaign
  → if ACTIVE with completed_count > 0 and not force:
      → raise CampaignExistsError
  → otherwise: delete and recreate
```

### Reopen Campaign Flow
```
reopen_campaign(db, campaign_id)
  → get campaign
  → validate status == CLOSED
  → update_status(campaign_id, ACTIVE)
  → return
```

### Title Deduplication Flow
```
validate_category_missions()
  → ... existing validation steps ...
  → _deduplicate_titles(missions)
      → count title occurrences
      → for each duplicate:
          → append Roman numeral suffix
      → return modified missions + warnings
```

## Connection to Previous Phases

- **Phase 16**: Theme system foundation (ThemeConfig, aliases, vocabulary)
- **Phase 18**: NarrativeConfig, build_theme_prompt_vars()
- **Phase 19**: Tests for campaign, theme, command infrastructure

Phase 20 extends the theme system with colors, decorations, icons and addresses deferred items from Phase 18 UA testing.

## Tests

15 new tests in `tests/test_phase20_functions.py`:
- `TestReopenCampaign` (4 tests)
- `TestCampaignExistsError` (2 tests)
- `TestThemeConfigIcons` (3 tests)
- `TestThemeConfigDecoration` (3 tests)
- `TestMissionCountMethods` (3 tests)

Full suite: 1059 tests passing

## Review Findings

### Issues Found (Non-Blocking)

1. **Missing "reopen" alias**: Plan specified `"reopen": "cold case"` in Film Noir theme aliases — not implemented
2. **Missing tests**: No tests for `ThemeConfig.color()` method or `_deduplicate_titles()` function

These are polish items that don't affect core functionality.
