feat: surface OCR structured health in doctor and status

This commit is contained in:
Research Assistant 2026-06-05 10:35:23 +08:00
parent 5cfb58668d
commit 22d189e69e
7 changed files with 161 additions and 4 deletions

View file

@ -15,6 +15,7 @@
"done": 0,
"failed": 0
},
"structured_ocr_health": [],
"path_errors": 0,
"env_configured": false,
"lifecycle_level_counts": {},

View file

@ -1,6 +1,7 @@
"""OCR command — unifies OCR run and diagnose."""
import argparse
import json
import logging
from pathlib import Path
@ -51,6 +52,35 @@ def _collect_ocr_queue_data(vault: Path) -> dict:
}
def _collect_ocr_health_summary(vault: Path) -> list[dict]:
"""Scan OCR directories for health/ocr_health.json files and return summaries."""
from paperforge.worker._utils import pipeline_paths
paths = pipeline_paths(vault)
ocr_root = paths.get("ocr")
if not ocr_root or not ocr_root.exists():
return []
summaries = []
for dir_path in sorted(ocr_root.iterdir()):
if not dir_path.is_dir():
continue
health_path = dir_path / "health" / "ocr_health.json"
if health_path.exists():
try:
health = json.loads(health_path.read_text(encoding="utf-8"))
summaries.append({
"key": dir_path.name,
"overall": health.get("overall", "unknown"),
"page_count": health.get("page_count", 0),
"blocks_count": health.get("blocks_count", 0),
"figure_count": health.get("figure_caption_count", 0),
"table_count": health.get("table_caption_count", 0),
})
except Exception:
continue
return summaries
def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int:
"""Run OCR diagnostics and print results."""
from paperforge.ocr_diagnostics import ocr_doctor
@ -61,6 +91,7 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
if json_output:
queue_data = _collect_ocr_queue_data(vault)
structured_health = _collect_ocr_health_summary(vault)
pf_error = None
if not passed:
if level == 1:
@ -86,6 +117,7 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
"passed": passed,
"message": result.get("message", result.get("error", "")),
},
"structured_health": structured_health,
**queue_data,
},
error=pf_error,
@ -97,13 +129,25 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
print("-" * 40)
if passed:
print(f"[PASS] {result.get('message', 'All checks passed')}")
return 0
else:
print(f"[FAIL] Level {level}: {result.get('error', 'Unknown failure')}")
print(f"[FIX] {result.get('fix', 'No fix suggestion available')}")
if result.get("raw_response"):
print(f"[RAW] {result['raw_response'][:200]}...")
return 1
structured_health = _collect_ocr_health_summary(vault)
if structured_health:
print()
print("--- Structured OCR Health ---")
print(f"Papers with health data: {len(structured_health)}")
for entry in structured_health:
print(
f"- {entry['key']}: overall={entry['overall']}, "
f"{entry['page_count']} pages, {entry['blocks_count']} blocks, "
f"{entry['figure_count']} figures, {entry['table_count']} tables"
)
return 0 if passed else 1
def _get_run_ocr():

View file

@ -1110,6 +1110,10 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
except Exception:
pass
# Structured OCR health
from paperforge.commands.ocr import _collect_ocr_health_summary as _collect_health
_structured_health = _collect_health(vault)
if json_output:
payload = {
"version": __import__("paperforge").__version__,
@ -1128,6 +1132,7 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
"done": ocr_done,
"failed": ocr_failed,
},
"structured_ocr_health": _structured_health,
"path_errors": path_error_count,
"env_configured": len(env_found) > 0,
# Phase 25: lifecycle/health/maturity from canonical index ({} when unavailable)
@ -1171,8 +1176,17 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
f"asset={ha['asset_health']['healthy']}/{ha['asset_health']['unhealthy']}"
)
print(
f"- ocr: {ocr_done}/{ocr_total} done (pending: {ocr_pending}, processing: {ocr_processing}, failed: {ocr_failed})"
f"- ocr: {ocr_done}/{ocr_total} done "
f"(pending: {ocr_pending}, processing: {ocr_processing}, failed: {ocr_failed})"
)
if _structured_health:
print(f"- structured_ocr_health: {len(_structured_health)} paper(s)")
for entry in _structured_health:
print(
f" - {entry['key']}: overall={entry['overall']}, "
f"{entry['page_count']} pages, {entry['blocks_count']} blocks, "
f"{entry['figure_count']} figures, {entry['table_count']} tables"
)
print(f"- path_errors: {path_error_count}")
if path_error_count > 0:
print(" Tip: Run `paperforge repair --fix-paths` to attempt resolution")

View file

@ -19,6 +19,7 @@
"done": 0,
"failed": 0
},
"structured_ocr_health": [],
"path_errors": 0,
"env_configured": true,
"lifecycle_level_counts": {},

View file

@ -47,7 +47,7 @@ class TestStatusJson:
OPTIONAL_KEYS = {
"version", # duplicated from envelope (old format preserved in data)
"formal_notes", "exports", "domains", "bases", "path_errors",
"env_configured", "ocr",
"env_configured", "ocr", "structured_ocr_health",
"lifecycle_level_counts", "health_aggregate", "maturity_distribution",
}

View file

@ -2,6 +2,7 @@
import json
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
from paperforge.ocr_diagnostics import ocr_doctor
@ -182,3 +183,44 @@ def test_l4_live_failure():
assert result["level"] == 4
assert result["passed"] is False
assert "bad pdf" in result["error"]
# ---------------------------------------------------------------------------
# Structured OCR Health in _diagnose()
# ---------------------------------------------------------------------------
def test_doctor_reads_structured_ocr_health(tmp_path: Path, capsys) -> None:
"""_diagnose() reads ocr_health.json and prints structured summary."""
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" / "HLTH001"
ocr_dir.mkdir(parents=True)
health_dir = ocr_dir / "health"
health_dir.mkdir(parents=True)
(health_dir / "ocr_health.json").write_text(
json.dumps({
"overall": "yellow",
"page_count": 5,
"blocks_count": 100,
"figure_caption_count": 3,
"table_caption_count": 2,
}),
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 "Structured OCR Health" in captured.out
assert "yellow" in captured.out
assert "HLTH001" in captured.out
assert "3 figures" in captured.out
assert "2 tables" in captured.out

View file

@ -150,6 +150,33 @@ class TestStatusJsonIndexSource:
assert data["health_aggregate"] == {}
assert data["maturity_distribution"] == {}
def test_status_json_includes_structured_ocr_health(self, tmp_path: Path, capsys) -> None:
"""JSON output includes structured_ocr_health key."""
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" / "HLTH001"
ocr_dir.mkdir(parents=True)
health_dir = ocr_dir / "health"
health_dir.mkdir(parents=True)
(health_dir / "ocr_health.json").write_text(
json.dumps({"overall": "green", "page_count": 3, "blocks_count": 50}),
encoding="utf-8",
)
code = run_status(vault, json_output=True)
captured = capsys.readouterr().out
assert code == 0
envelope = json.loads(captured)
data = envelope["data"]
assert "structured_ocr_health" in data
assert len(data["structured_ocr_health"]) == 1
assert data["structured_ocr_health"][0]["key"] == "HLTH001"
assert data["structured_ocr_health"][0]["overall"] == "green"
def test_status_text_output_includes_index_section(self, tmp_path: Path, capsys) -> None:
"""Text output contains 'lifecycle:' line when index is present."""
from paperforge.worker.status import run_status
@ -245,3 +272,31 @@ class TestDoctorIndexHealth:
output = self._run_doctor(vault)
assert "Index Health" in output
assert "No canonical index" in output
# ---------------------------------------------------------------------------
# Structured OCR Health in status
# ---------------------------------------------------------------------------
def test_status_text_structured_ocr_health(tmp_path: Path, capsys) -> None:
"""run_status() text output shows structured OCR health."""
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" / "HLTH001"
ocr_dir.mkdir(parents=True)
health_dir = ocr_dir / "health"
health_dir.mkdir(parents=True)
(health_dir / "ocr_health.json").write_text(
json.dumps({"overall": "yellow", "page_count": 5, "blocks_count": 100}),
encoding="utf-8",
)
code = run_status(vault)
captured = capsys.readouterr().out
assert code == 0
assert "structured_ocr_health" in captured
assert "HLTH001" in captured
assert "yellow" in captured