#!/usr/bin/env python3 """Audit Volume 1 and Volume 2 cross-references. This script is deliberately conservative. It reports mechanical facts and chapter-sized editorial cues; it does not rewrite prose or infer final targets. """ from __future__ import annotations import argparse import json import re from collections import Counter, defaultdict from dataclasses import asdict, dataclass from pathlib import Path def find_repo_root() -> Path: for parent in Path(__file__).resolve().parents: if (parent / "book" / "quarto" / "contents").exists(): return parent raise RuntimeError("Could not locate repository root containing book/quarto/contents") ROOT = find_repo_root() CONTENTS = ROOT / "book" / "quarto" / "contents" OUT_DIR = ROOT / "review" / "cross-references" INVENTORY_PATH = OUT_DIR / "inventory.json" REPORT_PATH = OUT_DIR / "report.md" PACKET_DIR = OUT_DIR / "chapter-packets" CANONICAL_INDEX_PATH = OUT_DIR / "canonical-target-candidates.yml" SCHEMA_PATH = OUT_DIR / "chapter-report-schema.yml" def configure_output(out_dir: Path) -> None: global OUT_DIR, INVENTORY_PATH, REPORT_PATH, PACKET_DIR, CANONICAL_INDEX_PATH, SCHEMA_PATH OUT_DIR = out_dir INVENTORY_PATH = OUT_DIR / "inventory.json" REPORT_PATH = OUT_DIR / "report.md" PACKET_DIR = OUT_DIR / "chapter-packets" CANONICAL_INDEX_PATH = OUT_DIR / "canonical-target-candidates.yml" SCHEMA_PATH = OUT_DIR / "chapter-report-schema.yml" ANCHOR_RE = re.compile(r"\{[^}]*#((?:sec|fig|tbl|eq|pri)-[A-Za-z0-9_-]+)(?=[\s}])") QMD_REF_RE = re.compile(r"(? str | None: parts = path.parts if "vol1" in parts: return "vol1" if "vol2" in parts: return "vol2" return None def kind_for(ref_id: str) -> str: return ref_id.split("-", 1)[0] def rel(path: Path) -> str: return path.relative_to(ROOT).as_posix() def iter_qmd_files() -> list[Path]: files: list[Path] = [] for volume in ("vol1", "vol2"): base = CONTENTS / volume for path in sorted(base.rglob("*.qmd")): if path.name in SKIP_PARTS: continue if "_shelved" in path.as_posix(): continue files.append(path) return files def clean_heading_title(raw: str) -> str: return re.sub(r"\s+\{#[^}]+\}\s*$", "", raw).strip() def is_rendered_skip_line(line: str, in_fence: bool) -> bool: stripped = line.strip() return ( in_fence or stripped.startswith("