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
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]>
94 lines
4.2 KiB
Python
Executable file
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())
|