From abab8df3f5c895ff0cbac3ea0944c9ce672c2643 Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Fri, 24 Apr 2026 18:06:35 +0800 Subject: [PATCH] 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 --- AGENTS.md | 15 +- command/pf-deep.md | 2 +- command/pf-ocr.md | 2 +- command/pf-paper.md | 2 +- command/pf-status.md | 2 +- command/pf-sync.md | 2 +- docs/COMMANDS.md | 10 +- paperforge/cli.py | 7 +- paperforge/ocr_diagnostics.py | 14 +- .../worker/scripts/literature_pipeline.py | 6 +- scripts/consistency_audit.py | 281 ++++++++++++++++++ scripts/welcome.py | 2 +- skills/literature-qa/prompt_deep_subagent.md | 2 +- skills/literature-qa/scripts/ld_deep.py | 10 +- 14 files changed, 318 insertions(+), 39 deletions(-) create mode 100644 scripts/consistency_audit.py diff --git a/AGENTS.md b/AGENTS.md index 643e10ae..37da9d7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -424,19 +424,14 @@ cp -r 新下载的scripts/* /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 ` | `/pf-deep ` | Agent 精读命令 | -| `/LD-paper ` | `/pf-paper ` | 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 | 快速开始指南 | 安装后阅读* diff --git a/command/pf-deep.md b/command/pf-deep.md index 32bb8f51..b20bb654 100644 --- a/command/pf-deep.md +++ b/command/pf-deep.md @@ -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) — 命令总览与矩阵 diff --git a/command/pf-ocr.md b/command/pf-ocr.md index d21dcfca..b41e49d8 100644 --- a/command/pf-ocr.md +++ b/command/pf-ocr.md @@ -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) — 命令总览与矩阵 diff --git a/command/pf-paper.md b/command/pf-paper.md index 31f4ef58..5b5d1f51 100644 --- a/command/pf-paper.md +++ b/command/pf-paper.md @@ -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) — 命令总览与矩阵 diff --git a/command/pf-status.md b/command/pf-status.md index 05c2c920..99a836f1 100644 --- a/command/pf-status.md +++ b/command/pf-status.md @@ -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) — 命令总览与矩阵 diff --git a/command/pf-sync.md b/command/pf-sync.md index dd79198e..fdeb384c 100644 --- a/command/pf-sync.md +++ b/command/pf-sync.md @@ -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) — 命令总览与矩阵 diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 75779147..202c6801 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -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) — 状态检查 --- diff --git a/paperforge/cli.py b/paperforge/cli.py index 987067a0..30a7fc1a 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -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 /PaperForge/.env before dispatching to worker functions, matching the legacy pipeline behavior. diff --git a/paperforge/ocr_diagnostics.py b/paperforge/ocr_diagnostics.py index d30b747a..57520745 100644 --- a/paperforge/ocr_diagnostics.py +++ b/paperforge/ocr_diagnostics.py @@ -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`.", ) diff --git a/pipeline/worker/scripts/literature_pipeline.py b/pipeline/worker/scripts/literature_pipeline.py index 7dc67d92..9cca2c91 100644 --- a/pipeline/worker/scripts/literature_pipeline.py +++ b/pipeline/worker/scripts/literature_pipeline.py @@ -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 中勾选 analyze,OCR 完成后运行 /LD-deep 。', '']) + 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 中勾选 analyze,OCR 完成后运行 /pf-deep 。', '']) 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('') diff --git a/scripts/consistency_audit.py b/scripts/consistency_audit.py new file mode 100644 index 00000000..fab31406 --- /dev/null +++ b/scripts/consistency_audit.py @@ -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()) diff --git a/scripts/welcome.py b/scripts/welcome.py index 7c075eee..e2e024fd 100644 --- a/scripts/welcome.py +++ b/scripts/welcome.py @@ -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 {Colors.ENDC}") + print(f" {Colors.BRIGHT_CYAN}4.{Colors.ENDC} Start deep reading: {Colors.BOLD}/pf-deep {Colors.ENDC}") print(f"\n{Colors.DIM}For help: https://github.com/LLLin000/PaperForge{Colors.ENDC}\n") diff --git a/skills/literature-qa/prompt_deep_subagent.md b/skills/literature-qa/prompt_deep_subagent.md index b46dc4eb..5fbd6160 100644 --- a/skills/literature-qa/prompt_deep_subagent.md +++ b/skills/literature-qa/prompt_deep_subagent.md @@ -1,4 +1,4 @@ -# Subagent Prompt for /LD-deep +# Subagent Prompt for /pf-deep ## 任务 diff --git a/skills/literature-qa/scripts/ld_deep.py b/skills/literature-qa/scripts/ld_deep.py index 7e6106c0..e7b25869 100644 --- a/skills/literature-qa/scripts/ld_deep.py +++ b/skills/literature-qa/scripts/ld_deep.py @@ -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")