---
phase: 13-logging-foundation
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/worker/repair.py
  - paperforge/worker/deep_reading.py
  - paperforge/worker/ocr.py
  - paperforge/worker/sync.py
  - paperforge/worker/status.py
  - paperforge/worker/base_views.py
  - paperforge/worker/update.py
  - paperforge/commands/ocr.py
  - paperforge/commands/repair.py
  - paperforge/commands/deep.py
  - paperforge/commands/sync.py
  - paperforge/commands/status.py
  - paperforge/update.py
autonomous: true
requirements:
  - OBS-01
  - OBS-02
must_haves:
  truths:
    - "All 7 worker modules have `logger = logging.getLogger(__name__)` at module level"
    - "All 5 command modules have `logger = logging.getLogger(__name__)` at module level"
    - "All `[repair]` tagged diagnostic print() calls in worker/repair.py use logger.*() instead"
    - "Diagnostic print() calls in commands/ocr.py use logger.*() instead"
    - "update.py:_log() uses logger.info() instead of print()"
    - "All stdout user-facing print() calls remain unchanged"
  artifacts:
    - path: "paperforge/worker/repair.py"
      provides: "logger module instance with all diagnostic prints migrated"
      min_lines: 548
    - path: "paperforge/update.py"
      provides: "_log() uses logger.info() instead of print()"
      contains: "logger"
  key_links:
    - from: "paperforge/worker/repair.py"
      to: "logging"
      via: "All [repair] diagnostic calls migrated to logger.info/warning/error"
      pattern: "logger\\."
---

<objective>
Add `import logging; logger = logging.getLogger(__name__)` to all worker and command modules, and migrate all diagnostic `print()` calls to use the logging module instead.

Purpose: Per OBS-01, every module that produces diagnostic/trace/error output must have a module-level logger. Per OBS-02, the dual-output strategy means diagnostic output (stderr via logging) is separated from user-facing output (stdout via print()).

Output:
- 7 worker + 5 command modules + update.py with module-level logger instances
- All [repair]-tagged diagnostic prints in worker/repair.py migrated to logger.*()
- Diagnostic prints in commands/ocr.py migrated to logger.*()
- update.py:_log() migrated to logger.info()
</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/STATE.md
@.planning/ROADMAP.md
@.planning/phases/13-logging-foundation/13-CONTEXT.md
@.planning/phases/13-logging-foundation/13-RESEARCH.md
@paperforge/pdf_resolver.py
@paperforge/worker/repair.py
@paperforge/commands/ocr.py
@paperforge/update.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add module-level logger to all worker and command modules</name>

  <files>
    paperforge/worker/repair.py
    paperforge/worker/deep_reading.py
    paperforge/worker/ocr.py
    paperforge/worker/sync.py
    paperforge/worker/status.py
    paperforge/worker/base_views.py
    paperforge/worker/update.py
    paperforge/commands/ocr.py
    paperforge/commands/repair.py
    paperforge/commands/deep.py
    paperforge/commands/sync.py
    paperforge/commands/status.py
    paperforge/update.py
  </files>

  <read_first>
  - paperforge/pdf_resolver.py -- Reference: existing working pattern `import logging` + `logger = logging.getLogger(__name__)` (lines 8, 12)
  - .planning/phases/13-logging-foundation/13-RESEARCH.md section 1.1 -- stdout print() classification (DO NOT TOUCH these)
  - .planning/phases/13-logging-foundation/13-RESEARCH.md section 1.2 -- diagnostic print() classification (migrated in Task 2)
  </read_first>

  <action>
  For each of the 13 files listed above, add exactly TWO lines at module level (after existing imports, before any function definitions):

  `import logging`
  `logger = logging.getLogger(__name__)`

  Placement rules per file (insert after specified line):
  - worker/repair.py: After `from __future__ import annotations`, before `import argparse`
  - worker/deep_reading.py: After `from __future__ import annotations`, before `import argparse`
  - worker/ocr.py: After `from __future__ import annotations`, before `import argparse`
  - worker/sync.py: After `from __future__ import annotations`, before `import argparse`
  - worker/status.py: After existing imports, before function definitions
  - worker/base_views.py: After existing imports, before function definitions
  - worker/update.py: After existing imports, before function definitions
  - commands/ocr.py: After `from pathlib import Path`, before function definitions
  - commands/repair.py: After `from pathlib import Path`, before function definitions
  - commands/deep.py: After `from pathlib import Path`, before function definitions
  - commands/sync.py: After `from pathlib import Path`, before function definitions
  - commands/status.py: After `from pathlib import Path`, before function definitions
  - update.py (paperforge/update.py): After existing imports, before function definitions

  IMPORTANT -- Do NOT touch: cli.py (Plan 01), pdf_resolver.py (has logger), config.py, ocr_diagnostics.py. Verify no duplicate import logging or logger = before adding.
  </action>

  <acceptance_criteria>
  - Every file contains `import logging` at module level
  - Every file contains `logger = logging.getLogger(__name__)` at module level
  - No duplicate `import logging` or `logger = ` in any file
  - `python -c "from paperforge.worker.repair import logger; print(logger.name)"` must output `paperforge.worker.repair` (and same pattern for other modules)
  </acceptance_criteria>

  <verify>
  <automated>python -c "import sys; sys.path.insert(0, '.'); mods=['paperforge.worker.repair','paperforge.worker.deep_reading','paperforge.worker.ocr','paperforge.worker.sync','paperforge.worker.status','paperforge.commands.ocr','paperforge.commands.repair','paperforge.commands.deep','paperforge.commands.sync','paperforge.commands.status','paperforge.worker.update']; [print('OK: '+__import__(m, fromlist=['logger']).logger.name) for m in mods]"</automated>
  </verify>

  <done>
  All 13 modules have module-level loggers. Import test passes for all.
  </done>
</task>

<task type="auto">
  <name>Task 2: Migrate diagnostic print() calls to logger.*() in repair.py, commands/ocr.py, and update.py</name>

  <files>
    paperforge/worker/repair.py
    paperforge/commands/ocr.py
    paperforge/update.py
  </files>

  <read_first>
  - paperforge/worker/repair.py -- All [repair] tagged print() calls (lines 236, 303, 339, 357, 360, 363, 368, 423, 456, 526, 533, 540, 542, 544)
  - paperforge/commands/ocr.py -- Diagnostic print() calls (lines 71, 78, 82, 87)
  - paperforge/update.py -- _log() function at line 226-227 that wraps print(_color(msg, c))
  - .planning/phases/13-logging-foundation/13-RESEARCH.md section 1.2 -- Full classification table
  - .planning/phases/13-logging-foundation/13-CONTEXT.md D-07 -- stdout/stderr boundary classification
  </read_first>

  <action>
  **A) Migrate worker/repair.py diagnostic print() calls:**

  Replace each `[repair]`-tagged `print()` with the appropriate `logger.*()` call. Keep stdout user-facing print() calls at function exit points (lines 533, 540, 542, 544) unchanged -- these are run_repair() summary messages.

  Migration table (print -> logger):
  - Line 236: `print(f"[repair] error reading {record_path}: {e}")` -> `logger.error("error reading %s: %s", record_path, e)`
  - Lines 303-305: `print(f"[repair] error loading export for {domain}: {e}")` -> `logger.error("error loading export for %s: %s", domain, e)`
  - Lines 339-341: `print(f"[repair] fixed path for {zotero_key}: {new_wikilink}")` -> `logger.info("fixed path for %s: %s", zotero_key, new_wikilink)`
  - Line 357: `print(f"[repair] cleared path_error for {zotero_key}")` -> `logger.info("cleared path_error for %s", zotero_key)`
  - Line 360: `print(f"[repair] {zotero_key} path still unresolved")` -> `logger.warning("%s path still unresolved", zotero_key)`
  - Lines 363-365: `print(f"[repair] {zotero_key} has empty pdf_path (not_found)")` -> `logger.warning("%s has empty pdf_path (not_found)", zotero_key)`
  - Line 368: `print(f"[repair] {zotero_key} has no pdf_path field")` -> `logger.warning("%s has no pdf_path field", zotero_key)`
  - Line 423: `print(f"[repair] {zotero_key} meta validation error: {validated_error}")` -> `logger.warning("%s meta validation error: %s", zotero_key, validated_error)`
  - Line 456: `print(f"[repair] divergent: {zotero_key} | {div_reason}")` -> `logger.info("divergent: %s | %s", zotero_key, div_reason)`
  - Line 526: `print(f"[repair] fixed {fixed_count} files for {zotero_key}")` -> `logger.info("fixed %d files for %s", fixed_count, zotero_key)`

  KEEP AS print(): Lines 533-535 (`[repair] Found ... items with path errors: ...`), Line 540 (`[repair] Fixed ... PDF paths`), Line 542 (`[repair] Tip: run with --fix-paths...`), Line 544 (`[repair] No path errors found`) -- these are user-facing run_repair() summary output on stdout.

  **B) Migrate commands/ocr.py diagnostic print() calls:**

  - Line 71: `print(f"[INFO] Processing specific key: {key}")` -> `logger.info("Processing specific key: %s", key)`
  - Line 78: `print("\n[Auto-diagnose] Running post-OCR diagnostic...")` -> `logger.info("Running post-OCR diagnostic...")`
  - Lines 82-85: `print("[WARN] Post-OCR diagnostic found issues, " ...)` -> `logger.warning("Post-OCR diagnostic found issues, but OCR completed successfully.")` (single string join)
  - Line 87: `print(f"[WARN] Auto-diagnose failed: {e}")` -> `logger.warning("Auto-diagnose failed: %s", e)`

  **C) Migrate update.py _log() function:**

  Replace the existing `_log()` function body (currently `print(_color(msg, c))`):
  - Remove the `_log()` function entirely (defined at line 226)
  - Replace every call site `_log(msg, c)` with an appropriate logging call
  - The colored output is no longer needed since logging goes to stderr (formatting is stripped)
  - For warning messages: `logger.warning(msg)`
  - For info messages: `logger.info(msg)`
  - Keep the `_color()` function if still used elsewhere; otherwise remove dead code

  Scan update.py for all `_log()` call sites and map each to the appropriate logger level:
  - Version check failures -> `logger.warning()`
  - Update progress -> `logger.info()`
  - Error messages -> `logger.error()`

  **General rules for ALL migrations:**
  - Use printf-style formatting (`%s`, `%d`) instead of f-strings -- this is the standard logging pattern that enables lazy evaluation
  - Do NOT add `file=sys.stderr` -- logging goes to stderr via the StreamHandler configured in logging_config.py
  - Keep all stdout user-facing print() calls unchanged (sync.py line 881, deep_reading.py line 322, ocr.py line 1374, status.py lines 598-613, etc.)
  - The `if verbose:` guards remain in place -- they control whether the logger call is made at all
  </action>

  <acceptance_criteria>
  - No `print(f"[repair] ...")` patterns remain in worker/repair.py (only the 4 kept summary print() calls remain)
  - No `print(f"[INFO] ...")` or `print(f"[WARN] ...")` patterns in commands/ocr.py
  - `_log()` function no longer exists in update.py or uses logger.*() instead of print()
  - All migrated calls use printf-style `%s`/`%d` formatting
  - All stdout print() calls in sync.py, deep_reading.py, ocr.py, status.py remain unchanged
  - `grep -c "print(f.\[repair\]" paperforge/worker/repair.py` returns 0 (no [repair] print calls remain)
  - `grep -c "print(f.\[INFO\]" paperforge/commands/ocr.py` returns 0
  - `grep -c "print(f.\[WARN\]" paperforge/commands/ocr.py` returns 0
  </acceptance_criteria>

  <verify>
  <automated>python -c "import sys; sys.path.insert(0, '.'); src=open('paperforge/worker/repair.py').read(); assert 'print(f' \"'\"'[repair]' \"'\"' not in src, 'fail'; src2=open('paperforge/commands/ocr.py').read(); assert 'print(f' \"'\"'[INFO]' \"'\"' not in src2 and 'print(f' \"'\"'[WARN]' \"'\"' not in src2, 'fail'; print('OK')"</automated>
  </verify>

  <done>
  No diagnostic `[repair]` print() calls remain in repair.py. No `[INFO]`/`[WARN]` print() calls remain in commands/ocr.py. update.py uses logger.*() instead of _log(). All stdout user-facing print() calls preserved.
  </done>
</task>

</tasks>

<verification>
1. Verify all 13 modules import logging and have module-level logger instances
2. Verify no `[repair]` print calls remain in worker/repair.py
3. Verify no `[INFO]`/`[WARN]` print calls remain in commands/ocr.py
4. Verify update.py no longer uses print() for diagnostic output
5. Verify stdout user-facing print() calls in sync.py, deep_reading.py, ocr.py, status.py remain unchanged
</verification>

<success_criteria>
- All worker and command modules have module-level loggers
- All diagnostic print() calls migrated to logger.*() with printf-style formatting
- All user-facing stdout print() calls remain unchanged
- Dual-output boundary (OBS-02) established: stdout = user, stderr/logging = diagnostic
</success_criteria>

<output>
After completion, create `.planning/phases/13-logging-foundation/13-02-SUMMARY.md`
</output></automated>
  </verify>