---
phase: 02-paddleocr-and-pdf-path-hardening
plan: 04
type: execute
wave: 3
depends_on: ["02-01", "02-02"]
files_modified:
  - pipeline/worker/scripts/literature_pipeline.py
  - tests/test_selection_sync_pdf.py
autonomous: true
requirements:
  - ZOT-02

must_haves:
  truths:
    - "selection-sync marks records without PDF attachments as ocr_status: nopdf"
    - "selection-sync marks records with missing/unreadable PDF files as ocr_status: nopdf"
    - "selection-sync uses resolved PDF paths in library-record frontmatter"
  artifacts:
    - path: "pipeline/worker/scripts/literature_pipeline.py"
      provides: "run_selection_sync() with PDF preflight and nopdf reporting"
      contains: "resolve_pdf_path", "ocr_status.*nopdf", "run_selection_sync"
    - path: "tests/test_selection_sync_pdf.py"
      provides: "Unit tests for selection-sync PDF reporting"
      exports: ["test_no_pdf_sets_nopdf", "test_missing_pdf_sets_nopdf", "test_valid_pdf_sets_pending"]
  key_links:
    - from: "pipeline/worker/scripts/literature_pipeline.py"
      to: "paperforge/pdf_resolver.py"
      via: "import resolve_pdf_path"
      pattern: "from paperforge.pdf_resolver import resolve_pdf_path"
    - from: "run_selection_sync()"
      to: "library-records/*.md"
      via: "record_path.write_text with ocr_status: nopdf"
      pattern: "ocr_status.*nopdf"
---

<objective>
Update selection-sync to detect and report missing or unreadable PDFs in library-record frontmatter.

Purpose: Give users immediate visibility into which records lack PDFs before queueing OCR, preventing wasted OCR runs on attachments that don't exist.
Output: run_selection_sync() with PDF preflight, library records showing ocr_status: nopdf when appropriate, tests.
</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/REQUIREMENTS.md
@.planning/phases/02-paddleocr-and-pdf-path-hardening/02-CONTEXT.md
@.planning/phases/02-paddleocr-and-pdf-path-hardening/02-01-SUMMARY.md

<interfaces>
From pipeline/worker/scripts/literature_pipeline.py (run_selection_sync current PDF handling):
```python
pdf_attachments = [a for a in item.get('attachments', []) if a.get('contentType') == 'application/pdf']
...
'has_pdf': bool(pdf_attachments),
'pdf_path': obsidian_wikilink_for_pdf(vault, pdf_attachments[0]['path']) if pdf_attachments else '',
'ocr_status': ocr_status,  # from meta.json or 'pending'
```

From paperforge/pdf_resolver.py (created in Plan 02-01):
```python
def resolve_pdf_path(pdf_path: str, has_pdf: bool, vault_root: Path, zotero_dir: Path | None) -> str:
    ...
```

From CONTEXT.md D-05:
"During selection-sync, if has_pdf: false or resolved pdf_path is empty, record ocr_status: 'nopdf' in library-record frontmatter."
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add PDF preflight to run_selection_sync()</name>
  <files>pipeline/worker/scripts/literature_pipeline.py</files>
  <read_first>
    - pipeline/worker/scripts/literature_pipeline.py lines 678-729 (full run_selection_sync function)
    - paperforge/pdf_resolver.py (resolve_pdf_path from Plan 02-01)
    - .planning/phases/02-paddleocr-and-pdf-path-hardening/02-CONTEXT.md section 3.5
  </read_first>
  <behavior>
    - Item with no PDF attachments → library record has ocr_status: nopdf
    - Item with PDF attachment but missing file → library record has ocr_status: nopdf
    - Item with PDF attachment and readable file → library record has ocr_status: pending (or existing meta status)
    - pdf_path field in record contains resolved absolute path or empty string
  </behavior>
  <action>
    Modify `pipeline/worker/scripts/literature_pipeline.py` in `run_selection_sync()` (around lines 688-715):

    1. After `pdf_attachments = [...]` on line 688, add:
       ```python
       has_pdf = bool(pdf_attachments)
       raw_pdf_path = pdf_attachments[0].get('path', '') if pdf_attachments else ''
       from paperforge.pdf_resolver import resolve_pdf_path
       # Derive zotero_dir from vault config; use system/Zotero as default
       cfg = load_vault_config(vault)
       zotero_dir = vault / cfg.get('system_dir', '99_System') / 'Zotero'
       resolved_pdf = resolve_pdf_path(raw_pdf_path, has_pdf, vault, zotero_dir)
       ```

    2. Update the frontmatter dictionary (around line 715) to use `resolved_pdf` and compute `ocr_status`:
       ```python
       if not has_pdf or not resolved_pdf:
           record_ocr_status = 'nopdf'
       else:
           record_ocr_status = meta.get('ocr_status', 'pending') if meta else 'pending'
       ```
       Replace `'has_pdf': bool(pdf_attachments)` with `'has_pdf': has_pdf`.
       Replace `'pdf_path': obsidian_wikilink_for_pdf(vault, pdf_attachments[0]['path']) if pdf_attachments else ''` with `'pdf_path': obsidian_wikilink_for_pdf(vault, resolved_pdf)`.
       Replace `'ocr_status': ocr_status` with `'ocr_status': record_ocr_status`.

    3. In the update-existing-record branch (lines 716-724), ensure `ocr_status` is updated to `record_ocr_status`:
       ```python
       updated_content = update_frontmatter_field(updated_content, 'ocr_status', record_ocr_status)
       ```
       (This line already exists but uses the old `ocr_status` variable — replace with `record_ocr_status`.)

    4. Ensure `load_vault_config` is imported at the top of literature_pipeline.py. Check if it's already imported (it is used elsewhere in the file).

    Do NOT change the `obsidian_wikilink_for_pdf` function itself — only its call site. The wikilink should be computed from the resolved path.
  </action>
  <verify>
    <automated>
      grep -n "resolve_pdf_path" pipeline/worker/scripts/literature_pipeline.py
      grep -n "record_ocr_status" pipeline/worker/scripts/literature_pipeline.py
      grep -n "ocr_status.*nopdf" pipeline/worker/scripts/literature_pipeline.py
    </automated>
  </verify>
  <done>run_selection_sync() resolves PDF paths and sets ocr_status to 'nopdf' when has_pdf is False or resolved path is empty; existing records are updated with corrected ocr_status</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add tests for selection-sync PDF reporting</name>
  <files>tests/test_selection_sync_pdf.py</files>
  <read_first>
    - tests/test_cli_worker_dispatch.py (for mocking patterns)
    - pipeline/worker/scripts/literature_pipeline.py lines 678-729
    - paperforge/pdf_resolver.py
  </read_first>
  <behavior>
    - Mock export item with no PDF → record frontmatter contains ocr_status: nopdf
    - Mock export item with PDF path to missing file → record frontmatter contains ocr_status: nopdf
    - Mock export item with PDF path to existing file → record frontmatter contains ocr_status: pending
    - pdf_path field is empty for nopdf cases, contains wikilink for valid cases
  </behavior>
  <action>
    Create `tests/test_selection_sync_pdf.py` with pytest tests:

    Use `tmp_path` fixture to create a realistic vault structure:
    ```python
    vault = tmp_path / "vault"
    vault.mkdir()
    system = vault / "99_System"
    system.mkdir()
    (system / "PaperForge" / "exports").mkdir(parents=True)
    (system / "PaperForge" / "ocr").mkdir(parents=True)
    resources = vault / "03_Resources"
    resources.mkdir()
    records = resources / "LiteratureControl" / "library-records"
    records.mkdir(parents=True)
    lit = resources / "Literature"
    lit.mkdir(parents=True)
    (vault / "paperforge.json").write_text('{}', encoding='utf-8')
    ```

    Create a helper `mock_export_item(key, has_pdf=True, pdf_path='')` that returns a dict matching Better BibTeX JSON shape with `attachments`, `collections`, `title`, `year`, `doi`, etc.

    Patch `pipeline.worker.scripts.literature_pipeline.load_export_rows` to return test items.
    Patch `pipeline.worker.scripts.literature_pipeline.load_domain_config` to return `{'domains': [{'export_file': 'test.json', 'domain': 'TestDomain'}]}`.
    Patch `pipeline.worker.scripts.literature_pipeline.ensure_base_views` to no-op.

    Tests:
    1. `test_no_pdf_sets_nopdf`: Export item with empty attachments. Assert created record contains `ocr_status: nopdf` and `has_pdf: false`.
    2. `test_missing_pdf_sets_nopdf`: Export item with attachment path to non-existent file. Assert record contains `ocr_status: nopdf` and `pdf_path: ''`.
    3. `test_valid_pdf_sets_pending`: Export item with attachment path to an existing dummy PDF file (create with `Path.write_bytes(b'%PDF-1.4\n')`). Assert record contains `ocr_status: pending` and non-empty `pdf_path`.
    4. `test_existing_record_updated`: Create an existing record with `ocr_status: pending`, run sync with item that now has no PDF, assert record updated to `ocr_status: nopdf`.

    Run with `pytest tests/test_selection_sync_pdf.py -v`.
  </action>
  <verify>
    <automated>pytest tests/test_selection_sync_pdf.py -v</automated>
  </verify>
  <done>All 4 tests pass: no PDF → nopdf, missing PDF → nopdf, valid PDF → pending, existing record updates correctly</done>
</task>

</tasks>

<verification>
- [ ] `pytest tests/test_selection_sync_pdf.py -v` passes
- [ ] `grep -n "resolve_pdf_path" pipeline/worker/scripts/literature_pipeline.py` returns at least 2 matches (one in run_ocr from 02-01, one in run_selection_sync)
- [ ] `grep -n "record_ocr_status" pipeline/worker/scripts/literature_pipeline.py` returns at least 2 matches
- [ ] `grep -n "ocr_status.*nopdf" pipeline/worker/scripts/literature_pipeline.py` returns at least 2 matches
- [ ] Library records created by selection-sync show `ocr_status: nopdf` when PDF is missing
</verification>

<success_criteria>
- selection-sync sets ocr_status to 'nopdf' for records without PDF attachments
- selection-sync sets ocr_status to 'nopdf' for records where resolve_pdf_path returns empty string
- selection-sync preserves 'pending'/'done' status for records with valid, resolvable PDFs
- Existing library records are updated when PDF availability changes
- Tests cover all three cases plus update scenario
</success_criteria>

<output>
After completion, create `.planning/phases/02-paddleocr-and-pdf-path-hardening/02-04-SUMMARY.md`
</output>
