feat: surface OCR version runtime state in status and doctor

This commit is contained in:
Research Assistant 2026-06-05 11:28:34 +08:00
parent 5efb4435b4
commit 0c3ecb2bc3
4 changed files with 169 additions and 0 deletions

View file

@ -89,6 +89,31 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
level = result.get("level", 0)
passed = result.get("passed", False)
# Collect OCR version state
_version_state_summary: dict = {"total_papers": 0, "derived_stale": [], "raw_upgradable": []}
try:
from paperforge.config import load_vault_config
cfg = load_vault_config(vault)
ocr_root = vault / cfg["system_dir"] / "PaperForge" / "ocr"
if ocr_root.exists():
from paperforge.worker._utils import read_json
_version_papers = []
for paper_dir in ocr_root.iterdir():
if not paper_dir.is_dir():
continue
meta_path = paper_dir / "meta.json"
if meta_path.exists():
meta = read_json(meta_path)
if "raw_version" in meta or "derived_version" in meta:
_version_papers.append(meta)
_version_state_summary = {
"total_papers": len(_version_papers),
"derived_stale": [m.get("zotero_key", "?") for m in _version_papers if m.get("derived_stale")],
"raw_upgradable": [m.get("zotero_key", "?") for m in _version_papers if m.get("raw_upgradable")],
}
except Exception:
pass
if json_output:
queue_data = _collect_ocr_queue_data(vault)
structured_health = _collect_ocr_health_summary(vault)
@ -118,6 +143,7 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
"message": result.get("message", result.get("error", "")),
},
"structured_health": structured_health,
"ocr_version_state": _version_state_summary,
**queue_data,
},
error=pf_error,
@ -147,6 +173,19 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
f"{entry['figure_count']} figures, {entry['table_count']} tables"
)
if _version_state_summary["total_papers"] > 0:
print()
print("--- OCR Version State ---")
print(f" ocr_version_state: {_version_state_summary['total_papers']} paper(s)")
if _version_state_summary["derived_stale"]:
print(f" derived_stale: {len(_version_state_summary['derived_stale'])} paper(s)")
for k in _version_state_summary["derived_stale"]:
print(f" - {k}")
if _version_state_summary["raw_upgradable"]:
print(f" raw_upgradable: {len(_version_state_summary['raw_upgradable'])} paper(s)")
for k in _version_state_summary["raw_upgradable"]:
print(f" - {k}")
return 0 if passed else 1

View file

@ -1114,6 +1114,36 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
from paperforge.commands.ocr import _collect_ocr_health_summary as _collect_health
_structured_health = _collect_health(vault)
# OCR version state
_ocr_version_state = {
"total_papers": 0, "derived_stale_count": 0, "raw_upgradable_count": 0,
"derived_stale_keys": [], "raw_upgrade_keys": [],
}
try:
ocr_root = vault / cfg["system_dir"] / "PaperForge" / "ocr"
if ocr_root.exists():
papers = []
for paper_dir in ocr_root.iterdir():
if not paper_dir.is_dir():
continue
meta_path = paper_dir / "meta.json"
if meta_path.exists():
meta = read_json(meta_path)
if "raw_version" in meta or "derived_version" in meta:
papers.append(meta)
if papers:
_ocr_version_state["total_papers"] = len(papers)
_ocr_version_state["derived_stale_count"] = sum(1 for m in papers if m.get("derived_stale"))
_ocr_version_state["raw_upgradable_count"] = sum(1 for m in papers if m.get("raw_upgradable"))
_ocr_version_state["derived_stale_keys"] = [
m.get("zotero_key", "?") for m in papers if m.get("derived_stale")
]
_ocr_version_state["raw_upgrade_keys"] = [
m.get("zotero_key", "?") for m in papers if m.get("raw_upgradable")
]
except Exception:
pass
if json_output:
payload = {
"version": __import__("paperforge").__version__,
@ -1133,6 +1163,7 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
"failed": ocr_failed,
},
"structured_ocr_health": _structured_health,
"ocr_version_state": _ocr_version_state,
"path_errors": path_error_count,
"env_configured": len(env_found) > 0,
# Phase 25: lifecycle/health/maturity from canonical index ({} when unavailable)
@ -1187,6 +1218,14 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
f"{entry['page_count']} pages, {entry['blocks_count']} blocks, "
f"{entry['figure_count']} figures, {entry['table_count']} tables"
)
if _ocr_version_state["total_papers"] > 0:
stale = _ocr_version_state["derived_stale_count"]
upgradable = _ocr_version_state["raw_upgradable_count"]
print(f"- ocr_version_state: {_ocr_version_state['total_papers']} paper(s)")
if stale:
print(f" derived_stale: {stale} (auto-rebuilt on sync)")
if upgradable:
print(f" raw_upgradable: {upgradable} (run `paperforge ocr redo` to upgrade)")
print(f"- path_errors: {path_error_count}")
if path_error_count > 0:
print(" Tip: Run `paperforge repair --fix-paths` to attempt resolution")

View file

@ -224,3 +224,36 @@ def test_doctor_reads_structured_ocr_health(tmp_path: Path, capsys) -> None:
assert "HLTH001" in captured.out
assert "3 figures" in captured.out
assert "2 tables" in captured.out
# ---------------------------------------------------------------------------
# OCR version state in _diagnose()
# ---------------------------------------------------------------------------
def test_doctor_mentions_version_state(tmp_path: Path, capsys) -> None:
"""_diagnose() output includes OCR version state."""
vault = tmp_path / "vault"
vault.mkdir()
(vault / "paperforge.json").write_text(
json.dumps({"vault_config": {"system_dir": "System", "resources_dir": "Resources"}}),
encoding="utf-8",
)
ocr_dir = vault / "System" / "PaperForge" / "ocr" / "DOC001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "DOC001",
"raw_version": {"ocr_model": "PaddleOCR-VL-1.5", "ocr_raw_schema_version": "1.0.0"},
"derived_version": {"renderer_version": "1.0.0-compat"},
"raw_upgradable": True,
"derived_stale": True,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
from paperforge.commands.ocr import _diagnose
with patch("paperforge.ocr_diagnostics.ocr_doctor",
return_value={"level": 3, "passed": True, "message": "All good"}):
exit_code = _diagnose(vault, live=False)
captured = capsys.readouterr()
assert exit_code == 0
assert "raw_upgradable" in captured.out or "version" in captured.out or "stale" in captured.out

View file

@ -300,3 +300,61 @@ def test_status_text_structured_ocr_health(tmp_path: Path, capsys) -> None:
assert "structured_ocr_health" in captured
assert "HLTH001" in captured
assert "yellow" in captured
# ---------------------------------------------------------------------------
# OCR version runtime state in status
# ---------------------------------------------------------------------------
def test_status_json_includes_ocr_version_state(tmp_path: Path, capsys) -> None:
"""JSON output includes ocr_version_state with version info."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "VST001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "VST001",
"raw_version": {"ocr_model": "PaddleOCR-VL-1.6", "ocr_raw_schema_version": "1.0.0"},
"derived_version": {"renderer_version": "2.0.0"},
"raw_upgradable": False,
"derived_stale": False,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
code = run_status(vault, json_output=True)
assert code == 0
captured = capsys.readouterr().out
envelope = json.loads(captured)
payload = envelope["data"]
assert "ocr_version_state" in payload
assert payload["ocr_version_state"]["total_papers"] == 1
assert payload["ocr_version_state"]["derived_stale_count"] == 0
assert payload["ocr_version_state"]["raw_upgradable_count"] == 0
def test_status_text_ocr_version_state(tmp_path: Path, capsys) -> None:
"""Text output includes ocr_version_state line."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "VST001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "VST001",
"raw_version": {"ocr_model": "PaddleOCR-VL-1.5", "ocr_raw_schema_version": "1.0.0"},
"derived_version": {"renderer_version": "1.0.0-compat"},
"raw_upgradable": True,
"derived_stale": True,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
code = run_status(vault)
assert code == 0
captured = capsys.readouterr().out
assert "ocr_version_state" in captured