#!/usr/bin/env python3 """Every `SPEC §N` a standard's code cites is a section that standard's SPEC.md has. Code cites normative text to say which rule it implements or enforces. When the cited section does not exist, the citation reads as authority it does not have: the mechanism is real, gate-enforcing and tested, and nothing normative stands behind it. That has happened here repeatedly — `jcp-runtime` cited `SPEC §4.10` in six places and §4.10 did not exist (fixed by #190); `aac` cited §3.3 for a rule its SPEC states as "§3 item 3"; `sandbox` cited §6.5 and §12.1 for rules that live in `spec/receipts.md` and `spec/build.md`. Each was found by reading, once, long after it was written. This is that reading, done every time. What it checks: in each directory holding a `SPEC.md`, every `SPEC §N` (one or more parts: §7, §4.10, §2.4.3) appearing in a `*.rs` file under that directory resolves to a numbered heading of that same `SPEC.md`. A heading counts when it begins `#`…, then the number, with or without `§`, as in `## 7.`, `### 6.4 `, `## §3.` or `### §3.1 `. What it deliberately does not check: - Parent numbers are not synthesised. A cited `§4` needs its own `§4` heading; a `§4.10` heading does not satisfy it. A standard that cites a section it never wrote should say so, not be excused by a child. - A citation is resolved against the SPEC.md of the directory the file is in. Every cross-standard citation in the tree today is a self-reference that names its own standard for the reader ("JouleClaw SPEC §1.3", inside jouleclaw/), so this is right for all of them. If a file ever cites another standard's SPEC, resolve it by naming that standard's own file instead — do not loosen this check, which would stop it finding anything. - Sections of a `spec/*.md` subfile are not sections of `SPEC.md`. Rules that live in a subfile should be cited by that file's name, which is what the sandbox fixes above do. - Markdown-to-markdown references are a different check, and WAI's are already covered by `check-wai-section-refs.py`. Exit status 1, with one line per citation naming a section its SPEC does not have; 0 otherwise. python3 scripts/check-spec-refs.py [repo-root] """ import collections import os import pathlib import re import sys HEADING = re.compile(r"^#{1,6}[ \t]+§?([0-9]+(?:\.[0-9]+)*)\.?(?=[ \t])", re.M) CITATION = re.compile(r"SPEC[ \t]+§[ \t]*([0-9]+(?:\.[0-9]+)*)") def sections(spec: pathlib.Path) -> set: return set(HEADING.findall(spec.read_text(errors="replace"))) def main() -> int: root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else pathlib.Path(__file__).resolve().parent.parent).resolve() standards = sorted(d for d in root.iterdir() if d.is_dir() and not d.name.startswith(".") and (d / "SPEC.md").is_file()) dangling = 0 cited_total = 0 for s in standards: defined = sections(s / "SPEC.md") found = collections.defaultdict(list) for f in s.rglob("*.rs"): if "target" in f.parts: continue try: text = f.read_text(errors="replace") except OSError: continue if "SPEC" not in text: continue for i, line in enumerate(text.splitlines(), 1): for m in CITATION.finditer(line): found[m.group(1)].append(f"{f.relative_to(root)}:{i}") cited_total += sum(len(v) for v in found.values()) for n, locs in sorted(found.items(), key=lambda kv: [int(x) for x in kv[0].split(".")]): if n in defined: continue for loc in locs: print(f"{loc}: cites SPEC §{n}, which {s.name}/SPEC.md does not define") dangling += 1 if dangling: print(f"\ncheck-spec-refs: {dangling} dangling citation(s) across {len(standards)} standards.") print("Either add the section, or cite where the rule actually lives " "(a `spec/*.md` subfile, or a numbered item of a parent section).") return 1 print(f"check-spec-refs: {cited_total} citations across {len(standards)} standards, all resolve.") return 0 if __name__ == "__main__": sys.exit(main())