open-standards/scripts/check-spec-refs.py
David Charlot 4b05fd80e1
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m15s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m22s
standards workspaces build + test / aac-rs (pull_request) Successful in 2m31s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 54s
standards workspaces build + test / map-rs (pull_request) Successful in 55s
standards workspaces build + test / mesh-rs (pull_request) Successful in 55s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 56s
standards workspaces build + test / openpay (pull_request) Successful in 3m4s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m36s
lockfiles / every Cargo.lock is current (pull_request) Successful in 40s
spec citations resolve / Code cites no section its SPEC lacks (pull_request) Successful in 4s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m10s
standards workspaces build + test / eoc-rs (pull_request) Successful in 2m0s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m12s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m31s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m11s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m19s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m22s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m46s
ci: a citation to a section that does not exist is now a failure
The commit before this fixed five of them. They were written long before
anyone read them, and the one in #190 — `jcp-runtime` citing `SPEC §4.10` in
six places, with no §4.10 — survived a gate-enforcing, tested mechanism for
months. Reading the tree once finds these; reading it every time is a check.

`scripts/check-spec-refs.py` resolves every `SPEC §N` in a standard's `*.rs`
against the numbered headings of the `SPEC.md` beside it. 1454 citations
across 15 standards resolve today.

Three decisions worth stating, because each is a way the check could have been
written to find nothing:

  - Parents are not synthesised. A cited `§4` needs its own `§4` heading; a
    `§4.10` heading does not satisfy it. Otherwise a standard citing a section
    it never wrote is excused by a child it happens to have.
  - A citation resolves against the SPEC.md of its own directory. Every
    cross-standard citation in the tree today is a self-reference that names
    its standard for the reader ("JouleClaw SPEC §1.3", inside jouleclaw/), so
    this is right for all of them; the header says what to do instead if that
    ever stops being true, rather than loosening the check.
  - Sections of a `spec/*.md` subfile are not sections of `SPEC.md`.

The self-test asserts the check can fail, in the four ways that matter: an
unresolvable citation fails, both heading spellings (`## 4.` and `## §4`) are
accepted, a child heading does not satisfy its parent, and one standard's
section does not excuse another's citation. A regex that quietly stops matching
would otherwise find nothing and exit 0 — the failure mode this repo has
already met in gated and skipped tests.

The workflow carries no `paths:` filter, deliberately. The check is a second of
Python with no toolchain, and a correct filter would have to name every
standard's `**/*.rs` and every `SPEC.md`; `standards-build.yml` records that
`**`-prefixed globs do not match on this forge and that a gate which quietly
fails to fire is worse than none, since it reports nothing and that reads as
green.

This is `check-wai-section-refs.py`'s complement, not a duplicate: that one
checks WAI markdown against WAI markdown, this one checks every standard's
code against its spec.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-13 03:07:23 -04:00

94 lines
4.2 KiB
Python
Executable file

#!/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())