feat(phase-10): add consistency audit script

- Add scripts/consistency_audit.py with 4 automated checks
- Check 1: No old command names in active code/docs
- Check 2: No paperforge_lite references in Python code
- Check 3: No dead internal links in markdown
- Check 4: Command docs have required sections

Fix violations found by audit:
- Update paperforge/ocr_diagnostics.py error messages to use new commands
- Update pipeline/worker/literature_pipeline.py generated content to use /pf-deep
- Update scripts/welcome.py to recommend /pf-deep
- Update skills/literature-qa/prompt_deep_subagent.md title to /pf-deep
- Update skills/literature-qa/scripts/ld_deep.py docstrings to /pf-deep
- Fix dead links in docs/COMMANDS.md and command/*.md
- Update AGENTS.md migration section to reference MIGRATION-v1.2.md
- Update paperforge/cli.py module docstring to use current commands
This commit is contained in:
Research Assistant 2026-04-24 18:06:35 +08:00
parent 747aabec18
commit abab8df3f5
14 changed files with 318 additions and 39 deletions

View file

@ -424,19 +424,14 @@ cp -r 新下载的scripts/* <system_dir>/PaperForge/worker/scripts/
从 v1.2 开始PaperForge 采用统一的命令接口:
| 旧命令v1.1 | 新命令v1.2 | 说明 |
|---------------|---------------|------|
| `paperforge selection-sync` | `paperforge sync` | 合并为统一 sync 命令 |
| `paperforge index-refresh` | `paperforge sync` | 合并为统一 sync 命令 |
| `paperforge sync --selection` | `paperforge sync --selection` | 仅执行 selection-sync |
| `paperforge sync --index` | `paperforge sync --index` | 仅执行 index-refresh |
| `paperforge ocr run` | `paperforge ocr` | 简化 OCR 命令 |
| `paperforge ocr doctor` | `paperforge ocr --diagnose` | 诊断模式 |
| `/LD-deep <key>` | `/pf-deep <key>` | Agent 精读命令 |
| `/LD-paper <key>` | `/pf-paper <key>` | Agent 摘要命令 |
- **CLI 统一入口**`paperforge sync`(替代 `selection-sync` + `index-refresh`)、`paperforge ocr`(替代 `ocr run`
- **Agent 统一前缀**`/pf-deep`、`/pf-paper`、`/pf-ocr`、`/pf-sync`、`/pf-status`(替代 `/LD-*``/lp-*`
- **Python 包重命名**`paperforge`(替代 `paperforge_lite`
**旧命令仍兼容**v1.2 继续支持旧命令名(`selection-sync`、`index-refresh`、`ocr run`),但文档已统一使用新命令。
详细迁移步骤和回滚说明参见 [docs/MIGRATION-v1.2.md](docs/MIGRATION-v1.2.md)。
---
*PaperForge Lite | 快速开始指南 | 安装后阅读*

View file

@ -296,4 +296,4 @@ Task(
- [pf-paper](pf-paper.md) — 快速摘要与问答
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
- [docs/COMMANDS.md](COMMANDS.md) — 命令总览与矩阵
- [docs/COMMANDS.md](../docs/COMMANDS.md) — 命令总览与矩阵

View file

@ -139,4 +139,4 @@ OCR 完成后,每个文献生成以下文件:
- [pf-sync](pf-sync.md) — 文献同步(生成 library-records
- [pf-deep](pf-deep.md) — 深度精读(依赖 OCR 结果)
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
- [docs/COMMANDS.md](COMMANDS.md) — 命令总览与矩阵
- [docs/COMMANDS.md](../docs/COMMANDS.md) — 命令总览与矩阵

View file

@ -116,4 +116,4 @@ Zotero Key: [key]
- [pf-deep](pf-deep.md) — 完整三阶段精读
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
- [docs/COMMANDS.md](COMMANDS.md) — 命令总览与矩阵
- [docs/COMMANDS.md](../docs/COMMANDS.md) — 命令总览与矩阵

View file

@ -118,4 +118,4 @@ PaperForge Lite v1.2
- [pf-sync](pf-sync.md) — 文献同步
- [pf-ocr](pf-ocr.md) — OCR 提取
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
- [docs/COMMANDS.md](COMMANDS.md) — 命令总览与矩阵
- [docs/COMMANDS.md](../docs/COMMANDS.md) — 命令总览与矩阵

View file

@ -154,4 +154,4 @@ paperforge sync --vault /path/to/vault
- [pf-ocr](pf-ocr.md) — OCR 提取(下一步操作)
- [pf-status](pf-status.md) — 检查系统状态
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
- [docs/COMMANDS.md](COMMANDS.md) — 命令总览与矩阵
- [docs/COMMANDS.md](../docs/COMMANDS.md) — 命令总览与矩阵

View file

@ -81,11 +81,11 @@ Agent 自动:
各命令的详细文档见对应文件:
- [`command/pf-deep.md`](pf-deep.md) — 深度精读
- [`command/pf-paper.md`](pf-paper.md) — 快速摘要
- [`command/pf-ocr.md`](pf-ocr.md) — OCR 提取
- [`command/pf-sync.md`](pf-sync.md) — 文献同步
- [`command/pf-status.md`](pf-status.md) — 状态检查
- [`command/pf-deep.md`](../command/pf-deep.md) — 深度精读
- [`command/pf-paper.md`](../command/pf-paper.md) — 快速摘要
- [`command/pf-ocr.md`](../command/pf-ocr.md) — OCR 提取
- [`command/pf-sync.md`](../command/pf-sync.md) — 文献同步
- [`command/pf-status.md`](../command/pf-status.md) — 状态检查
---

View file

@ -1,8 +1,11 @@
"""paperforge.cli — PaperForge Lite command-line interface.
Exposes `paperforge paths`, `paperforge status`, `paperforge sync`,
`paperforge selection-sync`, `paperforge index-refresh`, `paperforge ocr`,
`paperforge ocr run`, `paperforge ocr doctor`, and `paperforge deep-reading`.
`paperforge ocr`, `paperforge ocr --diagnose`, `paperforge deep-reading`,
`paperforge repair`, and `paperforge doctor`.
Backward-compatible aliases (deprecated): `selection-sync`, `index-refresh`,
`ocr run`, `ocr doctor`.
Loads .env from the vault root and from <system_dir>/PaperForge/.env before
dispatching to worker functions, matching the legacy pipeline behavior.

View file

@ -33,7 +33,7 @@ def ocr_doctor(config: dict[str, str] | None, live: bool = False) -> dict:
"level": 1,
"passed": False,
"error": "PADDLEOCR_API_TOKEN not found in environment",
"fix": "Set PADDLEOCR_API_TOKEN in .env or environment variables and re-run `paperforge ocr doctor`",
"fix": "Set PADDLEOCR_API_TOKEN in .env or environment variables and re-run `paperforge ocr --diagnose`",
}
# L2 — URL reachability
@ -52,14 +52,14 @@ def ocr_doctor(config: dict[str, str] | None, live: bool = False) -> dict:
"level": 2,
"passed": False,
"error": "URL returned 401 Unauthorized",
"fix": "PaddleOCR API token is invalid. Check PADDLEOCR_API_TOKEN and re-run `paperforge ocr doctor`",
"fix": "PaddleOCR API token is invalid. Check PADDLEOCR_API_TOKEN and re-run `paperforge ocr --diagnose`",
}
if resp.status_code >= 500:
return {
"level": 2,
"passed": False,
"error": f"URL returned {resp.status_code}",
"fix": "OCR provider is experiencing issues. Retry later with `paperforge ocr doctor`",
"fix": "OCR provider is experiencing issues. Retry later with `paperforge ocr --diagnose`",
}
if resp.status_code != 200:
if resp.status_code == 405:
@ -73,7 +73,7 @@ def ocr_doctor(config: dict[str, str] | None, live: bool = False) -> dict:
"level": 2,
"passed": False,
"error": f"URL returned {resp.status_code}",
"fix": "Check PADDLEOCR_JOB_URL in .env and re-run `paperforge ocr doctor`",
"fix": "Check PADDLEOCR_JOB_URL in .env and re-run `paperforge ocr --diagnose`",
}
except requests.RequestException as e:
return {
@ -120,7 +120,7 @@ def ocr_doctor(config: dict[str, str] | None, live: bool = False) -> dict:
"level": 3,
"passed": False,
"error": f"API submission test failed: {e}",
"fix": "Check PADDLEOCR configuration and network. Run `paperforge ocr doctor` again after fixes.",
"fix": "Check PADDLEOCR configuration and network. Run `paperforge ocr --diagnose` again after fixes.",
}
except (json.JSONDecodeError, KeyError) as e:
return {
@ -233,7 +233,7 @@ def classify_error(exception: Exception, response) -> tuple[str, str]:
)
return (
"error",
f"OCR HTTP error {status}. Retry with `paperforge ocr` or run `paperforge ocr doctor`.",
f"OCR HTTP error {status}. Retry with `paperforge ocr` or run `paperforge ocr --diagnose`.",
)
if isinstance(exception, json.JSONDecodeError):
return (
@ -252,5 +252,5 @@ def classify_error(exception: Exception, response) -> tuple[str, str]:
)
return (
"error",
f"Unexpected error: {exception}. Retry with `paperforge ocr` or run `paperforge ocr doctor`.",
f"Unexpected error: {exception}. Retry with `paperforge ocr` or run `paperforge ocr --diagnose`.",
)

View file

@ -1467,7 +1467,7 @@ def frontmatter_note(entry: dict, existing_text: str='') -> str:
lines.extend(yaml_list('collections', entry.get('collections', [])))
lines.extend(yaml_list('collection_tags', entry.get('collection_tags', [])))
lines.extend(yaml_block(entry.get('abstract', '')))
lines.extend([f"has_pdf: {('true' if entry.get('has_pdf') else 'false')}", f"ocr_status: {yaml_quote(entry.get('ocr_status', 'pending'))}", f"ocr_job_id: {yaml_quote(entry.get('ocr_job_id', ''))}", f"ocr_md_path: {yaml_quote(entry.get('ocr_md_path', ''))}", f"ocr_json_path: {yaml_quote(entry.get('ocr_json_path', ''))}", f"deep_reading_status: {yaml_quote(entry.get('deep_reading_status', 'pending'))}", f'deep_reading_md_path: {yaml_quote(deep_reading_path)}', f"pdf_path: {yaml_quote(entry.get('pdf_path', ''))}", 'tags:', ' - 文献阅读', f" - {entry.get('domain', '')}", '---', '', f"# {entry['title']}", '', '## 📄 文献基本信息', '', f"- Zotero Key: `{entry.get('zotero_key', '')}`", f"- Collection: `{entry.get('collection_path', '')}`", f"- 作者:{', '.join(entry.get('authors', []))}", f"- PDF: {('已检测' if entry.get('has_pdf') else '未检测到')}", f"- OCR: {entry.get('ocr_status', 'pending')}", f"- 精读: {entry.get('deep_reading_status', 'pending')}", '', '## 摘要', '', entry.get('abstract', '') or '暂无摘要', '', '## 💡 文献内容总结', '', '- 由 index-refresh worker 自动生成的正式文献卡片。', '- 精读笔记Deep Reading仅由 /LD-deep 命令维护index-refresh 只保留已有内容,不自动生成。', '- 如需精读,请在 Base 中勾选 analyzeOCR 完成后运行 /LD-deep <zotero_key>。', ''])
lines.extend([f"has_pdf: {('true' if entry.get('has_pdf') else 'false')}", f"ocr_status: {yaml_quote(entry.get('ocr_status', 'pending'))}", f"ocr_job_id: {yaml_quote(entry.get('ocr_job_id', ''))}", f"ocr_md_path: {yaml_quote(entry.get('ocr_md_path', ''))}", f"ocr_json_path: {yaml_quote(entry.get('ocr_json_path', ''))}", f"deep_reading_status: {yaml_quote(entry.get('deep_reading_status', 'pending'))}", f'deep_reading_md_path: {yaml_quote(deep_reading_path)}', f"pdf_path: {yaml_quote(entry.get('pdf_path', ''))}", 'tags:', ' - 文献阅读', f" - {entry.get('domain', '')}", '---', '', f"# {entry['title']}", '', '## 📄 文献基本信息', '', f"- Zotero Key: `{entry.get('zotero_key', '')}`", f"- Collection: `{entry.get('collection_path', '')}`", f"- 作者:{', '.join(entry.get('authors', []))}", f"- PDF: {('已检测' if entry.get('has_pdf') else '未检测到')}", f"- OCR: {entry.get('ocr_status', 'pending')}", f"- 精读: {entry.get('deep_reading_status', 'pending')}", '', '## 摘要', '', entry.get('abstract', '') or '暂无摘要', '', '## 💡 文献内容总结', '', '- 由 sync worker 自动生成的正式文献卡片。', '- 精读笔记Deep Reading仅由 /pf-deep 命令维护sync --index 只保留已有内容,不自动生成。', '- 如需精读,请在 Base 中勾选 analyzeOCR 完成后运行 /pf-deep <zotero_key>。', ''])
if preserved_deep:
lines.extend(['', preserved_deep, ''])
return '\n'.join(lines)
@ -2751,7 +2751,7 @@ def run_deep_reading(vault: Path, verbose: bool = False) -> int:
2. Updates library-records/*.md frontmatter to match actual state
3. Reports the queue of papers awaiting deep reading
Actual content filling is done via /LD-deep (agent-driven).
Actual content filling is done via /pf-deep (agent-driven).
"""
paths = pipeline_paths(vault)
config = load_domain_config(paths)
@ -2808,7 +2808,7 @@ def run_deep_reading(vault: Path, verbose: bool = False) -> int:
blocked = [q for q in pending_queue if q['is_analyze'] and q['ocr_status'] not in ('done', '') and not (q['is_do_ocr'] and q['ocr_status'] in ('pending', 'processing'))]
report_lines = ['# 待精读队列', '']
if ready:
report_lines.extend([f'## 就绪 ({len(ready)} 篇) — OCR 已完成,可直接 /LD-deep', ''])
report_lines.extend([f'## 就绪 ({len(ready)} 篇) — OCR 已完成,可直接 /pf-deep', ''])
for q in ready:
report_lines.append(f"- `{q['zotero_key']}` | {q['domain']} | {q['title']}")
report_lines.append('')

View file

@ -0,0 +1,281 @@
#!/usr/bin/env python3
"""PaperForge consistency audit script.
Checks hard constraints across the codebase:
1. No old command names in active code/docs
2. No paperforge_lite references in Python code
3. No dead internal links in markdown
4. All command/*.md files have valid structure
Exit code: 0 if all pass, 1 if any fail.
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
from typing import Iterable
# Root of the repository (parent of this script)
REPO_ROOT = Path(__file__).parent.parent.resolve()
# Patterns for old command names that should NOT appear outside migration/historical docs
OLD_COMMAND_PATTERNS = {
"paperforge selection-sync": r"paperforge\s+selection-sync",
"paperforge index-refresh": r"paperforge\s+index-refresh",
"paperforge ocr run": r"paperforge\s+ocr\s+run",
"paperforge ocr doctor": r"paperforge\s+ocr\s+doctor",
"/LD-deep": r"/LD-deep",
"/LD-paper": r"/LD-paper",
"/lp-ocr": r"/lp-ocr(?!\w)", # word boundary
"/lp-index-refresh": r"/lp-index-refresh",
"/lp-selection-sync": r"/lp-selection-sync",
"/lp-status": r"/lp-status(?!\w)", # word boundary
}
# Files/directories to exclude from Check 1
CHECK1_EXCLUDES = {
"docs/MIGRATION-v1.2.md",
"docs/ARCHITECTURE.md", # ADR-010 documents historical command names
".planning",
".pytest_cache",
"__pycache__",
".git",
"scripts/consistency_audit.py", # Don't flag the audit script itself
}
# In Python code, these contexts are acceptable (backward compat aliases, tests, etc.)
CHECK1_PY_EXCLUDE_PATTERNS = [
# Docstrings mentioning backward compatibility
r"backward compat",
r"deprecated",
r"alias",
r"命令迁移", # migration note in Chinese
r"migration",
# Test files checking for old commands
r"test_command_docs",
r"test_.*dispatch",
# Comments describing what a function handles
r"Handle `paperforge ocr doctor`",
]
# Required sections in command/*.md files
COMMAND_DOC_REQUIRED_SECTIONS = [
"Purpose",
"CLI Equivalent",
"Prerequisites",
"Arguments",
"Example",
"Output",
"Error Handling",
"Platform Notes",
]
def should_exclude_check1(path: Path) -> bool:
"""Check if a path should be excluded from Check 1."""
rel = path.relative_to(REPO_ROOT).as_posix()
for exclude in CHECK1_EXCLUDES:
if rel.startswith(exclude) or exclude in rel:
return True
return False
def find_files(pattern: str, root: Path = REPO_ROOT) -> Iterable[Path]:
"""Find files matching a glob pattern, excluding common ignored dirs."""
ignore_dirs = {".git", "__pycache__", ".pytest_cache", "node_modules", ".venv", "venv"}
for path in root.rglob(pattern):
if any(part in ignore_dirs for part in path.parts):
continue
yield path
def check_old_commands() -> tuple[int, list[str]]:
"""Check 1: No old command names in active code/docs."""
violations: list[str] = []
# Check markdown files
for md_path in find_files("*.md"):
if should_exclude_check1(md_path):
continue
content = md_path.read_text(encoding="utf-8")
rel = md_path.relative_to(REPO_ROOT).as_posix()
for name, pattern in OLD_COMMAND_PATTERNS.items():
for match in re.finditer(pattern, content):
# Skip matches inside inline code that explicitly mention migration
line_start = content.rfind("\n", 0, match.start()) + 1
line_end = content.find("\n", match.end())
if line_end == -1:
line_end = len(content)
line = content[line_start:line_end]
# Allow old commands in explicit migration sections of AGENTS.md
if rel == "AGENTS.md" and "命令迁移说明" in content[:match.start()]:
# Check if this is in section 11 (migration section)
section_start = content.find("## 11. 命令迁移说明")
if section_start != -1 and match.start() > section_start:
continue
violations.append(f" [{rel}] {name}: {line.strip()}")
# Check Python files (user-facing strings only)
for py_path in find_files("*.py"):
if should_exclude_check1(py_path):
continue
content = py_path.read_text(encoding="utf-8")
rel = py_path.relative_to(REPO_ROOT).as_posix()
for name, pattern in OLD_COMMAND_PATTERNS.items():
for match in re.finditer(pattern, content):
# Skip if in a test file or backward-compat code
if "test_" in rel or "tests/" in rel:
continue
line_start = content.rfind("\n", 0, match.start()) + 1
line_end = content.find("\n", match.end())
if line_end == -1:
line_end = len(content)
line = content[line_start:line_end]
# Skip docstrings/comments about backward compatibility
stripped = line.strip()
if any(re.search(p, stripped, re.IGNORECASE) for p in CHECK1_PY_EXCLUDE_PATTERNS):
continue
# Skip CLI parser setup for backward compat aliases
if "add_parser" in stripped or "help=" in stripped:
continue
violations.append(f" [{rel}] {name}: {stripped}")
passed = len(violations) == 0
return (0 if passed else 1), violations
def check_paperforge_lite() -> tuple[int, list[str]]:
"""Check 2: No paperforge_lite references in Python code."""
violations: list[str] = []
pattern = re.compile(r"paperforge_lite")
for py_path in find_files("*.py"):
rel = py_path.relative_to(REPO_ROOT).as_posix()
# Skip the audit script itself
if rel == "scripts/consistency_audit.py":
continue
content = py_path.read_text(encoding="utf-8")
for match in pattern.finditer(content):
line_start = content.rfind("\n", 0, match.start()) + 1
line_end = content.find("\n", match.end())
if line_end == -1:
line_end = len(content)
line = content[line_start:line_end].strip()
violations.append(f" [{rel}] {line}")
passed = len(violations) == 0
return (0 if passed else 1), violations
def check_dead_links() -> tuple[int, list[str]]:
"""Check 3: No dead internal links in markdown."""
violations: list[str] = []
# Match markdown links: [text](path) or [text](./path)
link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
for md_path in find_files("*.md"):
content = md_path.read_text(encoding="utf-8")
rel = md_path.relative_to(REPO_ROOT).as_posix()
for match in link_pattern.finditer(content):
link_text = match.group(1)
link_target = match.group(2)
# Skip external URLs
if link_target.startswith("http://") or link_target.startswith("https://"):
continue
# Skip anchor-only links
if link_target.startswith("#"):
continue
# Skip mailto links
if link_target.startswith("mailto:"):
continue
# Resolve the target relative to the current markdown file
if link_target.startswith("/"):
target_path = REPO_ROOT / link_target.lstrip("/")
else:
target_path = md_path.parent / link_target
# Check if the target exists (file or directory)
if not target_path.exists():
# Try appending .md for implicit markdown links
if not (target_path.with_suffix(".md")).exists():
violations.append(
f" [{rel}] Dead link to '{link_target}' (text: '{link_text}')"
)
passed = len(violations) == 0
return (0 if passed else 1), violations
def check_command_docs() -> tuple[int, list[str]]:
"""Check 4: All command/*.md files have valid structure."""
violations: list[str] = []
command_dir = REPO_ROOT / "command"
if not command_dir.exists():
violations.append(" command/ directory not found")
return 1, violations
command_files = sorted(command_dir.glob("pf-*.md"))
if not command_files:
violations.append(" No command/pf-*.md files found")
return 1, violations
for cmd_path in command_files:
content = cmd_path.read_text(encoding="utf-8")
rel = cmd_path.relative_to(REPO_ROOT).as_posix()
missing = []
for section in COMMAND_DOC_REQUIRED_SECTIONS:
# Look for ## Section or ## Section (subheading)
if not re.search(rf"^##\s+{re.escape(section)}(\s|$)", content, re.MULTILINE):
missing.append(section)
if missing:
violations.append(f" [{rel}] Missing sections: {', '.join(missing)}")
passed = len(violations) == 0
return (0 if passed else 1), violations
def main() -> int:
print("=== Consistency Audit Results ===\n")
checks = [
("Check 1: No old command names", check_old_commands),
("Check 2: No paperforge_lite in Python", check_paperforge_lite),
("Check 3: No dead links", check_dead_links),
("Check 4: Command docs structure", check_command_docs),
]
total_passed = 0
total_failed = 0
for name, check_fn in checks:
exit_code, details = check_fn()
status = "PASS" if exit_code == 0 else "FAIL"
print(f"[{status}] {name}")
if details:
print(f" Found: {len(details)} occurrence(s)")
for detail in details:
print(detail)
else:
print(" Found: 0 occurrences")
print()
if exit_code == 0:
total_passed += 1
else:
total_failed += 1
print("=== Summary ===")
print(f"Passed: {total_passed}/{len(checks)}")
print(f"Failed: {total_failed}/{len(checks)}")
return 1 if total_failed > 0 else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -114,7 +114,7 @@ def show_completion():
print(f" {Colors.BRIGHT_CYAN}1.{Colors.ENDC} Open Obsidian and load your vault")
print(f" {Colors.BRIGHT_CYAN}2.{Colors.ENDC} Run: {Colors.BOLD}python scripts/index_refresh.py{Colors.ENDC}")
print(f" {Colors.BRIGHT_CYAN}3.{Colors.ENDC} Queue papers for OCR processing")
print(f" {Colors.BRIGHT_CYAN}4.{Colors.ENDC} Start deep reading: {Colors.BOLD}/LD-deep <zotero_key>{Colors.ENDC}")
print(f" {Colors.BRIGHT_CYAN}4.{Colors.ENDC} Start deep reading: {Colors.BOLD}/pf-deep <zotero_key>{Colors.ENDC}")
print(f"\n{Colors.DIM}For help: https://github.com/LLLin000/PaperForge{Colors.ENDC}\n")

View file

@ -1,4 +1,4 @@
# Subagent Prompt for /LD-deep
# Subagent Prompt for /pf-deep
## 任务

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Helpers for /LD and /LD-deep literature sessions."""
"""Helpers for /pf-deep literature sessions."""
from __future__ import annotations
@ -21,7 +21,7 @@ def _load_vault_config(vault: Path) -> dict:
def _paperforge_paths(vault: Path) -> dict[str, Path]:
"""Build PaperForge path inventory for /LD-deep — delegates to shared resolver.
"""Build PaperForge path inventory for /pf-deep — delegates to shared resolver.
Returns ocr, records, literature keys matching shared resolver output.
"""
@ -956,7 +956,7 @@ def _read_json(path: Path) -> dict:
def prepare_deep_reading(vault: Path, zotero_key: str, force: bool = False) -> dict:
"""Automate all mechanical pre-reading steps for /LD-deep.
"""Automate all mechanical pre-reading steps for /pf-deep.
Returns a dict with:
- status: "ok" | "error"
@ -1080,7 +1080,7 @@ def prepare_deep_reading(vault: Path, zotero_key: str, force: bool = False) -> d
break
if formal_note is None:
result["message"] = f"[ERROR] Formal note not found in {literature_root}. Run index-refresh first."
result["message"] = f"[ERROR] Formal note not found in {literature_root}. Run sync --index first."
return result
result["formal_note"] = str(formal_note)
@ -1229,7 +1229,7 @@ if sys.platform == "win32":
def main() -> int:
parser = argparse.ArgumentParser(description="Helpers for /LD-deep note scaffolding")
parser = argparse.ArgumentParser(description="Helpers for /pf-deep note scaffolding")
subparsers = parser.add_subparsers(dest="command", required=True)
figure_parser = subparsers.add_parser("figure-index", help="Extract figures from fulltext markdown")