mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
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:
parent
747aabec18
commit
abab8df3f5
14 changed files with 318 additions and 39 deletions
15
AGENTS.md
15
AGENTS.md
|
|
@ -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 | 快速开始指南 | 安装后阅读*
|
||||
|
|
|
|||
|
|
@ -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) — 命令总览与矩阵
|
||||
|
|
|
|||
|
|
@ -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) — 命令总览与矩阵
|
||||
|
|
|
|||
|
|
@ -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) — 命令总览与矩阵
|
||||
|
|
|
|||
|
|
@ -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) — 命令总览与矩阵
|
||||
|
|
|
|||
|
|
@ -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) — 命令总览与矩阵
|
||||
|
|
|
|||
|
|
@ -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) — 状态检查
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 <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 中勾选 analyze,OCR 完成后运行 /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('')
|
||||
|
|
|
|||
281
scripts/consistency_audit.py
Normal file
281
scripts/consistency_audit.py
Normal 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())
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Subagent Prompt for /LD-deep
|
||||
# Subagent Prompt for /pf-deep
|
||||
|
||||
## 任务
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue