---
phase: 17-dead-code-precommit
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - pyproject.toml
  - .pre-commit-config.yaml
  - scripts/consistency_audit.py
  - paperforge/worker/ocr.py
  - paperforge/worker/sync.py
  - paperforge/worker/deep_reading.py
  - paperforge/worker/repair.py
  - paperforge/worker/status.py
  - paperforge/worker/update.py
  - paperforge/worker/base_views.py
  - tests/test_legacy_worker_compat.py
autonomous: true
requirements:
  - CH-04
  - DX-01
  - DX-02
  - OBS-05

must_haves:
  truths:
    - "ruff check --fix passes with zero warnings across the entire codebase"
    - ".pre-commit-config.yaml exists at repo root with ruff lint+format, check-yaml, check-toml, end-of-file-fixer, trailing-whitespace, and custom consistency-audit hooks"
    - "scripts/consistency_audit.py has a Check 5 that detects worker modules with duplicated utility functions outside the re-export from _utils.py"
    - "OCR error messages in meta.json include library_record (zotero_key) field alongside existing error and suggestion fields"
    - "No delegation wrapper functions (load_vault_config) remain in any worker module — all callers import directly from paperforge.config"
    - "All existing tests pass with zero failures"
  artifacts:
    - path: ".pre-commit-config.yaml"
      provides: "Pre-commit hook configuration at repo root"
      min_lines: 20
    - path: "scripts/consistency_audit.py"
      provides: "Check 5 — duplicate utility function detection"
      contains: "def check_duplicate_utils"
    - path: "pyproject.toml"
      provides: "[tool.ruff] section with rules E/F/I/UP/B/SIM targeting Python 3.10+"
      contains: "target-version = \"py310\""
    - path: "paperforge/worker/ocr.py"
      provides: "library_record field in error meta.json for upload and poll error handlers"
      contains: "meta['library_record']"
  key_links:
    - from: ".pre-commit-config.yaml"
      to: "scripts/consistency_audit.py"
      via: "custom hook entry"
      pattern: "scripts/consistency_audit.py"
    - from: "paperforge/worker/ocr.py poll error handler (lines 1218-1227)"
      to: "paperforge/ocr_diagnostics.py::classify_error"
      via: "classify_error(e, response)"
      pattern: "classify_error"
    - from: "paperforge/worker/ocr.py upload error handler (lines 1319-1328)"
      to: "paperforge/ocr_diagnostics.py::classify_error"
      via: "classify_error(e, response)"
      pattern: "classify_error"

---

<objective>
Remove dead code, install automated pre-commit safety nets, and add actionable OCR error context.

**Purpose:** Clean up unused imports, remove delegation wrappers that add unnecessary indirection, prevent future code duplication via pre-commit hooks, and make OCR error messages actionable by including the library-record name (zotero_key).

**Output:**
- `.pre-commit-config.yaml` at repo root with 6 hooks
- `[tool.ruff]` in `pyproject.toml` targeting Python 3.10+ (rules E, F, I, UP, B, SIM)
- Check 5 (duplicate-utility detection) added to `scripts/consistency_audit.py`
- All 7 worker modules: unused imports removed, `load_vault_config` delegation wrappers replaced with direct `paperforge.config` imports
- OCR error meta.json includes `library_record` field with zotero_key
- All tests pass after refactoring

**Note:** CH-04(a) — `UPDATE_*` constants from `status.py` — is already satisfied. These constants were removed during Phase 12 (architecture cleanup). No action needed.
</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/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md

## Source Files (primary edit targets)

@paperforge/worker/sync.py
@paperforge/worker/ocr.py
@paperforge/worker/deep_reading.py
@paperforge/worker/repair.py
@paperforge/worker/status.py
@paperforge/worker/update.py
@paperforge/worker/base_views.py
@scripts/consistency_audit.py
@pyproject.toml
@paperforge/ocr_diagnostics.py
@paperforge/config.py

## Test Files (import updates)

@tests/test_legacy_worker_compat.py

## Previous Phase Summary Patterns

@.planning/phases/12-architecture-cleanup/12-01-SUMMARY.md
</context>

<interfaces>
```
From paperforge/config.py:
  def load_vault_config(vault, env=None, overrides=None) -> dict[str, str]
  def paperforge_paths(vault, cfg=None) -> dict[str, Path]

From paperforge/ocr_diagnostics.py:
  def classify_error(exception: Exception, response) -> tuple[str, str]
    Returns (state, suggestion) — state is 'blocked' or 'error'

Current delegation wrapper pattern (identical in all 7 worker modules):
  def load_vault_config(vault: Path) -> dict:
      from paperforge.config import load_vault_config as _shared
      return _shared(vault)

  def pipeline_paths(vault: Path) -> dict[str, Path]:
      from paperforge.config import paperforge_paths as _shared
      shared = _shared(vault)
      cfg = load_vault_config(vault)
      # ... adds worker-only keys (same 17 extra keys in every module)
      return {**shared, ...}
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1 (Wave 1): Configuration scaffolding — ruff, pre-commit, Check 5, OBS-05</name>
  <files>
    pyproject.toml
    .pre-commit-config.yaml
    scripts/consistency_audit.py
    paperforge/worker/ocr.py
  </files>
  <action>
    This task creates all new config files and makes targeted small changes. Four distinct work items, order within task does not matter:

    ---
    **1a. Add [tool.ruff] section to pyproject.toml**

    Append at end of file (after last line `[tool.pytest.ini_options]` section):

    ```toml
    [tool.ruff]
    target-version = "py310"
    line-length = 120

    [tool.ruff.lint]
    select = ["E", "F", "I", "UP", "B", "SIM"]
    ignore = ["B904"]  # raise-within-try is intentional in many worker error paths

    [tool.ruff.format]
    quote-style = "double"
    ```

    Per D-06: standard E/F/I/UP/B/SIM rules, Python 3.10+ target. B904 ignored because existing code intentionally uses bare `raise` in try/except blocks for read_json/write_json error paths.

    ---
    **1b. Create .pre-commit-config.yaml at repo root**

    ```yaml
    repos:
      - repo: https://github.com/astral-sh/ruff-pre-commit
        rev: v0.4.0
        hooks:
          - id: ruff
            args: [--fix]
          - id: ruff-format

      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v4.6.0
        hooks:
          - id: check-yaml
          - id: check-toml
          - id: end-of-file-fixer
          - id: trailing-whitespace

      - repo: local
        hooks:
          - id: consistency-audit
            name: PaperForge Consistency Audit
            entry: python scripts/consistency_audit.py
            language: system
            pass_filenames: false
    ```

    Per D-01 and DX-01. Custom hook calls `scripts/consistency_audit.py` directly (no separate wrapper script per D-03). Do NOT install hooks automatically (DX-04 deferred to Phase 18).

    ---
    **1c. Add Check 5 (duplicate-utility detection) to scripts/consistency_audit.py**

    Add a new function `check_duplicate_utils()` following the same pattern as existing checks (returns `tuple[int, list[str]]`). Register it in the `checks` list inside `main()`.

    The logic:
    1. Scan all `.py` files in `paperforge/worker/` (excluding `_utils.py`, `_retry.py`, `_progress.py`, `__init__.py`)
    2. For each file, parse its source and look for function definitions that match names of functions exported from `paperforge/worker/_utils.py`
    3. The "known shared" function names from `_utils.py`: `read_json`, `write_json`, `read_jsonl`, `write_jsonl`, `yaml_quote`, `yaml_block`, `yaml_list`, `slugify_filename`, `_extract_year`, `load_journal_db`, `lookup_impact_factor`, `scan_library_records`, `_resolve_formal_note_path`
    4. For each function body found in a worker module, check whether it only contains a `from _utils import ...` re-export comment and call — if so, it's legitimate. If it defines the function with actual code, flag it as a violation
    5. A violation entry should show: `[<module_path>] Function '<name>' is defined locally but exists in _utils.py`

    Use `ast` module for robust detection (parse with `ast.parse`, walk FunctionDef nodes, compare names against the known list).

    Per D-02: "Scans all 7 worker modules for functions that are re-exported from _utils.py but still have local copies."

    ---
    **1d. OBS-05: Add library_record field to OCR error meta.json**

    In `paperforge/worker/ocr.py`, modify two error handling blocks:

    **Poll error handler (around lines 1218-1227):** After each `meta['ocr_status'] = state` block, add:
    - `meta['library_record'] = key` (where `key` is the `zotero_key` already available in the loop from `queue_row['zotero_key']`)

    The three error branches in the poll section (around lines 1219-1243) are:
    1. `except Exception as e:` (line 1218) — sets `state, suggestion = classify_error(e, ...)` → add `meta['library_record'] = key`
    2. `except (json.JSONDecodeError, KeyError) as e:` (line 1233) — sets `state, suggestion = classify_error(e, None)` → add `meta['library_record'] = key`
    3. The `else:` branch (line 1268) — `meta['ocr_status'] = 'error'` → add `meta['library_record'] = key`

    **Upload error handler (around lines 1319-1328):** After `meta['ocr_status'] = state`, add:
    - `meta['library_record'] = key`

    Per D-04: "Minimal change: add zotero_key (library-record name) to the error context passed to existing error handling paths." Per D-05: "Error messages in meta.json get an additional library_record field containing the zotero_key for context."

    Do NOT restructure the error message format — just add the field.
  </action>
  <verify>
    <automated>
      # Verify config syntax
      python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb')); print('pyproject.toml: valid')"
      python -c "
      import yaml
      yaml.safe_load(open('.pre-commit-config.yaml', encoding='utf-8'))
      print('.pre-commit-config.yaml: valid')
      "
      # Verify Check 5 exists
      grep -n "def check_duplicate_utils" scripts/consistency_audit.py
      # Verify OBS-05 changes
      grep -n "library_record" paperforge/worker/ocr.py
      # Verify consistency_audit can import
      python -c "import sys; sys.path.insert(0, '.'); from scripts.consistency_audit import check_duplicate_utils; print('Check 5 importable')"
    </automated>
  </verify>
  <done>
    - pyproject.toml has [tool.ruff] section with target-version="py310" and rules E/F/I/UP/B/SIM
    - .pre-commit-config.yaml exists with 6 hooks (ruff lint, ruff format, check-yaml, check-toml, end-of-file-fixer, trailing-whitespace, consistency-audit)
    - scripts/consistency_audit.py has check_duplicate_utils() registered in main's checks list
    - ocr.py error paths in meta.json include "library_record" field set to the zotero_key
  </done>
</task>

<task type="auto">
  <name>Task 2 (Wave 2): Dead code sweep — ruff check --fix + delegation wrapper removal + verify</name>
  <files>
    pyproject.toml
    paperforge/worker/ocr.py
    paperforge/worker/sync.py
    paperforge/worker/deep_reading.py
    paperforge/worker/repair.py
    paperforge/worker/status.py
    paperforge/worker/update.py
    paperforge/worker/base_views.py
    tests/test_legacy_worker_compat.py
  </files>
  <action>
    This task removes dead code and refactors delegation wrappers across all 7 worker modules. Execute in order.

    **IMPORTANT — Dependency:** Task 1 must be complete first (the [tool.ruff] section must exist so `ruff check --fix` uses the correct rule set).

    ---
    **Step 2a: Install ruff and add test dependency**

    Add `"ruff>=0.4.0"` to the `[project.optional-dependencies] test` list in `pyproject.toml`. Then:
    ```
    pip install ruff pre-commit
    ```

    ---
    **Step 2b: Run ruff check --fix to auto-remove unused imports**

    ```
    ruff check --fix paperforge/ scripts/ tests/
    ```

    This removes unused imports flagged by rules E (pycodestyle), F (pyflakes), I (isort), UP (pyupgrade), B (flake8-bugbear), SIM (flake8-simplify).

    After the auto-fix, run:
    ```
    ruff check paperforge/ scripts/ tests/
    ```
    Confirm zero remaining issues.

    **Known classes of imports that ruff will flag across all 7 worker modules (same import block in each):**
    - `csv`, `hashlib`, `shutil`, `subprocess`, `zipfile` — likely unused in most workers
    - `xml.etree.ElementTree` — likely unused
    - `fitz`, `PIL.Image` — used only in ocr.py, might be unused elsewhere
    - Various `_utils.py` re-exported functions — ruff will flag unused imports from the `from paperforge.worker._utils import (...)` block

    Let ruff handle all of these automatically.

    **After ruff fixes imports**, verify project imports still work:
    ```
    python -c "import paperforge.worker.sync; import paperforge.worker.ocr; import paperforge.worker.status; import paperforge.worker.repair; import paperforge.worker.update; import paperforge.worker.deep_reading; import paperforge.worker.base_views; print('All worker modules importable')"
    ```

    ---
    **Step 2c: Remove load_vault_config delegation wrappers from all 7 worker modules**

    For each of the 7 worker modules (sync.py, ocr.py, deep_reading.py, repair.py, status.py, update.py, base_views.py):

    1. Add `from paperforge.config import load_vault_config, paperforge_paths` at the top of the module's import block (after stdlib imports, before local module imports). Use a single import statement.
    2. Remove the entire `load_vault_config(vault)` delegation wrapper function (the ~8-line function with the docstring "Read vault configuration — delegates to shared resolver").
    3. In the `pipeline_paths(vault)` function body:
       - Remove the `from paperforge.config import paperforge_paths as _shared_paperforge_paths` intra-function import (it's now a top-level import)
       - Change `_shared_paperforge_paths(vault)` to `paperforge_paths(vault)`
       - Remove the `cfg = load_vault_config(vault)` call inside `pipeline_paths` and replace with the new top-level `load_vault_config` name (already imported). Actually, the call `cfg = load_vault_config(vault)` stays the same — the name `load_vault_config` now resolves to the top-level import from config instead of the local function we just removed.

    **For sync.py specifically:** There are two additional intra-function import patterns at lines 733 and 1271:
    ```python
    from paperforge.config import load_vault_config as _load_vault_config
    cfg = _load_vault_config(vault)
    ```
    These can be replaced with the top-level `load_vault_config` name:
    ```python
    cfg = load_vault_config(vault)
    ```

    **For ocr.py:** The `pipeline_paths` function also has a `system_dir` and `resources_dir` extraction within it. This stays — the worker-only key extension is the actual function's purpose, not a delegation hop. Only change is using the top-level `paperforge_paths` and `load_vault_config` names.

    **For base_views.py:** There's also a `load_simple_env` function that duplicates `paperforge.config.load_simple_env`. This is NOT a delegation wrapper per D-09 and is NOT in scope for this phase (Phase 14 CH-01/CH-02 would cover shared utility extraction). Leave it untouched unless ruff flags it as unused.

    **Concrete per-file changes summary:**

    | Worker Module | Remove `def load_vault_config` | Add top-level import | Replace in `pipeline_paths` | Replace intra-function imports |
    |---|---|---|---|---|
    | sync.py | Yes, lines 42-49 | `from paperforge.config import load_vault_config, paperforge_paths` before `from paperforge.worker._utils import ...` (before line 24) | `_shared_paperforge_paths(vault)` → `paperforge_paths(vault)` | Lines 733, 1271: remove `from paperforge.config import load_vault_config as _load_vault_config` and use `load_vault_config(vault)` |
    | ocr.py | Yes, lines 51-58 | Same, before line 30 | Same | None |
    | deep_reading.py | Yes, lines 48-55 | Same, before the `from paperforge.worker.sync import ...` block (before line 24) | Same | None |
    | repair.py | Yes, lines 50-57 | Same, before `from paperforge.worker.sync import ...` (before line 24) | Same | None |
    | status.py | Yes, lines 44-51 | Same, before the `from paperforge.worker._utils import ...` (before line 26) | Same | None |
    | update.py | Yes, lines 42-49 | Same, before the `from paperforge.worker._utils import ...` (before line 24) | Same | None |
    | base_views.py | Yes, lines 58-65 | Same, before `from paperforge.worker._utils import ...` (before line 24) | Same | None |

    ---
    **Step 2d: Verify backward compatibility**

    The `pipeline_paths` function still exists in each worker module with the same signature and return value. Tests that import or mock it should continue to work:

    - `from paperforge.worker.sync import pipeline_paths` in `tests/test_repair.py` and `tests/test_legacy_worker_compat.py` — still valid
    - `unittest.mock.patch("paperforge.worker.ocr.pipeline_paths")` in `tests/test_ocr_preflight.py` and `tests/test_ocr_state_machine.py` — still valid (function still exists in the module)
    - `unittest.mock.patch("paperforge.worker.sync.pipeline_paths")` in `tests/test_selection_sync_pdf.py` — still valid

    For `load_vault_config`: after refactoring, `from paperforge.worker.sync import load_vault_config` still works because the name is now bound at module level as a top-level import from `paperforge.config` (same as the old delegation wrapper did, just without the function hop).

    ---
    **Step 2e: Run tests to confirm zero regressions**

    ```
    pytest tests/ -x --tb=short 2>&1
    ```

    If any test fails, diagnose and fix:
    - Mock path failures: The mock target `paperforge.worker.ocr.load_vault_config` would no longer exist if tests mock it (unlikely — check if any test mocks `load_vault_config` from worker modules). If so, update mock path to `paperforge.worker.ocr.load_vault_config` (the name still resolves via the top-level import).
    - Import errors: Check for any `from paperforge.worker.X import load_vault_config` in other code paths.

    Fix any issues and re-run until all tests pass.
  </action>
  <verify>
    <automated>ruff check paperforge/ scripts/ tests/ && pytest tests/ -x --tb=short</automated>
  </verify>
  <done>
    - ruff check passes with zero warnings across paperforge/, scripts/, tests/
    - pytest tests/ passes with zero failures
    - All 7 worker modules: no `def load_vault_config(vault)` delegation wrapper remains
    - All 7 worker modules: `from paperforge.config import load_vault_config, paperforge_paths` at top level
    - No intra-function `from paperforge.config import ...` hops remain in any worker module (sync.py's two inline imports cleaned up)
    - `pipeline_paths` function still exists in each worker module (retains its worker-only key extension behavior)
  </done>
</task>

</tasks>

<verification>
All-Clear sirens sound when:

1. **Code quality:** `ruff check paperforge/ scripts/ tests/` — zero warnings, zero errors
2. **Tests:** `pytest tests/ -x --tb=short` — all tests pass (current baseline: ~203 passing, 0 failing)
3. **Pre-commit config:** `pre-commit run --all-files` runs without error (run with `pre-commit run --all-files 2>&1`)
4. **Check 5:** `python scripts/consistency_audit.py` — all 5 checks pass (Check 5 returns PASS)
5. **Import integrity:** `python -c "import paperforge.worker.sync; import paperforge.worker.ocr; import paperforge.worker.status; import paperforge.worker.repair; import paperforge.worker.update; import paperforge.worker.deep_reading; import paperforge.worker.base_views; print('OK')"` — all modules importable
</verification>

<success_criteria>
- [x] CH-04: Dead code removed — ruff check passes clean, delegation wrappers eliminated, test suite passes
- [x] DX-01: .pre-commit-config.yaml created with 6 hooks (ruff lint+format, check-yaml/toml, eof-fixer, trailing-whitespace, consistency-audit)
- [x] DX-02: [tool.ruff] section in pyproject.toml with rules E/F/I/UP/B/SIM targeting Python 3.10+, ruff installed as test dependency
- [x] OBS-05: OCR error meta.json includes library_record zotero_key context for both upload and poll error handlers
</success_criteria>

<output>
After completion, create `.planning/phases/17-dead-code-precommit/17-01-SUMMARY.md`
</output>
