mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-18 00:44:29 -05:00
179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
check_references.py — Bibliography completeness validator.
|
|
|
|
Scans each volume's QMD chapters for [@citation] keys and verifies every key
|
|
is defined in the shared book bibliography at contents/references.bib.
|
|
|
|
Usage:
|
|
# Check only (no changes):
|
|
python3 scripts/check_references.py
|
|
|
|
# Check only; --fix is retained for CLI compatibility but does not edit:
|
|
python3 scripts/check_references.py --fix
|
|
|
|
Run from book/quarto/ directory.
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
VOLUMES = {
|
|
"vol1": {
|
|
"chapters_dir": Path("contents/vol1"),
|
|
"bib_file": Path("contents/references.bib"),
|
|
},
|
|
"vol2": {
|
|
"chapters_dir": Path("contents/vol2"),
|
|
"bib_file": Path("contents/references.bib"),
|
|
},
|
|
}
|
|
|
|
# Quarto cross-reference prefixes — these are NOT citation keys
|
|
CROSSREF_PREFIXES = (
|
|
"sec-", "fig-", "tbl-", "eq-", "lst-", "exm-", "thm-",
|
|
"lem-", "cor-", "prp-", "cnj-", "def-", "rem-", "sol-", "alg-",
|
|
"algo-", "ch-", "nb-",
|
|
)
|
|
KNOWN_FALSE_POSITIVE_KEYS = {
|
|
"media", "keyframes", "import", "supports", "page", "font-face",
|
|
"charset", "namespace", "document",
|
|
"grad", "staticmethod", "classmethod", "property", "abstractmethod",
|
|
"dataclass", "cached_property", "wraps",
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parsing helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_CITATION_RE = re.compile(r"(?<![=,(])@([A-Za-z][\w:.-]*)\b")
|
|
|
|
|
|
def extract_cited_keys(qmd_path: Path) -> set[str]:
|
|
"""Return all bibliography citation keys used in a QMD file."""
|
|
keys: set[str] = set()
|
|
text = qmd_path.read_text(encoding="utf-8")
|
|
text = re.sub(r"\A---\n.*?\n---\n", "", text, flags=re.S)
|
|
text = re.sub(r"<!--+.*?-->", "", text, flags=re.S)
|
|
text = re.sub(r"(?m)^[ \t]*```[^\n]*\n.*?\n[ \t]*```[^\n]*$", "", text, flags=re.S)
|
|
text = re.sub(r"`[^`]+`", "", text)
|
|
for match in _CITATION_RE.finditer(text):
|
|
key = match.group(1).rstrip(".,;:)")
|
|
if (
|
|
key
|
|
and key not in KNOWN_FALSE_POSITIVE_KEYS
|
|
and not key.lower().startswith(CROSSREF_PREFIXES)
|
|
):
|
|
keys.add(key)
|
|
return keys
|
|
|
|
|
|
def parse_bib_entries(bib_path: Path) -> dict[str, str]:
|
|
"""Return {key: full_entry_text} for every entry in a .bib file."""
|
|
entries: dict[str, str] = {}
|
|
if not bib_path.exists():
|
|
return entries
|
|
|
|
text = bib_path.read_text(encoding="utf-8")
|
|
# Split on entry boundaries — each starts with @Type{
|
|
raw_entries = re.split(r"(?=^@)", text, flags=re.MULTILINE)
|
|
for raw in raw_entries:
|
|
raw = raw.strip()
|
|
if not raw:
|
|
continue
|
|
m = re.match(r"@[A-Za-z]+\{([^,]+),", raw)
|
|
if m:
|
|
entries[m.group(1).strip()] = raw
|
|
return entries
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core logic
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def check_volume(vol_id: str, config: dict, all_bibs: dict[str, dict[str, str]]) -> dict[str, list[str]]:
|
|
"""
|
|
Returns {missing_key: [qmd_files_that_cite_it]} for citations in this
|
|
volume's chapters that are absent from the shared references.bib.
|
|
"""
|
|
own_bib = all_bibs[vol_id]
|
|
chapters_dir = config["chapters_dir"]
|
|
|
|
missing: dict[str, list[str]] = {}
|
|
for qmd in sorted(chapters_dir.rglob("*.qmd")):
|
|
for key in extract_cited_keys(qmd):
|
|
if key not in own_bib:
|
|
missing.setdefault(key, []).append(str(qmd))
|
|
return missing
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--fix", action="store_true",
|
|
help="Deprecated no-op; missing shared-bib entries require a reviewed BibTeX addition.")
|
|
args = parser.parse_args()
|
|
|
|
shared_bib_path = Path("contents/references.bib")
|
|
if not shared_bib_path.exists():
|
|
print(f"[ERROR] {shared_bib_path} not found — run from book/quarto/", file=sys.stderr)
|
|
return 1
|
|
shared_bib = parse_bib_entries(shared_bib_path)
|
|
all_bibs: dict[str, dict[str, str]] = {vol_id: shared_bib for vol_id in VOLUMES}
|
|
print(f"[INFO] shared references.bib: {len(shared_bib)} entries")
|
|
|
|
overall_ok = True
|
|
|
|
for vol_id, config in VOLUMES.items():
|
|
print(f"\n{'='*60}")
|
|
print(f"Checking {vol_id} ({config['chapters_dir']}) ...")
|
|
print(f"{'='*60}")
|
|
|
|
missing = check_volume(vol_id, config, all_bibs)
|
|
|
|
if not missing:
|
|
print(f" ✅ All citations resolved in shared references.bib")
|
|
continue
|
|
|
|
overall_ok = False
|
|
not_found_anywhere: list[str] = []
|
|
|
|
for key, qmds in sorted(missing.items()):
|
|
not_found_anywhere.append(key)
|
|
status = "MISSING FROM SHARED BIB"
|
|
print(f" ⚠️ {key} [{status}]")
|
|
for q in qmds[:3]: # limit noise
|
|
print(f" cited in: {q}")
|
|
if len(qmds) > 3:
|
|
print(f" ... and {len(qmds) - 3} more file(s)")
|
|
|
|
if not_found_anywhere:
|
|
print(f"\n ❌ {len(not_found_anywhere)} key(s) not found in any volume's bib:")
|
|
for k in not_found_anywhere:
|
|
print(f" {k}")
|
|
|
|
if args.fix:
|
|
print("\n --fix is no longer automatic with a shared bibliography.")
|
|
print(" Add a reviewed BibTeX entry to contents/references.bib, or update the citation key.")
|
|
|
|
print(f"\n{'='*60}")
|
|
if overall_ok:
|
|
print("✅ All book citations resolve against the shared bibliography.")
|
|
return 0
|
|
else:
|
|
print("❌ Missing citations found. See above.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|