---
phase: 50-repair-blind-spots
plan: 001
type: execute
wave: 1
depends_on: [49-module-hardening]
files_modified:
  - paperforge/worker/repair.py
autonomous: true
requirements:
  - REPAIR-01
  - REPAIR-02
  - REPAIR-03
  - REPAIR-04
user_setup: []

must_haves:
  truths:
    - "paperforge repair detects ocr_status pending in formal note vs done/failed in meta.json — previously silently skipped"
    - "paperforge repair --fix either handles every detected divergence type or prints explicit [WARNING] for unhandled types"
    - "paperforge repair --fix logs index write failures via logger.warning() instead of silent except:pass"
    - "repair.py contains zero dead code — no orphaned load_domain_config call or unused dict comprehension"
  artifacts:
    - path: "paperforge/worker/repair.py"
      provides: "Repair worker with all 4 fixes applied"
      min_lines: 400
      contains: "logger.warning"
  key_links:
    - from: "paperforge/worker/repair.py:252-258"
      to: "formal note frontmatter ocr_status and meta.json ocr_status"
      via: "Condition 4 comparison now includes note_ocr_status == pending vs meta_ocr_status in (done, failed)"
      pattern: "note_ocr_status.*pending.*meta_ocr_status"
    - from: "paperforge/worker/repair.py:278-363"
      to: "Console output"
      via: "WARNING printed for unhandled fix cases"
      pattern: "WARNING.*No.*fix.*handler"
    - from: "paperforge/worker/repair.py:306,348,356"
      to: "logger"
      via: "bare except:pass replaced with logger.warning()"
      pattern: "logger\\.warning\\(\"Failed to"
---

<objective>
Fix all 4 repair worker blind spots: condition 4 divergence detection, --fix mode coverage for all types, silent exception swallowing, and dead code removal.

Purpose: Ensure the repair worker (run_repair()) reliably detects all 3-way divergence conditions and handles them transparently — no silent gaps in detection, no silent skips in fix mode, no silent failures in error handling, no dead code.

Output: Modified paperforge/worker/repair.py with all 4 fixes applied.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/49-module-hardening/49-SUMMARY.md
@paperforge/worker/repair.py

<interfaces>
From paperforge/worker/repair.py:

```python
# Line 9 — unused import to remove
from paperforge.worker._domain import load_domain_config

# Lines 195-196 — dead code to remove
config = load_domain_config(paths)
{entry["export_file"]: entry["domain"] for entry in config["domains"]}

# Lines 249-265 — divergence detection (6 conditions)
if meta_validated_status == "done_incomplete":          # condition 1
elif note_ocr_status == "done" and meta in (pending, processing, None):  # condition 2
elif index_ocr_status == "done" and (meta is None or done_incomplete):   # condition 3
elif (note_ocr_status != "pending" and ... and note_ocr_status != meta_validated_status):  # condition 4 — MISSES pending vs done/failed

# Lines 278-363 — --fix mode (only handles 2 of 6 types)
# Needs: all types handled or explicit [WARNING] printed

# Lines 226, 306-307, 347-348, 355-356 — bare except Exception: pass
# Needs: logger.warning() with descriptive message
</interfaces>

Established logging pattern from Phase 49 (discussion.py hardening):
```python
logger.warning("Failed to refresh index for %s: %s", zotero_key, e)
```
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Remove dead code + fix condition 4 divergence detection (REPAIR-04 + REPAIR-01)</name>
  <files>paperforge/worker/repair.py</files>
  <behavior>
    - REPAIR-04: `load_domain_config` import (line 9) and dead code block (lines 195-196) removed
    - REPAIR-01: Condition 4 at lines 258-263 now ALSO detects `note_ocr_status == "pending"` vs `meta_ocr_status in ("done", "failed")` divergence

    Test expectations:
    - Test 1: A formal note with `ocr_status: pending` and a meta.json with `ocr_status: done` produces a divergent item in the result (was previously silently skipped)
    - Test 2: A formal note with `ocr_status: pending` and a meta.json with `ocr_status: failed` produces a divergent item in the result (was previously silently skipped)
    - Test 3: A formal note with `ocr_status: pending` and a meta.json with `ocr_status: pending` produces NO divergent item (consistent pending state is not a divergence)
    - Test 4: Dead `load_domain_config` import no longer present in repair.py
    - Test 5: No orphaned `load_domain_config` call or unused dict comprehension at former line 195-196
  </behavior>
  <action>
    Edit `paperforge/worker/repair.py`:

    **1. Remove unused import (REPAIR-04):**
    Delete line 9: `from paperforge.worker._domain import load_domain_config`

    **2. Remove dead code block (REPAIR-04):**
    Delete lines 195-196:
    ```
    config = load_domain_config(paths)
    {entry["export_file"]: entry["domain"] for entry in config["domains"]}
    ```

    **3. Fix condition 4 detection to include pending vs done/failed (REPAIR-01):**
    Replace the elif condition at lines 258-265 (the current condition 4 block):
    ```python
        elif (
            note_ocr_status != "pending"
            and meta_ocr_status is not None
            and meta_validated_status is not None
            and note_ocr_status != meta_validated_status
        ):
            is_divergent = True
            div_reason = f"formal_note={note_ocr_status} vs meta post-validation={meta_validated_status}"
    ```
    With a combined condition that ALSO catches the missing case:
    ```python
        elif (
            meta_ocr_status is not None
            and meta_validated_status is not None
            and note_ocr_status != meta_validated_status
            and not (
                note_ocr_status == "pending"
                and meta_validated_status == "pending"
            )
        ):
            is_divergent = True
            div_reason = f"formal_note={note_ocr_status} vs meta post-validation={meta_validated_status}"
    ```

    Design rationale:
    - Removing `note_ocr_status != "pending"` as the sole guard — this was too broad and excluded legitimate divergences where the note lags behind meta
    - The new guard `not (note_ocr_status == "pending" and meta_validated_status == "pending")` ensures: consistent pending states are NOT flagged as divergent; but note=pending vs meta=done/failed IS flagged
    - `meta_validated_status` handles normalized values from `validate_ocr_meta()` including "done" and "failed" — already accounts for "done_incomplete" normalization
  </action>
  <verify>
    <automated>pytest tests/test_repair.py -q --tb=short 2>&1</automated>
  </verify>
  <done>
    1. `load_domain_config` import removed from repair.py
    2. Dead code block (config = load_domain_config + unused dict comp) removed
    3. Condition 4 catches note_ocr_status == "pending" vs meta_ocr_status in ("done", "failed") — verified by tests passing
    4. All existing repair tests still pass
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Replace bare except:pass with logger.warning + extend --fix mode (REPAIR-03 + REPAIR-02)</name>
  <files>paperforge/worker/repair.py</files>
  <behavior>
    - REPAIR-03: 4 bare `except Exception: pass` blocks replaced with `logger.warning()` calls
    - REPAIR-02: --fix mode gains an `else` clause that prints `[WARNING]` for any unhandled divergence type

    Test expectations:
    - Test 1: Index load failure (line 226) logs a warning — verified via caplog
    - Test 2: Index write failure in first --fix branch (line 306) logs a warning — verified via caplog
    - Test 3: Index write failure in second --fix branch (line 348) logs a warning — verified via caplog
    - Test 4: Meta write failure (line 356) logs a warning — verified via caplog
    - Test 5: A divergence type with no explicit --fix handler prints `[WARNING]` containing the zotero_key and reason — verified via capsys
  </behavior>
  <action>
    Edit `paperforge/worker/repair.py`:

    **1. Line 226-227: Index load exception (outside --fix block):**
    Replace:
    ```python
            except Exception:
                pass
    ```
    With:
    ```python
            except Exception as e:
                logger.warning("Failed to load index for %s: %s", zotero_key, e)
    ```

    **2. Lines 306-307: Index write exception (first --fix branch, meta missing/invalid):**
    Replace:
    ```python
                            except Exception:
                                pass
    ```
    With:
    ```python
                            except Exception as e:
                                logger.warning("Failed to update index entry for %s: %s", zotero_key, e)
    ```

    **3. Lines 347-348: Index write exception (second --fix branch, note done vs meta pending):**
    Replace:
    ```python
                            except Exception:
                                pass
    ```
    With:
    ```python
                            except Exception as e:
                                logger.warning("Failed to update index entry for %s: %s", zotero_key, e)
    ```

    **4. Lines 355-356: Meta write exception (second --fix branch):**
    Replace:
    ```python
                            except Exception:
                                pass
    ```
    With:
    ```python
                            except Exception as e:
                                logger.warning("Failed to reset meta ocr_status for %s: %s", zotero_key, e)
    ```

    **5. Add --fix else clause for unhandled types (REPAIR-02):**
    After the existing `elif note_ocr_status == "done" and meta_ocr_status in ("pending", "processing"):` block (which ends around line 363), add an `else:` clause that prints a warning for any divergence type that has no explicit fix handler:

    Insert after line 363 (`fixed_formal_note_primary = True`) and before line 364 (`fixed_count = sum(...)`), i.e. before the fixed count line:

    Looking at the code flow: after the elif block at line 324-363, the next line is 364 `fixed_count = sum([fixed_formal_note_primary, fixed_index_entry, fixed_meta])`. We need to add an `else:` before that.

    The current outermost if/elif structure is:
    ```python
    if meta_ocr_status is None or meta_validated_status == "done_incomplete":
        # ... fix handling ...
    elif note_ocr_status == "done" and meta_ocr_status in ("pending", "processing"):
        # ... fix handling ...
    # NO ELSE — silently skips conditions 3, 4, and the new pending-vs-done/failed case
    ```

    Add after line 363 (the last `fixed_formal_note_primary = True` of the second branch):
    ```python
                else:
                    print(f"[WARNING] No --fix handler for {zotero_key}: {div_reason}")
    ```

    Verified location: The `else:` must be at the same indentation level as the existing `if` and `elif` (12 spaces, inside `if fix:` block). This ensures any divergence type not explicitly handled by the two if/elif branches prints a clear warning to the user.
  </action>
  <verify>
    <automated>pytest tests/test_repair.py -q --tb=short 2>&1</automated>
  </verify>
  <done>
    1. All 4 bare `except Exception: pass` blocks replaced with `logger.warning()` calls with descriptive messages
    2. --fix mode warns on unhandled divergence types via `[WARNING]` console output
    3. All existing repair tests still pass
  </done>
</task>

</tasks>

<verification>
All changes are in `paperforge/worker/repair.py`. Run full test suite for repair module:
```bash
pytest tests/test_repair.py -v --tb=short
```

Verify specific behaviors:
1. REPAIR-01: grep for `load_domain_config` in repair.py — should not appear
2. REPAIR-03: grep for `except Exception:\s+pass` in repair.py — should not appear (bare pass)
3. REPAIR-02: grep for `WARNING.*No.*--fix.*handler` in repair.py — should exist
4. All 464+ lines of test_repair.py pass with zero failures
</verification>

<success_criteria>
1. `paperforge repair` detects the missing condition 4 divergence: formal note `ocr_status: pending` vs meta.json `ocr_status: done/failed` — previous blind spot closed
2. `paperforge repair --fix` handles all 6 detected divergence types or produces explicit `[WARNING]` console lines for unhandled ones
3. `paperforge repair --fix` logs index write failures via `logger.warning()` — no silent `except Exception: pass` remains
4. Dead `load_domain_config` import, call, and unused dict comprehension removed from repair.py
5. All existing repair tests pass with zero regressions
</success_criteria>

<output>
After completion, create `.planning/phases/50-repair-blind-spots/50-001-SUMMARY.md`
</output>
