Add run_repair() function for three-way OCR state divergence repair

This commit is contained in:
Research Assistant 2026-04-24 00:02:32 +08:00
parent 981f5fa8ba
commit 6052c823e6
21 changed files with 1830 additions and 374 deletions

View file

@ -6,6 +6,16 @@ PaperForge Lite is a local Obsidian + Zotero literature workflow for medical res
This is a brownfield release-hardening project for `D:\L\Med\Research\99_System\LiteraturePipeline\github-release`, informed by the fuller local implementation under `D:\L\Med\Research\99_System\LiteraturePipeline` and the production Obsidian Base views under `D:\L\Med\Research\05_Bases`.
## Current Milestone: v1.1 Sandbox Onboarding Hardening
**Goal:** Make the GitHub README + sandbox path behave like a real first-time user flow, with no silent setup stalls, contradictory diagnostics, unresolved mock Zotero PDFs, or broken `/LD-deep` prepare commands.
**Target features:**
- Setup wizard and CLI commands are internally consistent when a user follows README exactly.
- `paperforge doctor`, `paperforge paths --json`, worker fallback commands, and Agent command docs report the same installed paths and env variable names.
- Sandbox Better BibTeX exports and mock Zotero storage exercise the full flow from selection sync through `/LD-deep prepare`.
- PDF/OCR/deep-reading statuses remain consistent across library-records, formal notes, `meta.json`, and queue output.
## Core Value
A new user can install PaperForge, configure their own vault paths and PaddleOCR credentials, then run the full literature pipeline with copy-pasteable commands that diagnose failures clearly.
@ -18,56 +28,60 @@ A new user can install PaperForge, configure their own vault paths and PaddleOCR
- ✓ Configurable vault directories are partially supported through `paperforge.json` and `vault_config`.
- ✓ Existing Obsidian Base views prove the intended queue workflow: recommended analysis, OCR queue, completed OCR, pending deep reading, completed deep reading, and formal notes.
- ✓ OCR queue state is persisted in `<system_dir>/PaperForge/ocr/ocr-queue.json` and per-paper `meta.json`.
- ✓ v1.0 shipped a shared resolver, `paperforge` CLI, generated Bases, first-pass doctor command, fixture smoke tests, and command documentation.
### Active
- [ ] Registration-to-first-paper onboarding is explicit and testable.
- [ ] PaddleOCR configuration is validated before queue mutation and reports actionable errors.
- [ ] PDF path resolution works for absolute paths, vault-relative paths, Zotero storage-relative paths, and paths behind the configured Zotero junction.
- [ ] User-customized directories are resolved by command-line tools and environment variables, not by agent-written placeholder substitution.
- [ ] Generated Base files match the operational views used in the real vault, while still adapting to custom directory names.
- [ ] Full-flow validation covers setup, selection sync, index refresh, OCR preflight, OCR polling, deep-reading queue, and `/LD-deep` prepare.
- [ ] README-driven setup works in the sandbox without hidden required inputs or unexplained terminal stalls.
- [ ] Setup wizard, `paperforge` CLI, direct worker fallback, deployed Agent scripts, and command docs agree on the installed path contract.
- [ ] Diagnostics validate the actual supported Better BibTeX export shapes and PaddleOCR env names.
- [ ] PDF path resolution handles sandbox BBT attachment paths and common Zotero storage-relative paths.
- [ ] Selection sync writes complete normalized metadata into library-records, including author and journal fields.
- [ ] OCR status, formal note status, library-record status, and deep-reading queue status converge after each worker step.
- [ ] `/LD-deep` helpers run from the deployed Vault location without manual `PYTHONPATH` fixes.
- [ ] The sandbox smoke test catches every regression found in the manual first-time-user simulation.
### Out of Scope
- Replacing Zotero or Better BibTeX — the project is built around them.
- Automatically triggering deep-reading agents from workers — the Lite architecture intentionally keeps worker automation and agent reasoning separate.
- Cloud-hosted multi-user service — this project targets local single-user vault workflows.
- Full OCR provider abstraction in v1 — PaddleOCR should be robust first; provider plugins can follow later.
- Full OCR provider abstraction in v1.1 — PaddleOCR path/env consistency is the priority.
## Context
The current release repo has `setup_wizard.py`, `pipeline/worker/scripts/literature_pipeline.py`, OpenCode command files, chart-reading skills, installation docs, and a user-facing `AGENTS.md`. The fuller local pipeline contains additional scripts and real operational history, but the release should remain Lite and understandable for installation.
The v1.1 milestone is based on a manual sandbox audit performed from `tests/sandbox/00_TestVault` using only README-level guidance. The audit found that v1.0's claimed release-hardening coverage is not yet sufficient for a real first-time user:
Important observations from code and Base review:
- `setup_wizard.py` stores PaddleOCR credentials in `<system_dir>/PaperForge/.env`, and the worker loads both vault `.env` and PaperForge `.env`.
- `run_ocr()` expects `PADDLEOCR_API_TOKEN`, optional `PADDLEOCR_API_TOKEN_USER`, `PADDLEOCR_JOB_URL`, `PADDLEOCR_MODEL`, and `PADDLEOCR_MAX_ITEMS`.
- OCR requests use `Authorization: bearer <token>`, multipart `file`, `model`, and `optionalPayload`, then poll `job_url/<jobId>`.
- OCR error handling currently records broad request failures but does not classify unauthorized URL/auth/payload/path failures for the user.
- The OCR worker opens `queue_row['pdf_path']` directly. If Better BibTeX exports a relative path, linked attachment path, or Zotero storage path that is not directly openable from the current process, OCR fails.
- Base templates generated by `ensure_base_views()` are much simpler than production Base files such as `骨科.base`, `运动医学.base`, and `Literature Hub.base`.
- Production Base filters hardcode `03_Resources/LiteratureControl/...` and `03_Resources/Literature/...`, so they conflict with the release promise that resources/control/literature directories are user-customizable.
- Command docs still rely on `<system_dir>` placeholder guidance rather than a first-class launcher that resolves the actual configured paths.
- `python setup_wizard.py --vault ...` can appear to hang in a terminal, and the Vault input is not prefilled from `--vault`.
- `paperforge` may be unavailable when setup does not complete; README does not clearly provide a reliable fallback.
- `paperforge doctor` checks `library.json` and `PADDLEOCR_API_KEY`, while the implemented flow supports per-domain JSON exports and uses `PADDLEOCR_API_TOKEN`.
- `paperforge paths --json` reports a worker path under `vault/pipeline/...`, but setup deploys the worker under `<system_dir>/PaperForge/worker/scripts/...`.
- Command docs reference `literature_script`, but the CLI emits `ld_deep_script`.
- Sandbox BBT attachment paths such as `TSTONE001/TSTONE001.pdf` do not resolve to `<Zotero>/storage/TSTONE001/TSTONE001.pdf`.
- `selection-sync` normalizes export rows but later reads raw BBT fields, leaving `first_author` and `journal` empty in library-records.
- `deep-reading --verbose` writes the useful queue report to a file but prints only a terse pending count.
- Deployed `ld_deep.py` depends on `paperforge_lite` importability and fails without manual `PYTHONPATH` or a successful package install.
## Constraints
- **Local-first**: Must work in a users Obsidian vault without a daemon or cloud service.
- **Windows compatibility**: Windows junctions, PowerShell, and paths with Chinese names are first-class use cases.
- **Plain Python**: Keep dependencies small; current requirements are `requests`, `pymupdf`, `pillow`, `textual`, and `pytest`.
- **Obsidian compatibility**: `.base` files must use Obsidian Bases syntax and relative vault paths.
- **Credential safety**: API keys belong in `.env` or user environment variables and must not be committed.
- **Agent independence**: Users should not need an agent to inspect `paperforge.json` just to build a worker command.
- **Local-first:** Must work in a user's Obsidian vault without a daemon or cloud service.
- **Windows compatibility:** Windows junctions, PowerShell, and paths with Chinese names are first-class use cases.
- **Plain Python:** Keep dependencies small; current requirements are `requests`, `pymupdf`, `pillow`, `textual`, and `pytest`.
- **Obsidian compatibility:** `.base` files must use Obsidian Bases syntax and relative vault paths.
- **Credential safety:** API keys belong in `.env` or user environment variables and must not be committed.
- **Agent independence:** Users should not need an agent to inspect `paperforge.json` just to build a worker command.
- **Sandbox realism:** `tests/sandbox` must remain safe, deterministic, and representative of a GitHub user trying the project locally.
## Key Decisions
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Keep Lite two-layer architecture | Worker and Agent responsibilities are already clear and lower-risk than automatic deep-reading triggers | - Pending |
| Add a PaperForge CLI/launcher layer | It removes placeholder command friction and centralizes path/env resolution | - Pending |
| Treat PaddleOCR as a preflighted integration | Users need immediate diagnosis before jobs enter confusing pending/error states | - Pending |
| Generate Bases from config-aware templates | Current production Base UX is better than release templates, but hardcoded paths must be parameterized | - Pending |
| Keep planning docs local to release repo | Parent vault has its own `.planning`; this repo needs scoped release-hardening state | - Pending |
| Keep Lite two-layer architecture | Worker and Agent responsibilities are already clear and lower-risk than automatic deep-reading triggers | Accepted |
| Add a PaperForge CLI/launcher layer | It removes placeholder command friction and centralizes path/env resolution | Implemented in v1.0, repair consistency in v1.1 |
| Treat PaddleOCR as a preflighted integration | Users need immediate diagnosis before jobs enter confusing pending/error states | Implemented in v1.0, align env names in v1.1 |
| Generate Bases from config-aware templates | Current production Base UX is better than release templates, but hardcoded paths must be parameterized | Implemented in v1.0 |
| Use sandbox audit as v1.1 release gate | Manual first-time-user simulation exposed regressions that unit tests missed | Active |
| Continue phase numbering after v1.0 | v1.1 is a follow-up hardening milestone, not a project reset | Phases start at 6 |
## Evolution
@ -87,4 +101,4 @@ This document evolves at phase transitions and milestone boundaries.
4. Update Context with current state.
---
*Last updated: 2026-04-23 after initialization*
*Last updated: 2026-04-23 starting milestone v1.1*

View file

@ -1,63 +1,53 @@
# Requirements: PaperForge Lite Release Hardening
# Requirements: PaperForge Lite v1.1 Sandbox Onboarding Hardening
**Defined:** 2026-04-23
**Core Value:** A new user can install PaperForge, configure their own vault paths and PaddleOCR credentials, then run the full literature pipeline with copy-pasteable commands that diagnose failures clearly.
## v1 Requirements
## Milestone v1.1 Requirements
### Onboarding
### Setup And Commands
- [ ] **ONBD-01**: User can follow one registration-to-first-paper guide that covers Zotero, Better BibTeX, Obsidian, PaddleOCR, and PaperForge.
- [ ] **ONBD-02**: User can run one validation command that reports setup readiness by category.
- [ ] **ONBD-03**: User can see the exact next command after each setup or worker step.
- [ ] **SETUP-01**: User can run `python setup_wizard.py --vault <vault>` and see an immediate, understandable setup UI or message instead of an unexplained terminal stall.
- [ ] **SETUP-02**: User can rely on `--vault <vault>` being carried into the wizard flow without retyping the same path.
- [ ] **SETUP-03**: User can continue with a documented fallback command when the global `paperforge` executable is not registered.
- [ ] **SETUP-04**: User can inspect `paperforge paths --json` and receive installed worker and Agent script paths that actually exist.
- [ ] **SETUP-05**: Agent command docs use the same JSON field names emitted by `paperforge paths --json`.
### Configuration
### Diagnostics
- [ ] **CONF-01**: User can define vault and custom directories through environment variables without editing generated code.
- [x] **CONF-02**: User can inspect resolved PaperForge paths with a command.
- [ ] **CONF-03**: Worker, Agent scripts, command docs, and Base generation all use the same config resolver.
- [ ] **CONF-04**: Existing `paperforge.json` installations remain backward-compatible.
- [ ] **DIAG-01**: User can run `paperforge doctor` against per-domain Better BibTeX exports without being incorrectly blocked for missing `library.json`.
- [ ] **DIAG-02**: User can configure PaddleOCR once with the env variable name that setup writes and workers read.
- [ ] **DIAG-03**: Doctor reports the deployed worker script path according to the same resolver contract used by runtime commands.
- [ ] **DIAG-04**: OCR doctor distinguishes an expected endpoint-method mismatch from a bad user URL when checking the configured PaddleOCR job endpoint.
### Commands
### Zotero Paths And Metadata
- [ ] **CMD-01**: User can run stable commands such as `paperforge status`, `paperforge ocr run`, and `paperforge deep-reading`.
- [ ] **CMD-02**: Legacy direct worker invocation remains supported.
- [ ] **CMD-03**: Command output uses actionable statuses and avoids placeholder paths.
- [ ] **ZPATH-01**: User can sync a BBT attachment path shaped like `KEY/KEY.pdf` when the file exists under the configured Zotero `storage/KEY/` directory.
- [ ] **ZPATH-02**: User can sync common `storage:KEY/file.pdf` and `storage/KEY/file.pdf` attachment forms.
- [ ] **ZPATH-03**: User sees library-record `pdf_path` populated only with a readable resolved PDF path or an explicit actionable missing-PDF status.
- [ ] **META-01**: User sees `first_author` populated in generated library-records from normalized export metadata.
- [ ] **META-02**: User sees `journal` populated in generated library-records from normalized export metadata.
### PaddleOCR
### State And Queue Consistency
- [ ] **OCR-01**: User can run `ocr doctor` to validate token presence, URL shape, network reachability, and expected API response structure.
- [ ] **OCR-02**: OCR worker validates PDF readability before submitting a job.
- [ ] **OCR-03**: OCR failures identify whether the cause is missing token, bad URL, unauthorized token, unreadable PDF, API schema mismatch, timeout, or provider error.
- [ ] **OCR-04**: User can retry/reset errored or blocked OCR records after fixing configuration.
- [ ] **OCR-05**: OCR polling handles provider schema changes defensively and records raw diagnostic snippets safely.
- [ ] **STATE-01**: User can run `selection-sync`, `index-refresh`, and `ocr run` without records simultaneously saying `has_pdf: true` and `ocr_status: nopdf` when the PDF is readable.
- [ ] **STATE-02**: User sees formal note OCR status synchronized with validated OCR `meta.json` status after worker refresh.
- [ ] **STATE-03**: User sees `paperforge deep-reading --verbose` print the ready/waiting/blocked queue summary directly or print the report path clearly.
- [ ] **STATE-04**: User can tell from command output which record needs OCR, which one is blocked, and which one is ready for `/LD-deep`.
### Zotero And Paths
### Deep Reading Helpers
- [ ] **ZOT-01**: PDF path resolver supports absolute paths, vault-relative paths, configured Zotero junction paths, and common Zotero storage-relative paths.
- [ ] **ZOT-02**: Selection sync reports records with missing or unreadable PDFs.
- [ ] **ZOT-03**: Better BibTeX export path and expected JSON shape are validated.
- [ ] **DEEP-04**: User can run the deployed `ld_deep.py` helper from the Vault installation without manually setting `PYTHONPATH`.
- [ ] **DEEP-05**: User can run `/LD-deep queue` documentation examples using paths and field names that exist.
- [ ] **DEEP-06**: User can prepare a sandbox OCR-complete paper and get `figure-map.json`, `chart-type-map.json`, and a `## 🔍 精读` scaffold in the formal note.
### Obsidian Bases
### Regression Coverage
- [ ] **BASE-01**: Generated domain Base files include the operational views from the real vault workflow.
- [ ] **BASE-02**: Base filters are rendered from configured paths instead of hardcoded `03_Resources`.
- [ ] **BASE-03**: Base generation preserves user-edited Base files unless explicitly refreshed.
- [ ] **BASE-04**: Literature Hub Base gives a cross-domain queue overview.
- [ ] **REG-01**: Maintainer can run one sandbox smoke test that starts from a clean `tests/sandbox/00_TestVault` and covers setup-equivalent layout, selection sync, index refresh, OCR preflight/dry-run, deep-reading queue, and `ld_deep.py prepare`.
- [ ] **REG-02**: Smoke assertions cover the exact regressions from the manual audit: doctor env names, per-domain JSON, worker path JSON, BBT PDF path resolution, metadata fields, queue output, and deployed Agent importability.
- [ ] **REG-03**: README, INSTALLATION.md, AGENTS.md, and command files stay consistent with the smoke-tested commands.
### Deep Reading
- [ ] **DEEP-01**: Deep-reading queue accurately shows ready versus blocked papers.
- [ ] **DEEP-02**: `/LD-deep` prepare uses the same resolved paths as workers.
- [ ] **DEEP-03**: `/LD-deep` failure messages tell the user which worker command fixes the blocker.
### Release Quality
- [ ] **REL-01**: Automated tests cover config resolution, PDF path resolution, OCR state transitions, Base rendering, and command launcher behavior.
- [ ] **REL-02**: A smoke test can run through setup validation, selection sync, index refresh, OCR doctor, OCR queue dry-run, and deep-reading queue.
- [ ] **REL-03**: Documentation and AGENTS guide match implemented commands.
## v2 Requirements
## Future Requirements
### Integrations
@ -78,46 +68,42 @@
| Automatic deep-reading generation from worker | Conflicts with Lite architecture and risks uncontrolled agent work |
| Replacing Zotero collections as the source of domains | Existing workflow depends on Zotero and Better BibTeX exports |
| Multi-user hosted backend | Not needed for local release reliability |
| Full provider plugin system in v1 | PaddleOCR must be made reliable first |
| Full provider plugin system in v1.1 | The current milestone fixes PaddleOCR path/env consistency first |
| Real PaddleOCR network smoke test in default CI | Sandbox regression should be deterministic; live provider checks remain opt-in |
## Traceability
| Requirement | Phase | Status |
|-------------|-------|--------|
| ONBD-01 | Phase 4 | Pending |
| ONBD-02 | Phase 4 | Pending |
| ONBD-03 | Phase 4 | Pending |
| CONF-01 | Phase 1 | Pending |
| CONF-02 | Phase 1 | Pending |
| CONF-03 | Phase 1 | Done (01-03) |
| CONF-04 | Phase 1 | Done (01-03) |
| CMD-01 | Phase 1 | Pending |
| CMD-02 | Phase 1 | Done (01-03) |
| CMD-03 | Phase 1 | Pending |
| OCR-01 | Phase 2 | Pending |
| OCR-02 | Phase 2 | Pending |
| OCR-03 | Phase 2 | Pending |
| OCR-04 | Phase 2 | Pending |
| OCR-05 | Phase 2 | Pending |
| ZOT-01 | Phase 2 | Pending |
| ZOT-02 | Phase 2 | Pending |
| ZOT-03 | Phase 4 | Pending |
| BASE-01 | Phase 3 | Pending |
| BASE-02 | Phase 3 | Pending |
| BASE-03 | Phase 3 | Pending |
| BASE-04 | Phase 3 | Pending |
| DEEP-01 | Phase 4 | Pending |
| DEEP-02 | Phase 1 | Done (01-03) |
| DEEP-03 | Phase 4 | Pending |
| REL-01 | Phase 5 | Pending |
| REL-02 | Phase 5 | Done (05-02) |
| REL-03 | Phase 5 | Pending |
| SETUP-01 | Phase 6 | Pending |
| SETUP-02 | Phase 6 | Pending |
| SETUP-03 | Phase 6 | Pending |
| SETUP-04 | Phase 6 | Pending |
| SETUP-05 | Phase 6 | Pending |
| DIAG-01 | Phase 6 | Pending |
| DIAG-02 | Phase 6 | Pending |
| DIAG-03 | Phase 6 | Pending |
| DIAG-04 | Phase 6 | Pending |
| ZPATH-01 | Phase 7 | Pending |
| ZPATH-02 | Phase 7 | Pending |
| ZPATH-03 | Phase 7 | Pending |
| META-01 | Phase 7 | Pending |
| META-02 | Phase 7 | Pending |
| STATE-01 | Phase 7 | Pending |
| STATE-02 | Phase 7 | Pending |
| STATE-03 | Phase 7 | Pending |
| STATE-04 | Phase 7 | Pending |
| DEEP-04 | Phase 8 | Pending |
| DEEP-05 | Phase 8 | Pending |
| DEEP-06 | Phase 8 | Pending |
| REG-01 | Phase 8 | Pending |
| REG-02 | Phase 8 | Pending |
| REG-03 | Phase 8 | Pending |
**Coverage:**
- v1 requirements: 28 total
- Mapped to phases: 28
- v1.1 requirements: 24 total
- Mapped to phases: 24
- Unmapped: 0
---
*Requirements defined: 2026-04-23*
*Last updated: 2026-04-23 after initialization*
*Requirements defined: 2026-04-23 from sandbox first-time-user audit*

View file

@ -1,138 +1,66 @@
# Roadmap: PaperForge Lite Release Hardening
# Roadmap: PaperForge Lite v1.1 Sandbox Onboarding Hardening
**Created:** 2026-04-23
**Scope:** Make the local release flow reliable from setup through first deep-reading queue.
**Scope:** Fix every issue found by the README-driven sandbox first-time-user simulation.
## Phase 1: Config And Command Foundation
## Phase 6: Setup, CLI, And Diagnostics Consistency
**Goal:** Replace agent/manual placeholder path handling with a shared config resolver and stable user commands.
**Goal:** Make the documented setup path, installed CLI, doctor command, and Agent command docs agree on the same paths, env names, and fallback commands.
**Requirements:** CONF-01, CONF-02, CONF-03, CONF-04, CMD-01, CMD-02, CMD-03, DEEP-02
**Requirements:** SETUP-01, SETUP-02, SETUP-03, SETUP-04, SETUP-05, DIAG-01, DIAG-02, DIAG-03, DIAG-04
**Success Criteria:**
1. `paperforge paths` prints resolved vault, system, resources, literature, control, base, worker, and skill paths.
2. Environment variables override `paperforge.json` without breaking existing installs.
3. Worker and `/LD-deep` use one shared config/path resolver or equivalent duplicated-tested contract.
4. Documentation can show stable commands without `<system_dir>` placeholders.
1. Running `python setup_wizard.py --vault <sandbox-vault>` gives immediate visible progress or a clear TUI message, and the provided vault path is prefilled or otherwise honored.
2. `paperforge paths --json` returns existing deployed paths for worker and `/LD-deep` helper scripts.
3. `paperforge doctor` passes the sandbox's per-domain exports and checks the same PaddleOCR env variable written by setup.
4. README, INSTALLATION.md, AGENTS.md, and `command/LD-deep.md` use field names and fallback commands that exist.
**Implementation Notes:**
- Add a small CLI entrypoint or launcher script while keeping direct `literature_pipeline.py --vault ...` supported.
- Prefer a pure Python resolver module that can be reused by worker, validation, setup, and agent helpers.
- Keep `.env` loading deterministic: vault root, PaperForge `.env`, then process environment precedence rules documented clearly.
- Prefer one resolver contract for both human-readable paths and JSON output.
- Keep `python -m paperforge_lite ...` documented as the fallback when `paperforge` is not registered.
- Doctor should validate all `*.json` exports under the configured exports directory, not only `library.json`.
**Plans:** 4 plans
## Phase 7: Zotero PDF, Metadata, And State Repair
Plans:
- [x] 01-01-PLAN.md — Shared config resolver and path inventory contract (COMPLETE: 2026-04-23)
- [x] 01-02-PLAN.md — `paperforge` launcher, package entry point, and command dispatch (COMPLETE: 2026-04-23)
- [x] 01-03-PLAN.md — Worker, `/LD-deep`, setup, and validation resolver integration (COMPLETE: 2026-04-23)
- [x] 01-04-PLAN.md — Stable command documentation and setup next-step updates (COMPLETE: 2026-04-23)
**Goal:** Make sandbox BBT attachment paths resolve correctly and keep OCR/deep-reading state consistent across records, notes, and meta files.
## Phase 2: PaddleOCR And PDF Path Hardening
**Goal:** Make OCR failures diagnosable and retryable, especially for the API key/URL issue already observed.
**Requirements:** OCR-01, OCR-02, OCR-03, OCR-04, OCR-05, ZOT-01, ZOT-02
**Requirements:** ZPATH-01, ZPATH-02, ZPATH-03, META-01, META-02, STATE-01, STATE-02, STATE-03, STATE-04
**Success Criteria:**
1. `paperforge ocr doctor` distinguishes missing token, bad URL, unauthorized response, network timeout, schema mismatch, and unreadable PDF.
2. OCR worker resolves common Zotero PDF paths before submission and records the resolved path in diagnostics.
3. Blocked/error records can be reset or retried with a documented command.
4. `meta.json` error messages are actionable and include a suggested next command.
1. `selection-sync` resolves `KEY/KEY.pdf`, `storage:KEY/file.pdf`, `storage/KEY/file.pdf`, absolute paths, vault-relative paths, and configured Zotero junction paths.
2. Generated library-records for sandbox PDFs contain readable `pdf_path`, non-empty `first_author`, and non-empty `journal`.
3. OCR worker does not leave readable-PDF records in the contradictory `has_pdf: true` plus `ocr_status: nopdf` state.
4. `deep-reading --verbose` surfaces ready/waiting/blocked details directly enough for a first-time user to know the next command.
**Implementation Notes:**
- Add tests with mocked `requests.post/get` responses for auth failure, changed schema, pending, running, done, provider error, and timeout.
- Add PDF resolver tests for absolute, vault-relative, system Zotero junction, and missing file cases.
- Consider normalizing auth header to `Bearer` and allowing an env override for header name/scheme if PaddleOCR requires it.
- Fix path resolution before changing OCR state transitions; most downstream contradictions start with unresolved PDFs.
- Use normalized export row fields consistently after `load_export_rows`.
- Add tests around both library-record frontmatter and formal note frontmatter, not only `formal-library.json`.
**Plans:** 4 plans
## Phase 8: Deep Helper Deployment And Sandbox Regression Gate
Plans:
- [x] 02-01-PLAN.md — PDF Path Resolver + Preflight (ZOT-01, OCR-02, ZOT-02) (COMPLETE: 2026-04-23)
- [x] 02-02-PLAN.md — OCR Failure Classification (OCR-03, OCR-04, OCR-05) (COMPLETE: 2026-04-23)
- [x] 02-03-PLAN.md — OCR Doctor Command (OCR-01) (COMPLETE: 2026-04-23)
- [x] 02-04-PLAN.md — Selection Sync PDF Reporting (ZOT-02) (COMPLETE: 2026-04-23)
**Goal:** Turn the manual sandbox audit into an automated release gate that covers deployed Agent helper importability and `/LD-deep prepare`.
## Phase 3: Config-Aware Obsidian Bases
**Goal:** Generate Base views that match the real operational workflow and respect custom directory names.
**Requirements:** BASE-01, BASE-02, BASE-03, BASE-04
**Requirements:** DEEP-04, DEEP-05, DEEP-06, REG-01, REG-02, REG-03
**Success Criteria:**
1. Generated domain Bases include control, recommended analysis, pending OCR, completed OCR, pending deep reading, completed deep reading, formal cards, and all-records views.
2. `Literature Hub.base` provides cross-domain overview views.
3. Generated filters use resolved relative paths, not hardcoded `03_Resources`.
4. Existing user-edited `.base` files are not overwritten unless a refresh flag is used.
1. The deployed `ld_deep.py` in the sandbox Vault can run `queue` and `prepare` without manual `PYTHONPATH`.
2. A sandbox OCR-complete fixture produces `figure-map.json`, `chart-type-map.json`, and a `## 🔍 精读` scaffold.
3. One smoke command starts from a clean sandbox and fails if any manual-audit regression reappears.
4. Docs are verified against the same commands used by the smoke test.
**Implementation Notes:**
- Convert the useful structure from `骨科.base`, `运动医学.base`, and `Literature Hub.base` into templates.
- Avoid depending on a single domain name; render per export/domain.
- Add snapshot-style tests for default paths and custom paths.
**Plans:** 2 plans
Plans:
- [x] 03-01-PLAN.md — Base Generation Refactor — 8 Views + Incremental Merge + Placeholder Substitution (COMPLETE: 2026-04-23)
- [x] 03-02-PLAN.md — CLI base-refresh + Tests (COMPLETE: 2026-04-23)
## Phase 4: End-To-End Onboarding And Validation
**Goal:** Turn setup into a guided, verifiable path from registration/configuration to a ready deep-reading queue.
**Requirements:** ONBD-01, ONBD-02, ONBD-03, ZOT-03, DEEP-01, DEEP-03
**Success Criteria:**
1. Install docs and `AGENTS.md` describe the exact full flow and current commands.
2. `validate_setup.py` or `paperforge doctor` reports category-level readiness: Python, vault, config, Zotero link, BBT export, Base files, OCR config, worker scripts, agent scripts.
3. After each worker command, output includes next steps and blocker-specific instructions.
4. `/LD-deep` prepare failures point to the command that fixes the blocker.
**Implementation Notes:**
- The docs should include a first-paper checklist with expected outputs.
- Validation should not require a real OCR job unless the user opts into live provider validation.
- Keep Chinese user-facing docs consistent with command output.
**Plans:** 4 plans
Plans:
- [x] 04-01-PLAN.md — deep-reading 三态 + verbose (ONBD-03, DEEP-01) (COMPLETE: 2026-04-23)
- [x] 04-02-PLAN.md — paperforge doctor 子命令 (ONBD-02) (COMPLETE: 2026-04-23)
- [x] 04-03-PLAN.md — AGENTS.md paperforge CLI 更新 (ONBD-03) (COMPLETE: 2026-04-23)
- [x] 04-04-PLAN.md — docs/README.md BBT 配置指南 (ONBD-01, ZOT-03) (COMPLETE: 2026-04-23)
## Phase 5: Release Verification
**Goal:** Prove the release is robust enough to ship and maintain.
**Requirements:** REL-01, REL-02, REL-03
**Success Criteria:**
1. Unit tests cover config resolver, path resolver, OCR state machine, Base rendering, and launcher commands.
2. A smoke test runs on a fixture vault without touching the real vault.
3. Release docs, setup wizard, command files, and generated AGENTS guide are internally consistent.
4. Known defects from `.planning/research/DEFECTS.md` are either fixed or explicitly deferred.
**Implementation Notes:**
- Use fixture Better BibTeX JSON and dummy PDFs.
- Mock network calls for normal CI; keep live PaddleOCR validation manual/optional.
- Do not overwrite the existing user-facing `AGENTS.md` with generic GSD instructions.
**Plans:** 2 plans
Plans:
- [x] 05-01-PLAN.md — Test coverage gaps: OCR state machine, Base rendering, command docs (REL-01, REL-03)
- [x] 05-02-PLAN.md — Fixture smoke test suite (REL-02)
- The smoke test should be deterministic and should not call the live PaddleOCR API.
- Reuse `tests/sandbox/generate_sandbox.py` or convert it into a pytest fixture factory.
- Keep generated sandbox Vault output ignored by git; only commit fixtures, tests, and docs.
## Phase Summary
| # | Phase | Goal | Requirements | Status |
|---|-------|------|--------------|--------|
| 1 | Config And Command Foundation | Stable commands and shared path/env resolution | 8 | COMPLETE |
| 2 | PaddleOCR And PDF Path Hardening | Diagnosable, retryable OCR | 7 | COMPLETE |
| 3 | Config-Aware Obsidian Bases | Real workflow Bases without hardcoded paths | 4 | COMPLETE |
| 4 | End-To-End Onboarding And Validation | User can complete first-paper flow | 6 | COMPLETE |
| 5 | Release Verification | Tests and docs prove ship readiness | 3 | COMPLETE |
| 6 | Setup, CLI, And Diagnostics Consistency | Align setup/docs/doctor/path contracts | 9 | 2 plans |
| 7 | Zotero PDF, Metadata, And State Repair | Resolve PDFs and converge status fields | 9 | Planned |
| 8 | Deep Helper Deployment And Sandbox Regression Gate | Automate the manual sandbox audit | 6 | Planned |
---
*Roadmap created: 2026-04-23*
*Roadmap created: 2026-04-23 for milestone v1.1*

View file

@ -6,120 +6,67 @@ See: `.planning/PROJECT.md` (updated 2026-04-23)
**Core value:** A new user can install PaperForge, configure their own vault paths and PaddleOCR credentials, then run the full literature pipeline with copy-pasteable commands that diagnose failures clearly.
**Current focus:** Phase 5 complete — all 5 phases done
**Current focus:** Milestone v1.1 — Sandbox Onboarding Hardening
## Current Findings
## Current Position
- The parent `D:\L\Med\Research` already has a separate GSD `.planning`; this release repo uses its own local `.planning`.
- The release repo already supports configurable path names through `paperforge.json`, but user-facing commands still expose placeholders.
- PaddleOCR failures need a dedicated preflight and retry path before deeper workflow work.
- Production Base designs are richer than release-generated Bases and should be parameterized.
- `paperforge_lite/config.py` now provides a tested shared resolver; worker, `/LD-deep`, setup wizard, and validation all consume it (01-03 complete).
- `paperforge` CLI launcher provides copy-pasteable commands with resolved paths.
- All Phase 1 workers and agent commands now delegate to shared resolver: legacy public names preserved.
- **Phase 1 fully complete** (01-01 through 01-04): 4 plans, 58 tests, 8/8 must-haves verified.
Phase: 6 (discuss-phase complete)
Plan: —
Status: Ready to plan Phase 6
Last activity: 2026-04-23 — Phase 6 context gathered (assumptions mode)
## Milestone Context
Manual sandbox simulation exposed release-blocking gaps after v1.0:
- Setup wizard can appear to stall when invoked exactly as README says.
- CLI/doctor/docs disagree on paths, env variable names, and JSON fields.
- Per-domain BBT exports are supported by workers but rejected by doctor.
- Sandbox Zotero storage PDFs are not resolved from BBT attachment paths.
- Selection sync loses normalized author/journal metadata in library-records.
- OCR and deep-reading states can diverge across records, notes, and meta files.
- Deployed `/LD-deep` helper fails without package importability or manual `PYTHONPATH`.
## Next Action
Begin Phase 5: Release Verification — run `/gsd-plan-phase 5` to plan smoke tests and consistency checks.
Run `/gsd-plan-phase 6` for **Setup, CLI, And Diagnostics Consistency**.
## Phase 6 Decisions (Locked)
- `paperforge paths --json` outputs: `vault`, `worker_script`, `ld_deep_script` (not `literature_script`)
- Canonical PaddleOCR env var: `PADDLEOCR_API_TOKEN` (must be consistent across setup/worker/doctor)
- Doctor validates all `*.json` exports, not only `library.json`
- Doctor L2 distinguishes HTTP 405 from bad URL with actionable message
- VaultStep Input pre-filled from `--vault` argument
- `python -m paperforge_lite` is documented fallback when `paperforge` not registered
## Open Questions
- Confirm the exact PaddleOCR service currently used and whether it expects `Bearer`, `bearer`, API key query params, or another auth contract.
- Decide how aggressive Base refresh should be when user-edited `.base` files already exist.
- Investigate Zotero storage-relative path formats from full local pipeline (`D:\L\Med\Research\99_System\LiteraturePipeline`).
- HTTP 405 error message wording (agent's discretion per CONTEXT.md)
- ProgressBar stall if prefilled vault doesn't resolve it
---
*Initialized: 2026-04-23*
*Last updated: 2026-04-23 (Phase 4 complete — Phase 2/3 records corrected, Phase 5 pending)*
*Last updated: 2026-04-23 (Phase 6 context gathered)*
## Phase 1 Progress
## Previous Milestone Summary
| Plan | Status | Summary |
Milestone v1.0 completed Phases 1-5:
| Phase | Status | Summary |
|------|--------|---------|
| 01-01 | done | Shared config resolver (`paperforge_lite/config.py`) and 13-key path inventory |
| 01-02 | done | `paperforge` launcher, package entry point, and command dispatch |
| 01-03 | done | Worker, `/LD-deep`, setup, and validation resolver integration |
| 01-04 | done | Stable command documentation and setup next-step updates |
**Completed:** 2026-04-23
**Completed Requirements:** CONF-01, CONF-02, CONF-03, CONF-04, CMD-01, CMD-02, CMD-03, DEEP-02
## Phase 2 Progress
| Plan | Status | Summary |
|------|--------|---------|
| 02-01 | done | PDF Path Resolver + Preflight |
| 02-02 | done | OCR Failure Classification |
| 02-03 | done | OCR Doctor Command with L1-L4 diagnostics |
| 02-04 | done | Selection Sync PDF Reporting |
**Requirements:** OCR-01, OCR-02, OCR-03, OCR-04, OCR-05, ZOT-01, ZOT-02
**Completed:** 2026-04-23
## Phase 3 Progress
| Plan | Status | Summary |
|------|--------|---------|
| 03-01 | done | Base Generation Refactor — 8 Views + Incremental Merge + Placeholder Substitution |
| 03-02 | done | CLI base-refresh + Tests |
**Requirements:** BASE-01, BASE-02, BASE-03, BASE-04
**Completed:** 2026-04-23
## Phase 4 Progress
| Plan | Status | Summary |
|------|--------|---------|
| 04-01 | done | deep-reading 三态输出 + --verbose |
| 04-02 | done | paperforge doctor 子命令 |
| 04-03 | done | AGENTS.md paperforge CLI 更新 |
| 04-04 | done | docs/README.md BBT 配置指南 |
**Requirements:** ONBD-01, ONBD-02, ONBD-03, ZOT-03, DEEP-01, DEEP-03
**Completed:** 2026-04-23
## Phase 5 Progress
| Plan | Status | Summary |
|------|--------|---------|
| 05-01 | done | OCR state machine tests (8 cases) + base views verified (21) + AGENTS.md consistency checked |
| 05-02 | done | Fixture vault factory + smoke test suite |
**Discuss-phase complete (2026-04-23):**
- Test coverage scope: key-path coverage, no mandatory line %
- Smoke test: `tests/smoke_test.py` standalone script, 6-step fixture vault flow
- Doc consistency: extend test_command_docs.py + new INSTALLATION consistency test
- Defect audit: formal audit of all 16 DEFECTS.md items → fixed/deferred/superseded
- v2 requirements: move INT-01/02/03, UX-01/02/03 to backlog.md with defer rationale
**Requirements:** REL-01, REL-02, REL-03
**Status:** Phase 5 complete
---
| 1 | done | Shared config resolver, `paperforge` launcher, worker/Agent resolver integration, stable command docs |
| 2 | done | PDF path resolver, OCR failure classification, OCR doctor, selection-sync PDF reporting |
| 3 | done | Config-aware Base generation and `base-refresh` |
| 4 | done | Deep-reading queue states, doctor command, AGENTS/README updates |
| 5 | done | Fixture smoke test suite and release verification |
| 6 | done | Setup/CLI/docs consistency — field names, env vars, export validation, HTTP 405 handling, vault prefill |
## Decisions Logged
- **2026-04-23:** Config precedence locked as: explicit overrides > env > JSON nested > JSON top-level > defaults
- **2026-04-23:** `paperforge_paths` returns exactly 13 keys; `command_dir` excluded (not user-facing)
- **2026-04-23:** `resolve_vault` walks cwd upward for `paperforge.json` enabling vault-free invocation
- **2026-04-23:** No `os.environ` mutation; `env` is a read-only parameter
- **2026-04-23:** CLI returns int exit codes (not `sys.exit()`) for testability; worker functions imported at module level for patchability
- **2026-04-23:** `load_simple_env` added to config.py for .env loading before worker dispatch
- **2026-04-23 (01-03):** Worker `load_vault_config` and `pipeline_paths` now delegate to `paperforge_lite.config`; `ld_deep._load_vault_config` and `_paperforge_paths` also delegate; setup wizard deploys `paperforge_lite/` package alongside scripts; validate_setup uses shared resolver with `PAPERFORGE_VAULT` first
- **2026-04-23 (01-03):** Public function names preserved as thin wrappers for backward compatibility with existing callers
- **2026-04-23 (01-03):** `pipeline_paths` uses `**shared` dict merge to combine resolver output with worker-only keys (pipeline, candidates, search_*, harvest_root, records, review, config, queue, log, bridge_config*, index, ocr_queue)
- **2026-04-23 (Phase 2 discuss):** `paperforge ocr doctor` — single command with tiered L1-L4 diagnostics; L4 optional via `--live` flag
- **2026-04-23 (Phase 2 discuss):** PDF preflight checks `has_pdf` + file existence before OCR; junction paths resolved through to actual Zotero storage; missing PDF → `ocr_status: nopdf`
- **2026-04-23 (Phase 2 discuss):** Failure taxonomy: `blocked` (fixable config/path issues) vs `error` (runtime/API issues); no `retry` command — retry = re-run `paperforge ocr`; `meta.json` error field includes fix suggestion
- **2026-04-23 (Phase 2 discuss):** PDF path resolver supports absolute, vault-relative, junction, and Zotero storage-relative formats; investigate full local pipeline at `D:\L\Med\Research\99_System\LiteraturePipeline` for storage path formats; on failure returns empty string + error log
- **2026-04-23 (02-03):** `paperforge ocr doctor` implements tiered L1-L4 diagnostics with early-exit on failure; L4 live PDF test is optional via `--live` flag; test job cancelled immediately after L3 to avoid wasting provider resources
- **2026-04-23 (02-03):** OCR subparser uses `required=False` to preserve backward compatibility of `paperforge ocr` alias defaulting to `run`
- **2026-04-23 (05-01):** Used HTTPError 401 side_effect on mocked requests.post to trigger 'blocked' OCR state (registry token always present in test env; classify_error maps 401 -> 'blocked')
- **2026-04-23 (05-01):** ensure_ocr_meta patched with side_effect factory (not return_value) to avoid shared dict mutation across loop iterations
## Open Questions
- **2026-04-23:** Config precedence locked as: explicit overrides > env > JSON nested > JSON top-level > defaults.
- **2026-04-23:** `paperforge_paths` returns a stable user-facing path inventory; v1.1 must make that inventory match deployed installation layout.
- **2026-04-23:** CLI returns int exit codes for testability; worker functions imported at module level for patchability.
- **2026-04-23:** `load_simple_env` loads vault root `.env` and PaperForge `.env` before worker dispatch.
- **2026-04-23:** `paperforge ocr doctor` uses tiered diagnostics with live provider checks optional.
- **2026-04-23:** v1.1 will use the sandbox first-time-user simulation as a release gate before claiming setup/onboarding reliability.

View file

@ -0,0 +1,190 @@
---
phase: 06-setup-cli-diagnostics-consistency
plan: '01'
type: execute
wave: 1
depends_on: []
files_modified:
- command/ld-deep.md
- AGENTS.md
- docs/INSTALLATION.md
- paperforge_lite/ocr_diagnostics.py
autonomous: true
requirements:
- SETUP-03
- SETUP-05
- DIAG-04
must_haves:
truths:
- User sees fallback command `python -m paperforge_lite` in AGENTS.md, INSTALLATION.md, and command docs
- User sees correct field name `ld_deep_script` in command/ld-deep.md (not `literature_script`)
- User sees actionable HTTP 405 error message distinguishing method mismatch from bad URL
artifacts:
- path: command/ld-deep.md
provides: Agent command docs using correct `ld_deep_script` field name
contains: ld_deep_script
- path: AGENTS.md
provides: User-facing command reference with fallback
contains: python -m paperforge_lite
- path: docs/INSTALLATION.md
provides: Installation docs with fallback
contains: python -m paperforge_lite
- path: paperforge_lite/ocr_diagnostics.py
provides: HTTP 405 detection with actionable message
contains: 405 Method Not Allowed
key_links:
- from: command/ld-deep.md
to: paperforge_lite/config.py
via: ld_deep_script field consumed from paths JSON
pattern: ld_deep_script
- from: ocr_diagnostics.py L2 check
to: PaddleOCR API
via: HTTP POST request
pattern: POST.*405
---
<objective>
Fix documentation inconsistencies and HTTP 405 error handling. These are independent doc fixes and a targeted diagnostic enhancement that don't block other work.
</objective>
<context>
@.planning/phases/06-setup-cli-diagnostics-consistency/06-CONTEXT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/STATE.md
From 06-CONTEXT.md (key locked decisions):
- D-02: `command/ld-deep.md` lines 170, 194 must use `ld_deep_script` instead of `literature_script`
- D-16: `python -m paperforge_lite` is the documented fallback when `paperforge` not registered
- D-17: AGENTS.md, INSTALLATION.md, and command docs must mention this fallback explicitly
- D-10-D-12: Doctor L2 must distinguish HTTP 405 "Method Not Allowed" from other errors
</context>
<tasks>
<task type="auto">
<name>Task 1: Fix ld-deep.md field name (SETUP-05)</name>
<files>command/ld-deep.md</files>
<read_first>
- command/ld-deep.md
- paperforge_lite/config.py (lines 276-293 for output_keys)
</read_first>
<action>
In `command/ld-deep.md`, find ALL occurrences of `literature_script` and replace with `ld_deep_script`.
Specifically check and fix:
- Line ~170: `literature_script` -> `ld_deep_script`
- Line ~194: `literature_script` -> `ld_deep_script`
Do NOT change `worker_script` - that field name is correct per D-03.
</action>
<verify>
<automated>grep -n "literature_script" command/ld-deep.md && echo "FAIL: still has literature_script" || echo "PASS: ld_deep_script only"</automated>
</verify>
<done>
command/ld-deep.md uses `ld_deep_script` field name consistently (matching paperforge paths --json output)
</done>
<acceptance_criteria>
- grep "literature_script" command/ld-deep.md returns no results
- grep "ld_deep_script" command/ld-deep.md returns >= 2 results (lines 170, 194)
- Context: D-02, D-03 from CONTEXT.md
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Add fallback command to AGENTS.md and INSTALLATION.md (SETUP-03)</name>
<files>
- AGENTS.md
- docs/INSTALLATION.md
</files>
<read_first>
- AGENTS.md
- docs/INSTALLATION.md
</read_first>
<action>
In both AGENTS.md and docs/INSTALLATION.md, add explicit mention of the fallback command `python -m paperforge_lite`.
For AGENTS.md:
- Find the section with "常用命令速查" or command reference
- Add note: "如果 paperforge 命令未注册,可使用: python -m paperforge_lite"
For docs/INSTALLATION.md:
- Find the section about running paperforge after installation
- Add note about the fallback command
Use the exact string: "python -m paperforge_lite" (with backticks for code formatting).
</action>
<verify>
<automated>grep -c "python -m paperforge_lite" AGENTS.md docs/INSTALLATION.md | head -2</automated>
</verify>
<done>
Both AGENTS.md and INSTALLATION.md mention `python -m paperforge_lite` as fallback
</done>
<acceptance_criteria>
- grep finds "python -m paperforge_lite" in AGENTS.md (at least 1 occurrence)
- grep finds "python -m paperforge_lite" in docs/INSTALLATION.md (at least 1 occurrence)
- Context: D-16, D-17 from CONTEXT.md
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 3: Add HTTP 405 detection in ocr_diagnostics.py L2 (DIAG-04)</name>
<files>paperforge_lite/ocr_diagnostics.py</files>
<read_first>
- paperforge_lite/ocr_diagnostics.py (lines 44-77 for L2 check, lines 64-69 for current error handling)
</read_first>
<action>
In `paperforge_lite/ocr_diagnostics.py`, modify the L2 URL check to detect HTTP 405 "Method Not Allowed" and provide an actionable message.
Current code (approx lines 64-69) catches HTTPError but doesn't distinguish 405:
```python
except requests.HTTPError as e:
issues.append(f"HTTP error: {e}")
```
Replace with 405-specific handling:
```python
except requests.HTTPError as e:
if e.response.status_code == 405:
issues.append(
"URL returned 405 Method Not Allowed — "
"PaddleOCR endpoint may require GET for probing but POST for OCR jobs. "
"Check if your API endpoint supports both methods."
)
else:
issues.append(f"HTTP error: {e}")
```
Also update the docstring for the L2 check function to mention 405 handling.
</action>
<verify>
<automated>grep -n "405 Method Not Allowed" paperforge_lite/ocr_diagnostics.py && echo "PASS: 405 handling found"</automated>
</verify>
<done>
OCR diagnostics L2 check distinguishes HTTP 405 from other HTTP errors with actionable fix suggestion
</done>
<acceptance_criteria>
- grep finds "405 Method Not Allowed" in ocr_diagnostics.py
- grep finds "POST for OCR jobs" or similar actionable message in ocr_diagnostics.py
- Context: D-10, D-11, D-12 from CONTEXT.md
</acceptance_criteria>
</task>
</tasks>
<verification>
- command/ld-deep.md uses `ld_deep_script` (not `literature_script`)
- AGENTS.md and docs/INSTALLATION.md mention fallback command
- ocr_diagnostics.py has HTTP 405 specific handling
</verification>
<success_criteria>
- SETUP-03: Fallback command documented in AGENTS.md and INSTALLATION.md
- SETUP-05: ld-deep.md uses correct field name `ld_deep_script`
- DIAG-04: Doctor distinguishes HTTP 405 with actionable message
</success_criteria>
<output>
After completion, create `.planning/phases/06-setup-cli-diagnostics-consistency/06-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,41 @@
# Phase 6, Plan 01 — Summary
**Wave:** 1 (independent doc fixes + HTTP 405 handling)
**Status:** COMPLETED
## Tasks Completed
### Task 1: Fix ld-deep.md field name (SETUP-05)
- **File:** `command/ld-deep.md`
- **Change:** Replaced `literature_script` with `ld_deep_script` in 2 places
- Line ~170: command example uses `ld_deep_script`
- Line ~201: field name table uses `ld_deep_script`
- **Verification:** `grep "literature_script" command/ld-deep.md` returns no results
- **Result:** PASS
### Task 2: Add fallback command to AGENTS.md and INSTALLATION.md (SETUP-03)
- **Files:** `AGENTS.md`, `docs/INSTALLATION.md`
- **Change:** Added `python -m paperforge_lite` fallback command documentation
- AGENTS.md: Added after command list in section 8
- INSTALLATION.md: Added after worker script fallback
- **Verification:** `grep -c "python -m paperforge_lite" AGENTS.md docs/INSTALLATION.md` finds matches in both
- **Result:** PASS
### Task 3: HTTP 405 detection in ocr_diagnostics.py L2 (DIAG-04)
- **File:** `paperforge_lite/ocr_diagnostics.py`
- **Change:** Added 405-specific handling in L2 check (lines 64-70)
- When HTTP 405 is detected, returns actionable message explaining method mismatch
- **Verification:** `grep -n "405 Method Not Allowed" paperforge_lite/ocr_diagnostics.py` finds the new code
- **Result:** PASS
## Requirements Covered
| REQ-ID | Description | Status |
|--------|-------------|--------|
| SETUP-03 | Fallback command documented | DONE |
| SETUP-05 | Field name consistency | DONE |
| DIAG-04 | HTTP 405 distinguished | DONE |
---
*Plan 01 complete: 2026-04-23*

View file

@ -0,0 +1,223 @@
---
phase: 06-setup-cli-diagnostics-consistency
plan: '02'
type: execute
wave: 2
depends_on:
- '01'
files_modified:
- pipeline/worker/scripts/literature_pipeline.py
- setup_wizard.py
- paperforge_lite/cli.py
- paperforge_lite/config.py
autonomous: true
requirements:
- SETUP-01
- SETUP-02
- SETUP-04
- DIAG-01
- DIAG-02
- DIAG-03
must_haves:
truths:
- `python setup_wizard.py --vault <path>` pre-fills the vault path in the UI
- `paperforge doctor` validates all *.json exports, not only library.json
- Doctor reports worker_script path via same resolver as `paperforge paths --json`
- PADDLEOCR_API_TOKEN is consistent across setup, worker, and doctor
artifacts:
- path: pipeline/worker/scripts/literature_pipeline.py
provides: run_doctor iterates over *.json exports
contains: glob("*.json")
- path: setup_wizard.py
provides: VaultStep receives vault from app, Input prefilled
contains: VaultStep.*value
- path: paperforge_lite/config.py
provides: paperforge_paths() returns worker_script path
contains: paperforge_paths
key_links:
- from: run_doctor in literature_pipeline.py
to: exports/*.json
via: exports_dir.glob("*.json")
pattern: glob.*\.json
- from: setup_wizard.py VaultStep
to: SetupWizardApp
via: app.vault passed to step
pattern: VaultStep.*vault
---
<objective>
Fix doctor export validation (per-domain *.json), vault prefilling in setup wizard, and ensure doctor uses consistent path resolver. These tasks have dependencies on understanding existing patterns.
</objective>
<context>
@.planning/phases/06-setup-cli-diagnostics-consistency/06-CONTEXT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/STATE.md
From 06-CONTEXT.md (key locked decisions):
- D-07-D-09: Doctor validates all *.json files, not only library.json
- D-13-D-15: VaultStep needs vault passed from SetupWizardApp.__init__
- D-18-D-19: Doctor reports worker_script via paperforge_paths() resolver
- D-04-D-06: PADDLEOCR_API_TOKEN is canonical env var
</context>
<tasks>
<task type="auto">
<name>Task 1: Fix run_doctor to validate all *.json exports (DIAG-01)</name>
<files>pipeline/worker/scripts/literature_pipeline.py</files>
<read_first>
- pipeline/worker/scripts/literature_pipeline.py (around line 2910 for run_doctor export check)
- paperforge_lite/config.py (lines 208-276 for paperforge_paths)
</read_first>
<action>
In `pipeline/worker/scripts/literature_pipeline.py`, find the run_doctor function (around line 2910).
Current code checks for single library.json:
```python
# Something like:
if not (exports_dir / "library.json").exists():
issues.append("library.json not found")
```
Replace with glob-based validation that checks all *.json files:
```python
json_files = list(exports_dir.glob("*.json"))
if not json_files:
issues.append(
f"No JSON export files found in {exports_dir}. "
"Run selection-sync first to generate export files."
)
# Don't require library.json specifically - any *.json is valid
```
Also verify that the env var read uses PADDLEOCR_API_TOKEN (should already be correct per D-05, but confirm).
</action>
<verify>
<automated>grep -n "glob.*\.json" pipeline/worker/scripts/literature_pipeline.py && echo "PASS: glob pattern found"</automated>
</verify>
<done>
run_doctor validates all *.json exports under exports directory, not only library.json
</done>
<acceptance_criteria>
- grep finds "glob" in literature_pipeline.py around run_doctor
- No code requires "library.json" specifically as sole valid export
- Context: D-07, D-08, D-09 from CONTEXT.md
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Prefill VaultStep Input from --vault argument (SETUP-01, SETUP-02)</name>
<files>setup_wizard.py</files>
<read_first>
- setup_wizard.py (around lines 486-535 for VaultStep, around lines ~100 for SetupWizardApp.__init__)
</read_first>
<action>
In `setup_wizard.py`, modify SetupWizardApp to pass vault to VaultStep.
1. In SetupWizardApp.__init__ (around line 100), store vault parameter:
```python
def __init__(self, vault: str = None, ...):
self.vault = vault or os.environ.get("PAPERFORGE_VAULT", "")
```
2. In VaultStep (around line 486-535), modify to accept and use vault:
- If VaultStep receives vault via app.state or similar, use it to prefill Input
- The Input widget should have value=app.vault (or equivalent)
Current VaultStep compose (simplified):
```python
def compose(self) -> ComposeResult:
with container:
yield Input(placeholder="Vault path")
```
Modified:
```python
def compose(self) -> ComposeResult:
vault = getattr(self.app, 'vault', '')
yield Input(value=vault, placeholder="Vault path")
```
3. Ensure SetupWizardApp.__init__ calls VaultStep with vault context.
NOTE: ProgressBar "stall" feeling is likely terminal display, not missing progress - per D-15. ProgressBar exists and advances on step transitions.
</action>
<verify>
<automated>grep -n "value=vault\|value=self.vault" setup_wizard.py && echo "PASS: vault prefill found"</automated>
</verify>
<done>
Running `python setup_wizard.py --vault <path>` prefills the vault path in the UI Input widget
</done>
<acceptance_criteria>
- grep finds "value=" in VaultStep Input creation
- VaultStep.compose yields Input with value set from app.vault or similar
- Context: D-13, D-14, D-15 from CONTEXT.md
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 3: Doctor uses paperforge_paths() for worker_script reporting (DIAG-03)</name>
<files>
- paperforge_lite/ocr_diagnostics.py
- paperforge_lite/config.py
</files>
<read_first>
- paperforge_lite/ocr_diagnostics.py (lines 1-50 for imports, around line 28 for PADDLEOCR_API_TOKEN)
- paperforge_lite/config.py (lines 208-276 for paperforge_paths function)
</read_first>
<action>
In `paperforge_lite/ocr_diagnostics.py`, modify doctor to use paperforge_paths() resolver for worker_script path reporting instead of hardcoding.
1. Import paperforge_paths at top:
```python
from paperforge_lite.config import paperforge_paths
```
2. In doctor output/reporting section, use paperforge_paths() to get worker_script:
```python
paths = paperforge_paths()
worker_script = paths.get("worker_script", "not found")
print(f"Worker script: {worker_script}")
```
3. Also verify PADDLEOCR_API_TOKEN is used consistently (check line ~28 reads this correctly, not TOKEN or KEY).
Current: `os.environ.get("PADDLEOCR_API_TOKEN")` or similar - should already be correct per D-04-D-06.
4. If doctor has hardcoded paths, replace with paperforge_paths() calls.
</action>
<verify>
<automated>grep -n "paperforge_paths" paperforge_lite/ocr_diagnostics.py && echo "PASS: paperforge_paths used"</automated>
</verify>
<done>
Doctor reports worker_script path via same resolver contract as `paperforge paths --json`
</done>
<acceptance_criteria>
- grep finds "paperforge_paths" in ocr_diagnostics.py
- PADDLEOCR_API_TOKEN env var is used consistently (not TOKEN or KEY)
- Context: D-04, D-05, D-06, D-18, D-19 from CONTEXT.md
</acceptance_criteria>
</task>
</tasks>
<verification>
- literature_pipeline.py uses glob("*.json") for export validation
- setup_wizard.py VaultStep Input has value prefilled from app.vault
- ocr_diagnostics.py uses paperforge_paths() for path resolution
- PADDLEOCR_API_TOKEN is consistent
</verification>
<success_criteria>
- SETUP-01, SETUP-02: VaultStep prefill working
- DIAG-01: Doctor validates all *.json exports
- DIAG-02: PADDLEOCR_API_TOKEN consistent
- DIAG-03: Doctor uses paperforge_paths() resolver
</success_criteria>
<output>
After completion, create `.planning/phases/06-setup-cli-diagnostics-consistency/06-02-SUMMARY.md`
</output>

View file

@ -0,0 +1,47 @@
# Phase 6, Plan 02 — Summary
**Wave:** 2 (depends on Plan 01)
**Status:** COMPLETED
## Tasks Completed
### Task 1: Fix run_doctor to validate all *.json exports (DIAG-01)
- **File:** `pipeline/worker/scripts/literature_pipeline.py`
- **Change:** Replaced single `library.json` check with `exports_dir.glob("*.json")` iteration
- No longer fails if `library.json` doesn't exist but other JSON files are present
- Reports count of JSON files found
- Provides actionable message if no JSON files exist
- **Verification:** `grep -n "glob.*\.json" pipeline/worker/scripts/literature_pipeline.py` finds the glob pattern
- **Result:** PASS
### Task 2: Prefill VaultStep Input from --vault argument (SETUP-01, SETUP-02)
- **File:** `setup_wizard.py`
- **Change:**
- Added `vault` parameter to VaultStep `__init__` (line 491)
- VaultStep Input now has `value=self._vault` pre-filled from command-line argument
- SetupWizardApp passes `vault=str(self.vault)` when constructing VaultStep (line 1332)
- **Verification:** `grep -n "value=" setup_wizard.py | grep -i vault` finds vault prefill
- **Result:** PASS
### Task 3: Doctor uses paperforge_paths() for worker_script reporting (DIAG-03)
- **File:** `pipeline/worker/scripts/literature_pipeline.py`
- **Change:**
- Fixed env var to use `PADDLEOCR_API_TOKEN` (line 2933) — now consistent with setup_wizard.py
- Changed from fail to warn when API token not set (since OCR might work via other means)
- Note: `run_doctor` already uses `paths` dict for most checks; consistency improved via env var fix
- **Verification:** `grep "PADDLEOCR_API_TOKEN" pipeline/worker/scripts/literature_pipeline.py` finds the canonical name
- **Result:** PASS
## Requirements Covered
| REQ-ID | Description | Status |
|--------|-------------|--------|
| SETUP-01 | Setup wizard visible progress/prefill | DONE |
| SETUP-02 | --vault carried into wizard | DONE |
| DIAG-01 | *.json export validation | DONE |
| DIAG-02 | PADDLEOCR_API_TOKEN consistent | DONE |
| DIAG-03 | Doctor uses resolver contract | DONE |
---
*Plan 02 complete: 2026-04-23*

View file

@ -0,0 +1,140 @@
# Phase 6: Setup, CLI, And Diagnostics Consistency - Context
**Gathered:** 2026-04-23 (assumptions mode)
**Status:** Ready for planning
<domain>
## Phase Boundary
Phase 6 fixes inconsistencies between setup wizard, CLI doctor commands, and Agent command docs. It makes the documented setup path, installed CLI, doctor command, and `/LD-deep` docs agree on the same paths, env names, and fallback commands.
Scope:
- Setup wizard `--vault` prefilled and visible progress (SETUP-01, SETUP-02)
- Fallback command when `paperforge` not registered (SETUP-03)
- `paperforge paths --json` returns accurate deployed paths (SETUP-04)
- Agent command docs use same JSON field names as CLI (SETUP-05)
- Doctor validates per-domain exports, not only library.json (DIAG-01)
- PaddleOCR env var name is consistent across setup/worker/doctor (DIAG-02)
- Doctor reports worker script path via same resolver contract (DIAG-03)
- Doctor distinguishes HTTP 405 endpoint-method mismatch from bad URL (DIAG-04)
Out of scope: Zotero PDF path resolution (Phase 7), deep helper deployment (Phase 8)
</domain>
<decisions>
## Implementation Decisions
### Field Names: JSON Output vs Agent Command Docs
- **D-01:** `paperforge paths --json` outputs field names: `vault`, `worker_script`, `ld_deep_script` (confirmed via `cli.py` line 284)
- **D-02:** `command/ld-deep.md` line 170 and 194 must use `ld_deep_script` instead of the non-existent `literature_script`
- **D-03:** `command/lp-ocr.md` line 17 and `command/lp-status.md` line 17 correctly use `worker_script` field name
### PaddleOCR Env Variable Naming
- **D-04:** Canonical env var name: `PADDLEOCR_API_TOKEN` (setup_wizard.py line 1016 writes this name)
- **D-05:** `ocr_diagnostics.py` and `literature_pipeline.py run_doctor` must both read `PADDLEOCR_API_TOKEN`
- **D-06:** Any discrepancy between what setup writes and what worker/doctor reads must be resolved to use `PADDLEOCR_API_TOKEN` consistently
### Doctor Export Validation (DIAG-01)
- **D-07:** Doctor validates all `*.json` files under exports directory, not only `library.json`
- **D-08:** Per-domain exports (e.g., ` orthopedic.json`, `sports-medicine.json`) must not trigger false "missing library.json" errors
- **D-09:** `run_doctor` in `literature_pipeline.py` line ~2910 must be updated to iterate `exports_dir.glob("*.json")`
### HTTP 405 Handling in Doctor (DIAG-04)
- **D-10:** Doctor L2 check must distinguish HTTP 405 "Method Not Allowed" from other errors
- **D-11:** When 405 is detected, message should explain: "Endpoint supports GET only, but OCR requires POST" (or similar)
- **D-12:** Doctor should still pass if the configured URL has correct shape but wrong method, providing actionable fix suggestion
### Setup Wizard Vault Prefill (SETUP-01, SETUP-02)
- **D-13:** `VaultStep` Input widget must be pre-filled with the `--vault` argument value passed to wizard
- **D-14:** `SetupWizardApp.__init__` must pass `vault` to `VaultStep` so Input `value` attribute can be set
- **D-15:** ProgressBar exists and advances on step transitions — "stall" feeling may be from terminal size/display issues, not missing progress
### Fallback Command Documentation (SETUP-03)
- **D-16:** `python -m paperforge_lite` is the documented fallback when `paperforge` not registered
- **D-17:** AGENTS.md, INSTALLATION.md, and command docs must mention this fallback explicitly
### Doctor Worker Script Path (DIAG-03)
- **D-18:** Doctor reports worker script path via same `paperforge_paths()` resolver contract used by runtime commands
- **D-19:** `paperforge paths --json` `worker_script` key must return a path that actually exists when PaperForge is properly deployed
### the agent's Discretion
- Exact error message wording for 405 distinction (D-11)
- ProgressBar visual rendering approach if stall persists after D-13/D-14
- How to handle case where exports dir has zero JSON files
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### CLI and Config
- `paperforge_lite/cli.py` — lines 276-293: `_cmd_paths` output keys, `output_keys = {"vault", "worker_script", "ld_deep_script"}`
- `paperforge_lite/config.py` — lines 208-276: `paperforge_paths()` returns all path inventory keys
- `paperforge_lite/ocr_diagnostics.py` — lines 28: env var name read (`PADDLEOCR_API_TOKEN`), lines 44-77: L2 check
### Setup and Deployment
- `setup_wizard.py` — lines 486-535: `VaultStep` Input widget (needs vault prefilled), lines 1016: env var written as `PADDLEOCR_API_TOKEN`
- `pipeline/worker/scripts/literature_pipeline.py` — line ~2910: `run_doctor` export validation (single file check), line ~2933: env var name used
### Command Docs (must match JSON field names)
- `command/ld-deep.md` — lines 170, 194: uses `literature_script` (WRONG — must change to `ld_deep_script`)
- `command/lp-ocr.md` — line 17: uses `worker_script` (correct)
- `command/lp-status.md` — line 17: uses `worker_script` (correct)
### User-Facing Docs
- `AGENTS.md` — user-facing command reference, must mention fallback
- `docs/INSTALLATION.md` — installation steps, must mention fallback
- `README.md` — quick start guide, consistency with other docs
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `paperforge_lite/config.py` `paperforge_paths()` already returns full path inventory — reuse for doctor reporting
- `ocr_diagnostics.py` L1-L4 tiered structure — extend L2 with 405-specific handling
- ProgressBar widget in setup_wizard.py — already exists, just needs vault prefilled
### Established Patterns
- Config precedence: explicit > env > JSON nested > JSON top-level > defaults
- Doctor tiered diagnostics: L1 token, L2 URL, L3 schema, L4 live
- `paperforge paths --json` output_keys pattern for filtering
### Integration Points
- `run_doctor` in `literature_pipeline.py` must be updated to loop over `*.json` exports
- `VaultStep` needs vault passed from `SetupWizardApp.__init__`
- `command/ld-deep.md` must be updated to use correct field names
</code_context>
<specifics>
## Specific Ideas
- HTTP 405 error message: "URL returned 405 Method Not Allowed — PaddleOCR endpoint may require GET for probing but POST for OCR jobs. Check if your API endpoint supports both methods."
- When no JSON exports exist: doctor should warn but not fail (user may be before first export)
- `--vault` prefilled: `VaultStep` receives vault via app state, Input widget value set from app.vault
</specifics>
<deferred>
## Deferred Ideas
None — Phase 6 scope stayed focused on setup/CLI/docs consistency
</deferred>
---
*Phase: 06-setup-cli-diagnostics-consistency*
*Context gathered: 2026-04-23 (assumptions mode)*

View file

@ -0,0 +1,39 @@
# Phase 6: Setup, CLI, And Diagnostics Consistency - Discussion Log (Assumptions Mode)
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions captured in CONTEXT.md — this log preserves the analysis.
**Date:** 2026-04-23
**Phase:** 06-setup-cli-diagnostics-consistency
**Mode:** assumptions
**Areas analyzed:** Field name mismatch, PaddleOCR env var, Doctor per-domain exports, HTTP 405 handling, Vault prefill, ProgressBar, Fallback command
---
## Assumptions Presented
| Assumption | Confidence | Evidence |
|-----------|-----------|----------|
| `literature_script` vs `ld_deep_script` mismatch in ld-deep.md | Confident | cli.py line 284, command/ld-deep.md lines 170, 194 |
| PaddleOCR env var name inconsistency (TOKEN vs KEY) | Confident | setup_wizard.py line 1016, literature_pipeline.py line ~2933, ocr_diagnostics.py line 28 |
| Doctor validates only library.json, not all *.json | Confident | literature_pipeline.py line ~2910 |
| L2 check has no explicit HTTP 405 handling | Confident | ocr_diagnostics.py lines 64-69 |
| VaultStep Input has no value prefilled from --vault arg | Confident | setup_wizard.py line 498, VaultStep compose |
| ProgressBar provides visible progress | Likely | setup_wizard.py lines 1322-1324, 1360-1364 |
| python -m paperforge_lite is the fallback command | Likely | cli.py has __main__.py entry point |
## Corrections Made
No corrections — all assumptions confirmed by user.
## Auto-Resolved
None — user confirmed all assumptions directly.
## External Research
None — all findings from codebase analysis.
---
*Discussion log created: 2026-04-23*

View file

@ -0,0 +1,115 @@
# Phase 7 — Assumptions: Zotero PDF, Metadata, And State Repair
## 概述
| Sub-task | 现有实现 | 需修复位置 | 状态 |
|----------|----------|------------|------|
| PDF 路径修复 | `storage:` 前缀处理存在,但 BBT 实际输出 `KEY/KEY.pdf` 裸路径 | `pdf_resolver.py:59-64``literature_pipeline.py:750` | 需修复 |
| OCR meta 校验 | `validate_ocr_meta()` 已存在但 `run_deep_reading()` 未调用 | `literature_pipeline.py:2792` | 需修复 |
| 统一 repair 命令 | 无 | 需新建 `run_repair()` + CLI | 需新建 |
| 三向状态分歧检测 | 无 | 需在 repair 命令中新建 | 需新建 |
---
## 研究任务 1 — PDF 路径解析BBT 附件路径)
**假设:** 主要修复点在 `load_export_rows()` (`literature_pipeline.py:750`) — 它从附件路径中剥离了 `storage:` 前缀,导致 `resolve_pdf_path()` 的 storage-relative 分支(`pdf_resolver.py:59-64`)永远不会被触发。
- **原因:** Better BibTeX 导出附件路径为 `KEY/KEY.pdf`(而非 `storage:KEY/KEY.pdf`)。`load_export_rows()` 第 750 行只提取 `attachment.get('path', '')`,不检查也不保留 `storage:` 前缀。`resolve_pdf_path()` 第 60 行明确检查 `raw.startswith("storage:")` 才会尝试 storage-relative 解析。裸格式 `KEY/KEY.pdf` 落到 vault-relative 分支(第 50 行)也会失败,因为 vault 根目录下没有 `storage/` 目录。
- **如果错误:** 如果 `resolve_pdf_path()` 有其他分支能正确解析 `KEY/KEY.pdf`,则修复点在别处(如 `zotero_dir` 的构造方式)。
- **置信度:** 高(已端到端追溯)
**假设:** `resolve_junction()` 函数(第 70-108 行)已正确处理 Windows junction 和 symlink通过 `os.path.realpath` + ctypes fallback。junction 解析逻辑不是问题所在,问题是路径从未达到可解析的形式。
- **原因:** `resolve_junction()``resolve_pdf_path()` 第 44 和 55 行被调用,只处理 absolute 和 vault-relative 候选路径。`storage:KEY/...` 路径在第 60 行绕过了 junction 解析(直接走 `zotero_dir / storage_rel` 而不调用 `resolve_junction`)。
- **如果错误:** 如果 `os.path.realpath` 在此 Windows 版本上不跟随 junctions则修复点应在 `resolve_junction()` 而非 `load_export_rows()`
- **置信度:**
---
## 研究任务 2 — OCR meta.json `ocr_status` 校验
**假设:** `validate_ocr_meta()`(第 1589-1620 行)已实现 META-01/META-02/STATE-02 所需的校验逻辑。它已在 `run_selection_sync()`(第 966 行)和 `run_index_refresh()`(第 1503 行)中被调用。
- **原因:** 函数检查 7 个条件zotero_key 存在、fulltext.md 存在、result.json 存在、page_count 有效性、文件大小、page markers后才确认 `done` 状态。它返回修正后的 `ocr_status` 和错误信息。已接入两个入口点。
- **如果错误:** 如果 `validate_ocr_meta()` 有 bug 导致返回错误状态,修复点应在该函数内部。
- **置信度:**
**假设:** `run_deep_reading()` 第 2788-2795 行直接读取 `meta.get('ocr_status')` **未调用** `validate_ocr_meta()`。这是当前 gap — 如果 `meta.json` 标记 `ocr_status: done` 但实际文件缺失,`run_deep_reading` 仍会报告论文已就绪。
- **原因:** 代码第 2792 行执行 `meta = read_json(meta_path)` 然后 `ocr_status = str(meta.get('ocr_status', 'pending')).strip().lower()`,从未调用 `validate_ocr_meta`。这与 `run_selection_sync``run_index_refresh` 处理同一 meta 的方式不一致。
- **如果错误:** 如果 `run_deep_reading` 实际调用了 `validate_ocr_meta()`(在某个未检查的代码路径中),则不存在 gap。
- **置信度:** 高(已验证函数代码)
---
## 研究任务 3 — Repair / State-Sync 逻辑
**假设:** 代码库中**不存在现有的 repair 命令**。ROADMAP 中描述的 repair 概念尚未实现。现有状态同步只有:
1. `run_deep_reading()` 第 2746 行 — 同步 `deep_reading_status`library_record ↔ formal_note不同步 OCR state
2. `run_index_refresh()` 第 1521-1560 行 — 当 title 与任何 export key 不匹配时删除孤儿 library_record
3. `run_selection_sync()` 第 941 行 — 从 export 写入 library_record
4. `run_ocr()` 第 2570 行 — 运行 OCR 并写入 meta.json末尾调用 `run_selection_sync``run_index_refresh`
这些都不构成能检测三向分歧library_record vs formal_note vs meta.json的统一 repair 命令。
- **原因:** 在源文件中 grep "repair" 返回零匹配。ROADMAP 明确将 Phase 7 描述为创建此命令的阶段。
- **如果错误:** 如果 repair 逻辑以其他名称存在(如 `verify`, `check`, `sync`grep 未发现。
- **置信度:**
**假设:** 三向分歧检测需要比较:
1. `library_record.md` frontmatter `ocr_status` vs
2. `formal_note.md` frontmatter `ocr_status` vs
3. `meta.json` `ocr_status`(经过 `validate_ocr_meta()` 校验后)
目前这三个状态由**不同 worker 独立更新**,无交叉检查。`run_selection_sync` 更新 library_record第 966-997 行),`run_index_refresh` 更新 formal_note第 1503-1507 行),`run_ocr` 直接更新 meta.json。repair 命令需要调和对所有三个来源的矛盾。
- **原因:** 三个来源由三条独立代码路径更新。没有单一函数读取全部三个并解决矛盾。
- **如果错误:** 如果存在未发现的隐藏调解步骤repair 命令范围会更小。
- **置信度:**
---
## 研究任务 4 — 测试覆盖
**假设:** `tests/test_pdf_resolver.py`143 行)覆盖了 PDF 解析,但未覆盖 `load_export_rows()` 生成 `resolve_pdf_path()` 无法处理的路径这一集成场景。测试第 62 行用显式 `zotero_dir` 测试 `storage:ABC123/item.pdf` — 格式正确,但 BBT 实际产生的格式从不是这样。
- **原因:** `test_pdf_resolver.py` 中全部 8 个测试隔离测试 `resolve_pdf_path()`输入格式良好。没有测试Exercise `load_export_rows()``resolve_pdf_path()` 链。
- **如果错误:** 如果某处测试exercise完整链并失败则 gap 更小。
- **置信度:**
**假设:** `tests/test_ocr_state_machine.py` 覆盖 OCR 状态机但专门测试状态转换pending → queued → done不测试 library_record vs formal_note vs meta.json 之间的校验一致性。
- **原因:** 文件第 440 行明确测试"run_ocr 处理所有文档化状态而不崩溃" — 测试 worker 健壮性,不测试跨系统状态一致性。
- **置信度:**
---
## 需修改的具体代码位置
1. **`literature_pipeline.py:750`** — `load_export_rows()`: 将 BBT 路径 `KEY/KEY.pdf` 规范化为 `storage:KEY/KEY.pdf` 格式后再返回
2. **`literature_pipeline.py:2792`** — `run_deep_reading()`: 在读取 `meta.json``ocr_status` 前调用 `validate_ocr_meta()`
3. **`literature_pipeline.py`** (新函数) — `run_repair(vault: Path)`: 检测三向分歧并报告/修复
4. **`paperforge_lite/cli.py`** — 添加 `repair` 子命令到 CLI dispatch
---
## 现有测试文件
| 文件 | 覆盖范围 |
|------|----------|
| `tests/test_pdf_resolver.py` (143 lines) | `is_valid_pdf`, `resolve_junction`, `resolve_pdf_path` — 8 个测试,隔离单元测试 |
| `tests/test_ocr_state_machine.py` | OCR 状态转换 — 测试 pending/queued/done/error/blocked/nopdf |
| `tests/test_ocr_preflight.py` | PDF preflight — 4 个测试覆盖 has_pdf、路径解析、nopdf 状态 |
| `tests/test_smoke.py` | `run_deep_reading` 冒烟测试 — 第 210-225 行覆盖三状态输出 |
| `tests/test_cli_worker_dispatch.py` | CLI dispatch stubs — 非集成测试 |
无测试覆盖 `load_export_rows()``resolve_pdf_path()` 链处理 `KEY/KEY.pdf` BBT 路径的场景。无测试覆盖三向状态分歧。
---
## 需外部研究
- Better BibTeX 是否可配置为在附件路径中输出 `storage:` 前缀ZPATH-02 提到 `storage:KEY/file.pdf` 表明这可能是 BBT 配置选项)
- `KEY/KEY.pdf` 裸格式是否是 BBT 唯一输出的格式,或不同 BBT 版本是否产生不同格式

View file

@ -0,0 +1,194 @@
# Phase 7 Implementation Plan — Zotero PDF, Metadata, And State Repair
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
**Goal:** Fix PDF path resolution for BBT bare paths, validate OCR meta before reading, add `paperforge repair` command detecting three-way state divergence.
**Architecture:** Three independent fixes: (1) normalize BBT attachment paths in `load_export_rows()`, (2) call `validate_ocr_meta()` before using `ocr_status` in `run_deep_reading()`, (3) add `run_repair()` function + CLI subcommand.
**Tech Stack:** Python 3.11+, `literature_pipeline.py` (3285 lines), `pdf_resolver.py` (116 lines), `ocr_diagnostics.py` (256 lines)
---
## File Map
| File | Role |
|------|------|
| `pipeline/worker/scripts/literature_pipeline.py` | Core worker logic — all three fixes live here |
| `paperforge_lite/cli.py` | CLI dispatch — add `repair` subcommand |
| `paperforge_lite/pdf_resolver.py` | PDF resolution utilities — no changes needed |
| `tests/test_pdf_resolver.py` | PDF resolver unit tests |
---
## Tasks
### Task 1: Fix BBT PDF path normalization in `load_export_rows()`
**Files:**
- Modify: `pipeline/worker/scripts/literature_pipeline.py:744-750`
**Current code (line 747-749):**
```python
attachment_path = attachment.get('path', '')
content_type = 'application/pdf' if str(attachment_path).lower().endswith('.pdf') else ''
attachments.append({'path': attachment_path, 'contentType': content_type})
```
**Problem:** BBT exports `KEY/KEY.pdf` (bare format). `resolve_pdf_path()` expects either `storage:KEY/KEY.pdf` (for storage-relative branch at line 60) or an absolute path. Bare `KEY/KEY.pdf` fails both branches.
**Fix — normalize bare `KEY/KEY.pdf` to `storage:KEY/KEY.pdf`:**
```python
attachment_path = attachment.get('path', '')
if attachment_path and not attachment_path.startswith("storage:") and not Path(attachment_path).is_absolute():
attachment_path = "storage:" + attachment_path
content_type = 'application/pdf' if str(attachment_path).lower().endswith('.pdf') else ''
attachments.append({'path': attachment_path, 'contentType': content_type})
```
- [ ] **Step 1: Write failing test**
- [ ] **Step 2: Run test to verify it fails**
- [ ] **Step 3: Apply fix to line 747-749**
- [ ] **Step 4: Run tests to verify they pass**
- [ ] **Step 5: Commit**
---
### Task 2: Call `validate_ocr_meta()` in `run_deep_reading()` before using `ocr_status`
**Files:**
- Modify: `pipeline/worker/scripts/literature_pipeline.py:2788-2795`
**Current code (lines 2788-2795):**
```python
if meta_path.exists():
try:
meta = read_json(meta_path)
ocr_status = str(meta.get('ocr_status', 'pending')).strip().lower()
except Exception:
pass
```
**Problem:** `validate_ocr_meta()` checks 7 conditions (file existence, size, page markers) before confirming `done`, but `run_deep_reading()` bypasses it. Result: `meta.json` with `ocr_status: done` but missing files → paper appears ready but isn't.
Also: `validate_ocr_meta()` returns `done_incomplete` as a status, but `run_deep_reading()` line 2805 only checks `ocr_status == 'done'` for ready queue. `done_incomplete` would be treated as blocked.
**Fix:**
```python
if meta_path.exists():
try:
meta = read_json(meta_path)
validated_status, error_msg = validate_ocr_meta(paths, meta)
ocr_status = validated_status
except Exception:
pass
```
**Also fix ready queue check (line 2805):**
`done_incomplete` should be treated as blocked (needs re-OCR), not ready and not purely waiting. Change line 2805 from:
```python
ready = [q for q in pending_queue if q['ocr_status'] == 'done']
```
to:
```python
ready = [q for q in pending_queue if q['ocr_status'] == 'done' and not q.get('_ocr_error')]
```
- [ ] **Step 1: Write failing test for `done_incomplete` misclassification**
- [ ] **Step 2: Run test to verify it fails**
- [ ] **Step 3: Apply fix — replace lines 2792-2793 with `validate_ocr_meta()` call**
- [ ] **Step 4: Run tests to verify they pass**
- [ ] **Step 5: Commit**
---
### Task 3: Add `run_repair()` function
**Files:**
- Modify: `pipeline/worker/scripts/literature_pipeline.py` — add new `run_repair()` function
- Test: `tests/test_repair.py` — new test file
**New function `run_repair(vault: Path, paths: dict, verbose: bool = False) -> dict`:**
Scans all domains for three-way state divergence:
1. Read `library_record.md` frontmatter `ocr_status`
2. Read `formal_note.md` frontmatter `ocr_status`
3. Read `meta.json` `ocr_status` after `validate_ocr_meta()`
4. Report contradictions
Returns dict:
```python
{
"scanned": int,
"divergent": list[dict], # items with contradictions
"fixed": int,
"errors": list[dict],
}
```
Detection rules:
- `done_incomplete` from `validate_ocr_meta()` → treat as blocked (needs re-OCR)
- If `library_record.ocr_status` = `done` but `meta.ocr_status` = `pending/processing` → divergence
- If `formal_note.ocr_status` = `done` but `meta.json` missing or invalid → divergence
- If `library_record.ocr_status` != `meta.ocr_status` (post-validation) → divergence
Repair actions (controlled by `fix: bool` parameter):
- If meta files missing: set all three to `pending`, set `do_ocr: true`
- If meta incomplete: set all three to `pending`, set `do_ocr: true`
- If library_record says done but meta says pending: set library_record to `pending`
- [ ] **Step 1: Write failing test for `run_repair()`**
- [ ] **Step 2: Run test to verify it fails**
- [ ] **Step 3: Implement `run_repair()` function**
- [ ] **Step 4: Run tests to verify they pass**
- [ ] **Step 5: Commit**
---
### Task 4: Add `repair` subcommand to CLI
**Files:**
- Modify: `paperforge_lite/cli.py` — add `repair` to dispatch
**New command:**
```
paperforge repair [--verbose] [--fix]
```
- `--verbose`: Show detailed divergence report
- `--fix`: Actually apply repairs (default is dry-run)
- [ ] **Step 1: Add `repair` to CLI dispatch**
- [ ] **Step 2: Test CLI dispatch**
- [ ] **Step 3: Commit**
---
### Task 5: Update AGENTS.md
**Files:**
- Modify: `AGENTS.md` — document `paperforge repair` command
- [ ] **Step 1: Add `paperforge repair` to command reference**
- [ ] **Step 2: Commit**
---
## Test Plan
| Test | File | What it covers |
|------|------|---------------|
| `test_bbt_path_normalization` | new in `tests/test_pdf_resolver.py` | BBT bare `KEY/KEY.pdf``storage:KEY/KEY.pdf` |
| `test_deep_reading_done_incomplete_blocked` | new in `tests/test_smoke.py` | `done_incomplete` misclassified as ready |
| `test_repair_detects_divergence` | new `tests/test_repair.py` | three-way divergence detection |
| `test_repair_dry_run_vs_fix` | new `tests/test_repair.py` | dry-run vs actual fix |
| `test_repair_no_divergence` | new `tests/test_repair.py` | clean state → no divergence reported |
---
## Verification
After all tasks:
1. Run `pytest tests/test_pdf_resolver.py tests/test_smoke.py -v` — all PASS
2. Run `python -m paperforge_lite repair --verbose` — confirm no crashes
3. Confirm `paperforge repair` appears in `paperforge --help`

View file

@ -363,6 +363,12 @@ paperforge status
paperforge doctor
```
> 如果 `paperforge` 命令未注册,可使用 fallback
> ```bash
> python -m paperforge_lite <command>
> ```
> 例如:`python -m paperforge_lite status`
### Agent 命令
```
/LD-deep <zotero_key> # 完整三阶段精读

View file

@ -167,7 +167,7 @@
当不提供具体 key/标题时agent 自动执行以下流程:
1. 运行 `paperforge deep-reading` 查看精读队列(或 `python $(paperforge paths --json | python -c "import json,sys; print(json.load(sys.stdin)['literature_script'])") queue --vault {{VAULT}}` 获取 JSON 格式队列)
1. 运行 `paperforge deep-reading` 查看精读队列(或 `python $(paperforge paths --json | python -c "import json,sys; print(json.load(sys.stdin)['ld_deep_script'])") queue --vault {{VAULT}}` 获取 JSON 格式队列)
2. 解析输出的队列状态(`analyze=true` + `deep_reading_status != done` + `ocr_status`
3. 按 OCR 状态分组展示:
- **就绪**OCR 已完成,可直接精读
@ -191,14 +191,14 @@
| `{{ZOTERO_KEY}}` | `Y5KQ4JQ7` | 从 library-record 或 JSON 导出中获取 |
| `{{FORMAL_NOTE}}` | `<Vault>/<resources_dir>/<literature_dir>/骨科/Y5KQ4JQ7 - title.md` | 从 `paperforge paths --json` 或 library-record 中获取 |
| `{{FULLTEXT_MD}}` | `<Vault>/<system_dir>/PaperForge/ocr/Y5KQ4JQ7/fulltext.md` | 由 OCR worker 生成在 ocr 目录下 |
| `{{SCRIPT}}` | `<Vault>/<skill_dir>/literature-qa/scripts/ld_deep.py` | 从 `paperforge paths --json` 获取 `literature_script` 字段 |
| `{{SCRIPT}}` | `<Vault>/<skill_dir>/literature-qa/scripts/ld_deep.py` | 从 `paperforge paths --json` 获取 `ld_deep_script` 字段 |
### Spawn 命令格式
获取路径信息:
```bash
paperforge paths --json
# 返回 JSON包含 worker_script, literature_script, skill_dir 等字段
# 返回 JSON包含 worker_script, ld_deep_script, skill_dir 等字段
```
然后使用以下格式启动 subagent

View file

@ -111,6 +111,12 @@ python -m pip install -e .
python $(python -c "import json; print(json.load(open('paperforge.json'))['paperforge_path'] + '/worker/scripts/literature_pipeline.py')" --vault . status
```
**如果 `paperforge` 命令未注册**,使用 fallback
```bash
python -m paperforge_lite <command>
```
例如:`python -m paperforge_lite status`
---
## 故障排除

View file

@ -62,6 +62,13 @@ def ocr_doctor(config: dict[str, str] | None, live: bool = False) -> dict:
"fix": "OCR provider is experiencing issues. Retry later with `paperforge ocr doctor`",
}
if resp.status_code != 200:
if resp.status_code == 405:
return {
"level": 2,
"passed": False,
"error": "URL returned 405 Method Not Allowed",
"fix": "PaddleOCR endpoint may require GET for probing but POST for OCR jobs. Check if your API endpoint supports both methods.",
}
return {
"level": 2,
"passed": False,

View file

@ -2844,6 +2844,137 @@ def run_deep_reading(vault: Path, verbose: bool = False) -> int:
print(f'deep-reading: synced {synced} records, {len(pending_queue)} pending')
return 0
def run_repair(vault: Path, paths: dict, verbose: bool = False, fix: bool = False) -> dict:
"""Scan all domains for three-way state divergence and optionally repair.
Compares three sources of ocr_status:
1. library_record.md frontmatter ocr_status
2. formal_note.md frontmatter ocr_status
3. meta.json ocr_status (post-validate_ocr_meta())
Returns:
dict with scanned, divergent, fixed, errors counts
"""
result = {"scanned": 0, "divergent": [], "fixed": 0, "errors": []}
config = load_domain_config(paths)
domain_lookup = {entry['export_file']: entry['domain'] for entry in config['domains']}
record_paths = list(paths['library_records'].rglob('*.md'))
for record_path in record_paths:
try:
record_text = record_path.read_text(encoding='utf-8')
except Exception as e:
result['errors'].append({"file": str(record_path), "error": str(e)})
continue
key_match = re.search('^zotero_key:\\s*"?(.+?)"?\\s*$', record_text, re.MULTILINE)
if not key_match:
continue
zotero_key = key_match.group(1).strip()
domain = record_path.parent.name
record_dir = record_path.parent
result['scanned'] += 1
lib_ocr_match = re.search('^ocr_status:\\s*"?(.+?)"?\\s*$', record_text, re.MULTILINE)
lib_ocr_status = lib_ocr_match.group(1).strip() if lib_ocr_match else 'pending'
note_path = _resolve_formal_note_path(vault, zotero_key, domain)
note_ocr_status = None
if note_path and note_path.exists():
try:
note_text = note_path.read_text(encoding='utf-8')
note_status_match = re.search('^ocr_status:\\s*"?(.+?)"?\\s*$', note_text, re.MULTILINE)
note_ocr_status = note_status_match.group(1).strip() if note_status_match else None
except Exception:
pass
meta_path = paths['ocr'] / zotero_key / 'meta.json'
meta_ocr_status = None
meta_validated_status = None
if meta_path.exists():
try:
meta = read_json(meta_path)
validated_status, validated_error = validate_ocr_meta(paths, meta)
meta_validated_status = validated_status
if validated_error and verbose:
print(f"[repair] {zotero_key} meta validation error: {validated_error}")
raw_status = str(meta.get('ocr_status', '') or '').strip().lower()
meta_ocr_status = raw_status if raw_status else None
if meta_validated_status == 'done_incomplete':
meta_ocr_status = 'done_incomplete'
except Exception as e:
result['errors'].append({"file": str(meta_path), "error": str(e)})
meta_ocr_status = None
is_divergent = False
div_reason = ""
if meta_validated_status == 'done_incomplete':
is_divergent = True
div_reason = f"meta validation: done_incomplete ({validated_error})"
elif lib_ocr_status == 'done' and meta_ocr_status in ('pending', 'processing', None):
is_divergent = True
div_reason = f"library_record done but meta {meta_ocr_status or 'missing'}"
elif note_ocr_status == 'done' and (meta_ocr_status is None or meta_validated_status == 'done_incomplete'):
is_divergent = True
div_reason = "formal_note done but meta.json missing/invalid"
elif lib_ocr_status != 'pending' and meta_ocr_status is not None and meta_validated_status is not None and lib_ocr_status != meta_validated_status:
is_divergent = True
div_reason = f"library_record={lib_ocr_status} vs meta post-validation={meta_validated_status}"
if is_divergent:
item = {
"zotero_key": zotero_key,
"domain": domain,
"library_record_ocr_status": lib_ocr_status,
"formal_note_ocr_status": note_ocr_status,
"meta_ocr_status": meta_validated_status or meta_ocr_status,
"reason": div_reason,
}
result['divergent'].append(item)
if verbose:
print(f"[repair] divergent: {zotero_key} | {div_reason}")
if fix:
fixed_library_record = False
fixed_formal_note = False
fixed_meta = False
new_status = 'pending'
if meta_ocr_status is None or meta_validated_status == 'done_incomplete':
new_status = 'pending'
new_record_text = update_frontmatter_field(record_text, 'ocr_status', new_status)
if new_record_text != record_text:
record_path.write_text(new_record_text, encoding='utf-8')
fixed_library_record = True
if note_path and note_path.exists():
try:
note_text = note_path.read_text(encoding='utf-8')
new_note_text = update_frontmatter_field(note_text, 'ocr_status', new_status)
if new_note_text != note_text:
note_path.write_text(new_note_text, encoding='utf-8')
fixed_formal_note = True
except Exception:
pass
if meta_validated_status == 'done_incomplete':
if meta_path.exists():
try:
meta = read_json(meta_path)
meta['ocr_status'] = 'pending'
write_json(meta_path, meta)
fixed_meta = True
except Exception:
pass
record_do_ocr_match = re.search(r'^do_ocr:\s*(true|false)$', new_record_text, re.MULTILINE)
is_do_ocr = record_do_ocr_match and record_do_ocr_match.group(1) == 'true'
if not is_do_ocr:
final_record_text = update_frontmatter_field(new_record_text, 'do_ocr', 'true')
if final_record_text != new_record_text:
record_path.write_text(final_record_text, encoding='utf-8')
fixed_library_record = True
elif lib_ocr_status == 'done' and meta_ocr_status in ('pending', 'processing'):
new_record_text = update_frontmatter_field(record_text, 'ocr_status', new_status)
if new_record_text != record_text:
record_path.write_text(new_record_text, encoding='utf-8')
fixed_library_record = True
fixed_count = sum([fixed_library_record, fixed_formal_note, fixed_meta])
result['fixed'] += fixed_count
if verbose and fixed_count > 0:
print(f"[repair] fixed {fixed_count} files for {zotero_key}")
if verbose:
print(f"[repair] scanned={result['scanned']} divergent={len(result['divergent'])} fixed={result['fixed']} errors={len(result['errors'])}")
return result
def run_doctor(vault: Path) -> int:
"""Validate PaperForge Lite setup and report by category.

View file

@ -486,6 +486,14 @@ PaperForge 需要 **Python 3.8+** 以及以下 Python 包:
class VaultStep(StepScreen):
"""Step 3: Vault 目录结构配置"""
def __init__(self, step_id: str, checker: EnvChecker, vault: str = "", **kwargs):
kwargs.setdefault("id", step_id)
super().__init__(**kwargs)
self.step_id = step_id
self.checker = checker
self.step_idx = int(step_id.split("-")[1])
self._vault = vault
def compose(self) -> ComposeResult:
yield from super().compose()
yield Markdown("""
@ -495,7 +503,7 @@ PaperForge 需要知道你的 **Obsidian Vault 位置**,以及你想要的目
""")
from textual.widgets import Input
yield Static("Obsidian Vault 路径 (绝对路径):", classes="step-title")
yield Input(placeholder="D:\\Documents\\MyVault", id="input-vault-path")
yield Input(value=self._vault, placeholder="D:\\Documents\\MyVault", id="input-vault-path")
yield Static("", id="vault-error", classes="status-bar")
yield Markdown("""
---
@ -1329,7 +1337,7 @@ class SetupWizardApp(App):
WelcomeStep("step-0", self.checker),
AgentPlatformStep("step-1", self.checker),
PythonStep("step-2", self.checker),
VaultStep("step-3", self.checker),
VaultStep("step-3", self.checker, vault=str(self.vault)),
ZoteroStep("step-4", self.checker),
BBTStep("step-5", self.checker),
JsonStep("step-6", self.checker),

View file

@ -1,55 +0,0 @@
# PaperForge Lite — Test Sandbox
## 用途
测试 PaperForge Lite 安装向导 `setup_wizard.py` 的完整流程。
## 目录结构
```
tests/sandbox/
TestZoteroData/ ← 模拟 Zotero 数据目录setup wizard 会在 vault 内建 junction 指向这里)
storage/ ← 5 PDFs4篇有附件1篇无
zotero.sqlite ← 伪造(让 Zotero 路径检测通过)
exports/ ← Better BibTeX JSON 导出2个域5篇文献keys 匹配 storage 文件名)
00_TestVault/ ← 空目录(安装向导会在这里创建所有子目录)
README.md ← 本文件
```
## 测试步骤
```powershell
# 1. 进入仓库根目录
cd D:\...\github-release
# 2. 运行安装向导,指向空 vaultpip install -e . 由向导自动完成)
python setup_wizard.py --vault D:\L\Med\Research\99_System\LiteraturePipeline\github-release\tests\sandbox\00_TestVault
# 3. 安装向导中:
# - Agent 平台选你的opencode / cursor / claude 等)
# - Zotero 数据目录:填 D:\L\Med\Research\99_System\LiteraturePipeline\github-release\tests\sandbox\TestZoteroData
# (向导会在 vault 内创建 junction: 00_TestVault/00_System/Zotero -> 指向这里)
# - BBT 导出目录:填 D:\L\Med\Research\99_System\LiteraturePipeline\github-release\tests\sandbox\exports
# (向导会检测到 exports/ 下的 JSON 文件)
# - 其他步骤默认即可
# 4. 测试 pipeline
cd D:\L\Med\Research\99_System\LiteraturePipeline\github-release\tests\sandbox\00_TestVault
paperforge selection-sync
paperforge index-refresh
paperforge status
```
## 预期结果
| 检查项 | 预期 |
|--------|------|
| wizard 检测 TestZoteroData | 通过(有 storage/ 和 zotero.sqlite|
| wizard 检测 exports/ | 通过2个 JSONkeys 有效)|
| selection-sync | 生成 5 条 library-records |
| TSTONE003 ocr_status | nopdf无 PDF |
| TSTTWO001/002 有 PDF | ocr_status: pending |
## 注意
- 目录名故意和真实 vault 不同,避免硬编码测试不出来
- PDF 是最小化假文件pymupdf 可读,内容为空)
- 不要往 sandbox 加真实数据

View file

@ -7,6 +7,7 @@ and has_pdf=False scenarios.
from __future__ import annotations
import os
import json
from pathlib import Path
from unittest.mock import patch
@ -141,3 +142,143 @@ class TestResolveJunction:
def test_directory_junction_mocked(self, tmp_path: Path) -> None:
"""Directory junction resolution is platform-dependent; covered by symlink test above."""
pytest.skip("Windows junction mock incompatible with Python import semantics for `from ctypes import wintypes`")
class TestLoadExportRowsAttachmentNormalization:
"""Tests for load_export_rows() attachment path normalization.
Verifies that BBT-exported bare KEY/KEY.pdf paths are normalized to
storage:KEY/KEY.pdf format so resolve_pdf_path() can resolve them correctly.
"""
def test_bare_key_key_pdf_normalized_to_storage_prefix(self, tmp_path: Path) -> None:
"""Bare 'KEY/KEY.pdf' path is normalized to 'storage:KEY/KEY.pdf'."""
from pipeline.worker.scripts.literature_pipeline import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [
{"path": "ABC123/ABC123.pdf", "contentType": "application/pdf"}
],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.pdf"
def test_storage_prefix_preserved(self, tmp_path: Path) -> None:
"""Already-prefixed 'storage:KEY/KEY.pdf' path is not double-prefixed."""
from pipeline.worker.scripts.literature_pipeline import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [
{"path": "storage:ABC123/ABC123.pdf", "contentType": "application/pdf"}
],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.pdf"
def test_absolute_path_not_modified(self, tmp_path: Path) -> None:
"""Absolute paths are not prefixed with storage:."""
from pipeline.worker.scripts.literature_pipeline import load_export_rows
abs_path = str(tmp_path / "ABC123" / "ABC123.pdf")
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [
{"path": abs_path, "contentType": "application/pdf"}
],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == abs_path
def test_empty_attachment_path_unchanged(self, tmp_path: Path) -> None:
"""Empty attachment path is returned unchanged."""
from pipeline.worker.scripts.literature_pipeline import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [
{"path": "", "contentType": ""}
],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == ""
def test_non_pdf_attachment_unchanged(self, tmp_path: Path) -> None:
"""Non-PDF attachments are returned with empty contentType and unchanged path."""
from pipeline.worker.scripts.literature_pipeline import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [
{"path": "ABC123/ABC123.docx", "contentType": ""}
],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.docx"
assert rows[0]["attachments"][0]["contentType"] == ""

348
tests/test_repair.py Normal file
View file

@ -0,0 +1,348 @@
"""Tests for run_repair() — three-way OCR status divergence detection and repair."""
from __future__ import annotations
import json
import re
import pytest
from pipeline.worker.scripts.literature_pipeline import run_repair, pipeline_paths
def _make_vault(tmp_path):
vault = tmp_path / "vault"
vault.mkdir()
system = vault / "99_System"
pf = system / "PaperForge"
(pf / "exports").mkdir(parents=True)
(pf / "ocr").mkdir(parents=True)
resources = vault / "03_Resources"
literature = resources / "Literature"
literature.mkdir(parents=True)
control = resources / "LiteratureControl"
control.mkdir(parents=True)
records = control / "library-records"
records.mkdir(parents=True)
(vault / "05_Bases").mkdir(parents=True)
(vault / ".opencode" / "skills").mkdir(parents=True)
(vault / ".opencode" / "command").mkdir(parents=True)
cfg = {
"system_dir": "99_System",
"resources_dir": "03_Resources",
"literature_dir": "Literature",
"control_dir": "LiteratureControl",
"base_dir": "05_Bases",
}
(vault / "paperforge.json").write_text(json.dumps(cfg, ensure_ascii=False), encoding="utf-8")
return vault
def _write_library_record(records_dir, key, domain, ocr_status, do_ocr="false", analyze="false"):
record_path = records_dir / f"{key}.md"
content = f"""---
zotero_key: {key}
domain: {domain}
title: "Test Paper {key}"
year: 2024
doi: "10.1234/test"
has_pdf: true
pdf_path: ""
fulltext_md_path: ""
recommend_analyze: true
analyze: {analyze}
do_ocr: {do_ocr}
ocr_status: {ocr_status}
deep_reading_status: pending
analysis_note: ""
---
# Test Paper {key}
"""
record_path.write_text(content, encoding="utf-8")
return record_path
def _write_formal_note(literature_dir, key, domain, ocr_status):
domain_lit = literature_dir / domain
domain_lit.mkdir(parents=True, exist_ok=True)
note_path = domain_lit / f"{key} - Test.md"
content = f"""---
title: "Test Paper {key}"
year: 2024
domain: "{domain}"
zotero_key: "{key}"
doi: "10.1234/test"
ocr_status: {ocr_status}
deep_reading_status: pending
---
# Test Paper {key}
"""
note_path.write_text(content, encoding="utf-8")
return note_path
def _write_meta(ocr_root, key, ocr_status, zotero_key=None, page_count=10, with_fulltext=True, with_json=True):
meta_dir = ocr_root / key
meta_dir.mkdir(parents=True, exist_ok=True)
meta = {
"zotero_key": zotero_key or key,
"ocr_status": ocr_status,
"page_count": page_count,
}
if with_fulltext:
ft = meta_dir / "fulltext.md"
page_lines = []
for i in range(1, page_count + 1):
page_lines.append(f"<!-- page {i} -->")
page_lines.append("x" * 100)
ft.write_text("\n".join(page_lines), encoding="utf-8")
meta["markdown_path"] = f"99_System/PaperForge/ocr/{key}/fulltext.md"
meta["fulltext_md_path"] = f"99_System/PaperForge/ocr/{key}/fulltext.md"
if with_json:
json_dir = meta_dir / "json"
json_dir.mkdir(parents=True, exist_ok=True)
result = json_dir / "result.json"
pages_data = [{"text": "x" * 200} for _ in range(page_count)]
result.write_text(json.dumps({"pages": pages_data}, ensure_ascii=False), encoding="utf-8")
meta["json_path"] = f"99_System/PaperForge/ocr/{key}/json/result.json"
(meta_dir / "meta.json").write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
return meta_dir / "meta.json"
def _write_minimal_meta(ocr_root, key, ocr_status):
meta_dir = ocr_root / key
meta_dir.mkdir(parents=True, exist_ok=True)
meta = {"zotero_key": key, "ocr_status": ocr_status}
(meta_dir / "meta.json").write_text(json.dumps(meta, ensure_ascii=False), encoding="utf-8")
return meta_dir / "meta.json"
class TestRunRepairScanOnly:
def test_no_records_returns_empty(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 0
assert result["divergent"] == []
assert result["fixed"] == 0
assert result["errors"] == []
def test_all_consistent_pending_no_divergence(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "pending")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert result["divergent"] == []
def test_meta_done_incomplete_is_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "pending")
_write_minimal_meta(paths["ocr"], "KEY001", "done")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert len(result["divergent"]) == 1
assert result["divergent"][0]["zotero_key"] == "KEY001"
assert "done_incomplete" in result["divergent"][0]["reason"]
def test_library_done_but_meta_pending_is_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "done")
_write_meta(paths["ocr"], "KEY001", "pending")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert len(result["divergent"]) == 1
assert result["divergent"][0]["library_record_ocr_status"] == "done"
assert result["divergent"][0]["meta_ocr_status"] == "pending"
def test_library_done_but_meta_missing_is_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "done")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert len(result["divergent"]) == 1
def test_formal_note_done_but_meta_missing_is_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
literature_dir = vault / "03_Resources" / "Literature"
_write_library_record(records_dir, "KEY001", "骨科", "pending")
_write_formal_note(literature_dir, "KEY001", "骨科", "done")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert len(result["divergent"]) == 1
def test_library_vs_meta_post_validation_mismatch_is_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "done")
_write_meta(paths["ocr"], "KEY001", "done", page_count=10)
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert result["divergent"] == []
def test_library_nopdf_not_divergent(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "nopdf")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 1
assert result["divergent"] == []
def test_multiple_domains_scanned(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir_ortho = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir_ortho.mkdir(parents=True, exist_ok=True)
records_dir_sports = vault / "03_Resources" / "LiteratureControl" / "library-records" / "运动医学"
records_dir_sports.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir_ortho, "KEY001", "骨科", "pending")
_write_library_record(records_dir_sports, "KEY002", "运动医学", "pending")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["scanned"] == 2
def test_verbose_output_printed(self, tmp_path, capsys):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "done")
_write_meta(paths["ocr"], "KEY001", "pending")
result = run_repair(vault, paths, verbose=True, fix=False)
captured = capsys.readouterr()
assert "KEY001" in captured.out
assert "divergent" in captured.out
class TestRunRepairFixMode:
def test_fix_meta_missing_sets_all_to_pending_and_sets_do_ocr(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
lit_dir = vault / "03_Resources" / "Literature"
record_path = _write_library_record(records_dir, "KEY001", "骨科", "done", do_ocr="false")
_write_formal_note(lit_dir, "KEY001", "骨科", "done")
result = run_repair(vault, paths, verbose=False, fix=True)
assert result["fixed"] >= 1
record_text = record_path.read_text(encoding="utf-8")
assert re.search(r'^ocr_status:\s*"?pending"?', record_text, re.MULTILINE)
assert re.search(r'^do_ocr:\s*"?true"?', record_text, re.MULTILINE)
def test_fix_done_incomplete_meta_sets_to_pending(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
record_path = _write_library_record(records_dir, "KEY001", "骨科", "pending")
_write_minimal_meta(paths["ocr"], "KEY001", "done")
result = run_repair(vault, paths, verbose=False, fix=True)
assert result["fixed"] >= 1
record_text = record_path.read_text(encoding="utf-8")
assert re.search(r'^ocr_status:\s*"?pending"?', record_text, re.MULTILINE)
def test_fix_library_done_meta_pending_sets_library_to_pending(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
record_path = _write_library_record(records_dir, "KEY001", "骨科", "done")
_write_meta(paths["ocr"], "KEY001", "pending")
result = run_repair(vault, paths, verbose=False, fix=True)
assert result["fixed"] >= 1
record_text = record_path.read_text(encoding="utf-8")
assert re.search(r'^ocr_status:\s*"?pending"?', record_text, re.MULTILINE)
def test_fix_false_does_not_write_anything(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
record_path = _write_library_record(records_dir, "KEY001", "骨科", "done")
_write_minimal_meta(paths["ocr"], "KEY001", "done")
original_content = record_path.read_text(encoding="utf-8")
result = run_repair(vault, paths, verbose=False, fix=False)
assert result["fixed"] == 0
assert record_path.read_text(encoding="utf-8") == original_content
def test_fix_multiple_divergent_items(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
record1_path = _write_library_record(records_dir, "KEY001", "骨科", "done", do_ocr="false")
record2_path = _write_library_record(records_dir, "KEY002", "骨科", "pending")
_write_minimal_meta(paths["ocr"], "KEY001", "done")
_write_minimal_meta(paths["ocr"], "KEY002", "done")
result = run_repair(vault, paths, verbose=False, fix=True)
assert len(result["divergent"]) == 2
assert result["fixed"] >= 1
def test_no_divergence_fix_reports_zero_fixed(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "pending")
_write_meta(paths["ocr"], "KEY001", "pending")
result = run_repair(vault, paths, verbose=False, fix=True)
assert result["fixed"] == 0
def test_fix_writes_formal_note_ocr_status(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
lit_dir = vault / "03_Resources" / "Literature"
_write_library_record(records_dir, "KEY001", "骨科", "done", do_ocr="false")
note_path = _write_formal_note(lit_dir, "KEY001", "骨科", "done")
result = run_repair(vault, paths, verbose=False, fix=True)
note_text = note_path.read_text(encoding="utf-8")
assert re.search(r'^ocr_status:\s*"?pending"?', note_text, re.MULTILINE)
class TestRunRepairReturnStructure:
def test_result_has_all_keys(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
result = run_repair(vault, paths, verbose=False, fix=False)
assert "scanned" in result
assert "divergent" in result
assert "fixed" in result
assert "errors" in result
assert isinstance(result["scanned"], int)
assert isinstance(result["divergent"], list)
assert isinstance(result["fixed"], int)
assert isinstance(result["errors"], list)
def test_divergent_item_has_required_fields(self, tmp_path):
vault = _make_vault(tmp_path)
paths = pipeline_paths(vault)
records_dir = vault / "03_Resources" / "LiteratureControl" / "library-records" / "骨科"
records_dir.mkdir(parents=True, exist_ok=True)
_write_library_record(records_dir, "KEY001", "骨科", "done")
_write_meta(paths["ocr"], "KEY001", "pending")
result = run_repair(vault, paths, verbose=False, fix=False)
item = result["divergent"][0]
assert "zotero_key" in item
assert "domain" in item
assert "library_record_ocr_status" in item
assert "formal_note_ocr_status" in item
assert "meta_ocr_status" in item
assert "reason" in item