"""Reference zone integrity + ordering + body continuity diagnostic. Checks across many processed papers: 1. Reference ordering: are reference numbers strictly increasing? 2. Zone leakage: are non-reference blocks between reference items? 3. Body continuity: does body text flow across pages without gaps? 4. Figure/table matching: basic count health. """ from __future__ import annotations import argparse import json import re import sys from collections.abc import Iterable from pathlib import Path from typing import Any def _find_repo_root(start: Path) -> Path: current = start.resolve() for parent in [current] + list(current.parents): if (parent / ".git").exists(): return parent raise RuntimeError(f"No repo root from {start}") _THIS_FILE = Path(__file__).resolve() _REPO_ROOT = _find_repo_root(_THIS_FILE) if str(_REPO_ROOT) not in sys.path: sys.path.insert(0, str(_REPO_ROOT)) from paperforge.worker.ocr_artifacts import artifact_paths_for_root # noqa: E402 # Regex: standard ref number prefixes (1. / 1) / [1] / (1)) _REF_NUM_RE = re.compile(r"^\s*(?:(\d+)[\.\)]|\[(\d+)\]|\((\d+)\))\s*") # Detect body paragraph start (non-empty, non-reference, non-heading) _BODY_START_RE = re.compile(r"^[A-Z][^.]+[.?!]") def _load_json(path: Path, default: Any = None) -> Any: if not path.exists(): return default try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return default def _load_block_trace_csv(path: Path) -> list[dict[str, str]]: import csv if not path.exists(): return [] with open(path, encoding="utf-8") as fh: return list(csv.DictReader(fh)) def _resolve_ocr_root(raw: str | None) -> Path: candidate = raw or os.environ.get("PAPERFORGE_OCR_ROOT") or os.environ.get("PAPERFORGE_REAL_OCR_ROOT") # backstop: relative to repo if not candidate: candidate = str(_REPO_ROOT / ".." / ".." / "OB" / "Literature-hub" / "System" / "PaperForge" / "ocr") path = Path(candidate) if not path.exists(): path = _REPO_ROOT.parent / "ocr" return path.resolve() def _find_paper_keys(ocr_root: Path, limit: int = 0) -> list[str]: """Return paper keys sorted by mtime (newest first).""" entries = sorted( [d for d in ocr_root.iterdir() if d.is_dir() and not d.name.startswith("_") and not d.name.startswith(".")], key=lambda d: d.stat().st_mtime, reverse=True, ) keys = [d.name for d in entries] return keys[:limit] if limit > 0 else keys def _parse_ref_number(text: str) -> int | None: """Extract reference number from standard prefix formats.""" m = _REF_NUM_RE.match(text) if m: for g in m.groups(): if g is not None: return int(g) return None def _analyze_reference_ordering(fulltext: str) -> dict: """Check reference ordering and non-reference intrusion. Scans from first `## References` (or `# References`) to end. """ ref_section_text = "" for sep in ["\n## References\n", "\n# References\n", "\n## Bibliography\n"]: if sep in fulltext: ref_section_text = fulltext.split(sep, 1)[1] break if not ref_section_text: return {"status": "no_ref_section", "count": 0, "issues": []} issues: list[dict] = [] blocks: list[dict] = [] current_text = "" current_num = None for line in ref_section_text.split("\n"): line_stripped = line.strip() if not line_stripped: continue num = _parse_ref_number(line_stripped) if num is not None: if current_text: blocks.append({"number": current_num, "text": current_text[:80]}) current_text = line_stripped current_num = num elif line_stripped.startswith("