from __future__ import annotations
import json
import re
import sys
from collections import Counter
from pathlib import Path
sys.path.insert(0, "/mnt/c/Users/RyanJ/spectre-rs/scripts")
from bench_markdown_heading_f1 import normalize_heading from sweep_heading_f1 import CORPUS, headings_from_toc
import pymupdf4llm
import spectre_rs
def headings_md(md: str) -> set[str]:
out: set[str] = set()
for m in re.finditer(r"^#+\s+(.+?)$", md, re.M):
h = normalize_heading(m.group(1))
if h and len(h) > 1:
out.add(h)
return out
def classify_miss(h: str) -> str:
if "," in h or ";" in h:
return "MULTI_CLAUSE_COMMA"
if "(" in h or ")" in h:
return "PARENTHETICAL"
words = h.split()
if len(words) >= 8:
return "LONG_8+"
if len(words) >= 5:
return "LONG_5_7"
if any(w.isdigit() for w in words):
return "WITH_NUMBER"
if h.endswith(".") or h.endswith("?"):
return "TRAILING_PUNCT"
return "SHORT_PLAIN"
def main() -> None:
cache_path = Path("/tmp/spectre_extra_corpus/pmu4llm_md_cache.json")
md_cache: dict[str, str] = {}
if cache_path.exists():
md_cache = json.loads(cache_path.read_text())
pmu_only_pattern: Counter[str] = Counter()
pmu_only_examples: dict[str, list[str]] = {}
spec_only_pattern: Counter[str] = Counter()
total_pmu_only = 0
total_spec_only = 0
total_both_hit = 0
total_both_miss = 0
per_doc: list[tuple[str, int, int, int, int]] = []
for label, path in CORPUS:
p = Path(path)
if not p.exists():
continue
b = p.read_bytes()
try:
sd = spectre_rs.Document(b)
toc = sd.toc()
except Exception:
continue
gt = headings_from_toc(toc)
if len(gt) < 5:
continue
spec_md = sd.markdown()
spec_heads = headings_md(spec_md)
cache_key = f"{path}|{p.stat().st_mtime_ns}"
if cache_key in md_cache:
pmu_md = md_cache[cache_key]
else:
try:
pmu_md = pymupdf4llm.to_markdown(path)
except Exception:
continue
md_cache[cache_key] = pmu_md
cache_path.write_text(json.dumps(md_cache))
pmu_heads = headings_md(pmu_md)
spec_only = spec_heads & gt - pmu_heads
pmu_only = pmu_heads & gt - spec_heads
both_hit = spec_heads & gt & pmu_heads
both_miss = gt - spec_heads - pmu_heads
total_spec_only += len(spec_only)
total_pmu_only += len(pmu_only)
total_both_hit += len(both_hit)
total_both_miss += len(both_miss)
per_doc.append((label, len(gt), len(spec_only), len(pmu_only), len(both_hit)))
for h in pmu_only:
cat = classify_miss(h)
pmu_only_pattern[cat] += 1
if cat not in pmu_only_examples:
pmu_only_examples[cat] = []
if len(pmu_only_examples[cat]) < 5:
pmu_only_examples[cat].append(f"{label}: {h!r}")
for h in spec_only:
spec_only_pattern[classify_miss(h)] += 1
print(f"Aggregate over {len(per_doc)} PDFs with TOC ≥5 entries")
print(f" both_hit: {total_both_hit}")
print(f" spec_only: {total_spec_only}")
print(f" pmu_only: {total_pmu_only} ← load-bearing fix-targets")
print(f" both_miss: {total_both_miss}")
print()
print("pmu_only miss categories (sorted by count):")
for cat, n in pmu_only_pattern.most_common():
pct = n / max(total_pmu_only, 1) * 100
print(f" {cat:<22} {n:>5} ({pct:>5.1f}%)")
print()
print("Examples per category:")
for cat, ex in pmu_only_examples.items():
print(f"\n {cat}:")
for e in ex:
print(f" {e}")
print()
print("\nspec_only miss categories (where spec uniquely catches):")
for cat, n in spec_only_pattern.most_common():
pct = n / max(total_spec_only, 1) * 100
print(f" {cat:<22} {n:>5} ({pct:>5.1f}%)")
if __name__ == "__main__":
main()