mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-16 14:42:29 -05:00
162 lines
5.6 KiB
Python
Executable File
162 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Enforces strict bibliography boundaries between project components."""
|
|
|
|
import re
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
# Define strict scopes: {Name: {"sources": [dirs/files], "bibs": [files]}}
|
|
SCOPES = {
|
|
"Book": {
|
|
"sources": ["book/quarto/contents"],
|
|
"bibs": ["book/quarto/contents/references.bib"]
|
|
},
|
|
"Interviews": {
|
|
"sources": ["interviews"],
|
|
"bibs": ["interviews/paper/references.bib"]
|
|
},
|
|
"TinyTorch": {
|
|
"sources": ["tinytorch"],
|
|
"bibs": ["tinytorch/paper/references.bib"]
|
|
},
|
|
"MLSysIm": {
|
|
"sources": ["mlsysim"],
|
|
"bibs": [
|
|
"mlsysim/paper/references.bib",
|
|
"mlsysim/docs/references.bib"
|
|
]
|
|
},
|
|
"DesignGrammar": {
|
|
"sources": ["design-grammar"],
|
|
"bibs": ["design-grammar/paper/references.bib"]
|
|
}
|
|
}
|
|
|
|
EXCLUDE = ("_build", "_site", "node_modules", ".git", "__pycache__", ".venv")
|
|
NON_CITE_PREFIXES = (
|
|
"sec-", "fig-", "tbl-", "eq-", "lst-", "alg-", "algo-", "exr-", "exm-", "thm-"
|
|
)
|
|
|
|
# Tightened regex: Must NOT be followed by a / (which suggests a file path)
|
|
# and should be followed by a word boundary or specific punctuation.
|
|
CITE_RE = re.compile(r"(?<![=,(])\[?@([A-Za-z][\w:.-]*)(?![/])\b")
|
|
TEX_CITE_RE = re.compile(r"\\cite[a-z]*\*?\{([^}]+)\}")
|
|
|
|
def parse_bib_keys(path):
|
|
if not path.exists(): return set()
|
|
text = path.read_text(encoding="utf-8")
|
|
return set(re.findall(r"^@\w+\s*\{\s*([\w:_-]+)\s*,", text, re.M))
|
|
|
|
# Keys that look like CSS / JS / Python decorators / emails — false positives
|
|
KNOWN_FALSE_POSITIVE_KEYS = {
|
|
"media", "keyframes", "import", "supports", "page", "font-face",
|
|
"charset", "namespace", "document", # CSS at-rules
|
|
"grad", "staticmethod", "classmethod", "property", "abstractmethod",
|
|
"dataclass", "cached_property", "wraps", # Python decorators
|
|
"eecs.harvard.edu", "harvard.edu", "google.com", # emails
|
|
"B", "B.T", "W", "X", "Y", "A", "C", "Z", # Math variables
|
|
}
|
|
|
|
def extract_cites(path):
|
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
if path.suffix == ".tex":
|
|
text = re.sub(r"(?<!\\)%.*", "", text)
|
|
keys = set()
|
|
for m in TEX_CITE_RE.finditer(text):
|
|
for k in m.group(1).split(","):
|
|
k = k.strip()
|
|
if k and k not in KNOWN_FALSE_POSITIVE_KEYS:
|
|
keys.add(k)
|
|
return keys
|
|
else: # .qmd
|
|
text = re.sub(r"```.*?```", "", text, flags=re.S)
|
|
text = re.sub(r"`[^`]+`", "", text)
|
|
keys = set()
|
|
for m in CITE_RE.finditer(text):
|
|
k = m.group(1).rstrip(".,;:)")
|
|
if (
|
|
k
|
|
and not k.lower().startswith(NON_CITE_PREFIXES)
|
|
and not k.endswith("_")
|
|
and k not in KNOWN_FALSE_POSITIVE_KEYS
|
|
):
|
|
keys.add(k)
|
|
return keys
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--verbose", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
all_defined_keys = {} # key -> list of bibs
|
|
scope_bib_keys = {} # scope -> set of keys
|
|
|
|
# 1. Load all bibliographies
|
|
for scope, cfg in SCOPES.items():
|
|
keys = set()
|
|
for bp in cfg["bibs"]:
|
|
path = REPO_ROOT / bp
|
|
pkeys = parse_bib_keys(path)
|
|
keys.update(pkeys)
|
|
for k in pkeys:
|
|
if k not in all_defined_keys: all_defined_keys[k] = []
|
|
all_defined_keys[k].append(bp)
|
|
scope_bib_keys[scope] = keys
|
|
|
|
# 2. Scan sources per scope
|
|
exit_code = 0
|
|
print(f"Checking {len(SCOPES)} citation boundaries...\n")
|
|
|
|
for scope, cfg in SCOPES.items():
|
|
found_cites = defaultdict(list) # key -> list of files
|
|
for src_root in cfg["sources"]:
|
|
p = REPO_ROOT / src_root
|
|
if not p.exists(): continue
|
|
files = list(p.rglob("*.qmd")) + list(p.rglob("*.tex"))
|
|
for f in files:
|
|
if any(part in EXCLUDE for part in f.parts): continue
|
|
cites = extract_cites(f)
|
|
rel = str(f.relative_to(REPO_ROOT))
|
|
for k in cites:
|
|
found_cites[k].append(rel)
|
|
|
|
cited_keys = set(found_cites.keys())
|
|
local_bib = scope_bib_keys[scope]
|
|
|
|
unresolved = sorted(cited_keys - local_bib)
|
|
orphans = sorted(local_bib - cited_keys)
|
|
|
|
print(f"=== Scope: {scope} ===")
|
|
print(f" Bib(s): {', '.join(cfg['bibs'])}")
|
|
print(f" Citations: {len(cited_keys)} unique keys")
|
|
|
|
if not unresolved and not orphans:
|
|
print(" ✅ Integrity perfect.")
|
|
else:
|
|
if unresolved:
|
|
print(f" ❌ {len(unresolved)} unresolved or boundary-leaking citations:")
|
|
for k in unresolved:
|
|
other_bibs = all_defined_keys.get(k)
|
|
if other_bibs:
|
|
print(f" - {k} (LEAK: Found in {', '.join(other_bibs)})")
|
|
else:
|
|
print(f" - {k} (MISSING: Not found in any bib)")
|
|
if args.verbose:
|
|
print(f" Cited in: {', '.join(found_cites[k][:3])}")
|
|
exit_code = 1
|
|
|
|
if orphans:
|
|
print(f" ⚠️ {len(orphans)} scope-specific orphans (defined but not cited in this scope)")
|
|
if args.verbose:
|
|
for k in orphans: print(f" - {k}")
|
|
print()
|
|
|
|
return exit_code
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|