The registry's unregistered labels now hold every wai.quantum string the tree uses that no revision registers, each saying what it names, grouped: the receipt kinds, the modules of wai-quantum, the names a record's members carry, the fabric's kernels and corpora, the circuit contracts' revisions, the documents with no schema file, the digest domains, and the strings the tests and the provider corpus give a reader to see it refuse them. A document's name, wai.quantum.<document>/<major>, is accounted for by its schema file (schemas/<document>.v<major>.json, its title the name) and needs no row. scripts/check-wai-quantum-names.py reads the registry export and the schema files and every text file but the changelog, and names the file and line of any string neither accounts for; it also fails on a pattern that matches nothing, a schema file whose title is not its file's name, a quantum label nothing outside the registry uses, and a string held twice. The repository checks run it and its self-test. The wai-quantum constants test compares the receipt kinds alone, and the registry test refuses a label listed twice. Co-Authored-By: Claude Opus 5.5 <[email protected]>
216 lines
9 KiB
Python
Executable file
216 lines
9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""check-wai-quantum-names.py: every `wai.quantum.*` string in the tree is accounted for.
|
|
|
|
A string of the quantum namespace is one of:
|
|
|
|
- a string the registry registers (a capability or a record), or one of its named
|
|
identifiers, unregistered labels or wire identifiers. The registry is one table,
|
|
wai/wai-rs/src/codecs/registry.rs, exported by `wai registry export` as
|
|
wai/registry/capabilities.json (CI holds the export to the table with
|
|
`wai registry export --check`). This reads the export. A label there says what it
|
|
names: a module of the quantum crates, a receipt kind, a kernel, a document with no
|
|
schema file, or a string a test gives a reader to see it refused.
|
|
- the name of a document a quantum crate writes, `wai.quantum.<document>/<major>`,
|
|
kept as a JSON Schema at `schemas/<document>.v<major>.json` beside the crate's
|
|
manifest, whose `title` is the name (wai/wai-quantum/VERSIONING.md, *The schemas*).
|
|
A name such a file gives as the `const` of a member (a document carried inside
|
|
another) is accounted for the same way. The schema files are the list: a new
|
|
document adds its file, and no line anywhere else.
|
|
|
|
A string is `wai.quantum.` followed by dot-separated segments of letters, digits, `_`
|
|
and `-`, and optionally `/` and a revision (`wai.quantum.circuit2`,
|
|
`wai.quantum.cal.noise_learn`, `wai.quantum.job-result/1`, `wai.quantum.sv/run-wide`).
|
|
A pattern `wai.quantum.<segments>.*` is accounted for when some accounted string starts
|
|
with `wai.quantum.<segments>.`. A placeholder (`wai.quantum.<document>`) or a prefix
|
|
(`"wai.quantum."`) is not a string and is not read.
|
|
|
|
Every text file git tracks or would track is read, except the changelog
|
|
(wai/CHANGELOG.md and the entry files of wai/changes/): it records names as they were
|
|
when each change was made. Binary files are not read.
|
|
|
|
Findings, one line each:
|
|
|
|
- a string accounted for by none of the above, with its file and line;
|
|
- a quantum schema file whose `title` is not the name its file name gives;
|
|
- a `wai.quantum.*` label or wire identifier that nothing outside the registry's own
|
|
files (registry.rs, its export, REGISTRY.md) uses, so the list stays the set of
|
|
strings in use;
|
|
- a `wai.quantum.*` string the registry holds twice.
|
|
|
|
check-wai-quantum-names.py [--root DIR]
|
|
|
|
Exit 0 when every string is accounted for, 1 with one line a finding otherwise, 2 when
|
|
the tree cannot be read. Standard library only.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
PREFIX = "wai.quantum."
|
|
# A string of the namespace, with its revision and a trailing `.*` (a pattern).
|
|
TOKEN = re.compile(
|
|
r"wai\.quantum\.([A-Za-z0-9_][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*)*)"
|
|
r"(/[A-Za-z0-9_-]+)?(\.\*)?"
|
|
)
|
|
EXPORT = "wai/registry/capabilities.json"
|
|
# The registry's own files: the table, its export and the page that prints it.
|
|
REGISTRY_FILES = {"wai/wai-rs/src/codecs/registry.rs", EXPORT, "wai/REGISTRY.md"}
|
|
# The record of past changes, read by nothing here.
|
|
HISTORY = ("wai/CHANGELOG.md", "wai/changes/")
|
|
SCHEMA_FILE = re.compile(r"(?:^|/)schemas/([a-z0-9_-]+(?:\.[a-z0-9_-]+)*)\.v([0-9]+)\.json$")
|
|
|
|
|
|
def fail(msg):
|
|
print(f"check-wai-quantum-names: {msg}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
|
|
def git(root, *args):
|
|
r = subprocess.run(["git", "-C", root, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
return r.returncode, r.stdout.decode("utf-8", "replace"), r.stderr.decode("utf-8", "replace")
|
|
|
|
|
|
def occurrences(root):
|
|
"""Every string of the namespace in the text files git tracks or would track:
|
|
[(path, line, string, is_pattern)]."""
|
|
rc, out, err = git(root, "grep", "-I", "-n", "-z", "--untracked", "-F", "-e", PREFIX)
|
|
if rc == 1:
|
|
return []
|
|
if rc != 0:
|
|
fail(f"git grep failed ({rc}): {err.strip()}")
|
|
found = []
|
|
for rec in out.split("\n"):
|
|
if not rec:
|
|
continue
|
|
parts = rec.split("\0", 2)
|
|
if len(parts) != 3:
|
|
fail(f"unreadable git grep line: {rec[:120]!r}")
|
|
path, line, text = parts
|
|
if path == HISTORY[0] or path.startswith(HISTORY[1]):
|
|
continue
|
|
for m in TOKEN.finditer(text):
|
|
name = PREFIX + m.group(1) + (m.group(2) or "")
|
|
found.append((path, int(line), name, bool(m.group(3))))
|
|
return found
|
|
|
|
|
|
def registry(root):
|
|
"""The strings the export holds, by table, and the quantum strings it holds twice."""
|
|
path = os.path.join(root, EXPORT)
|
|
try:
|
|
with open(path, encoding="utf-8") as fh:
|
|
doc = json.load(fh)
|
|
except (OSError, ValueError) as e:
|
|
fail(f"cannot read {EXPORT}: {e}")
|
|
tables = {
|
|
"registered": [c["capability"] for c in doc.get("capabilities", [])],
|
|
"identifier": [i["identifier"] for i in doc.get("identifiers", [])],
|
|
"label": [l["label"] for l in doc.get("unregistered_labels", [])],
|
|
"wire identifier": [w["name"] for w in doc.get("wire_identifiers", [])],
|
|
}
|
|
if not tables["registered"]:
|
|
fail(f"{EXPORT} registers nothing")
|
|
held = {}
|
|
twice = []
|
|
for table, names in tables.items():
|
|
for n in names:
|
|
if n.startswith(PREFIX):
|
|
if n in held:
|
|
twice.append(f"{EXPORT}: {n} is held twice ({held[n]}, {table})")
|
|
held.setdefault(n, table)
|
|
return tables, held, twice
|
|
|
|
|
|
def schema_names(root):
|
|
"""The document names the quantum schema files give, and the findings about them."""
|
|
rc, out, err = git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "wai/")
|
|
if rc != 0:
|
|
fail(f"git ls-files failed ({rc}): {err.strip()}")
|
|
names, findings = {}, []
|
|
|
|
def consts(v, acc):
|
|
if isinstance(v, dict):
|
|
for k, x in v.items():
|
|
if k == "const" and isinstance(x, str):
|
|
acc.append(x)
|
|
else:
|
|
consts(x, acc)
|
|
elif isinstance(v, list):
|
|
for x in v:
|
|
consts(x, acc)
|
|
|
|
for path in sorted(p for p in out.split("\0") if p):
|
|
m = SCHEMA_FILE.search(path)
|
|
if not m or not os.path.isfile(os.path.join(root, path)):
|
|
continue
|
|
try:
|
|
with open(os.path.join(root, path), encoding="utf-8") as fh:
|
|
doc = json.load(fh)
|
|
except (OSError, ValueError) as e:
|
|
findings.append(f"{path}: not a JSON document ({e})")
|
|
continue
|
|
title = doc.get("title") if isinstance(doc, dict) else None
|
|
if not (isinstance(title, str) and title.startswith(PREFIX)):
|
|
continue
|
|
want = f"{PREFIX}{m.group(1)}/{m.group(2)}"
|
|
if title != want:
|
|
findings.append(f"{path}: its title is {title}, but a schema of that file is named {want}")
|
|
continue
|
|
names.setdefault(title, path)
|
|
acc = []
|
|
consts(doc, acc)
|
|
for c in acc:
|
|
t = TOKEN.fullmatch(c)
|
|
if t and not t.group(3):
|
|
names.setdefault(c, path)
|
|
return names, findings
|
|
|
|
|
|
def main(argv):
|
|
root = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
if len(argv) == 2 and argv[0] == "--root":
|
|
root = os.path.abspath(argv[1])
|
|
elif argv:
|
|
fail("usage: check-wai-quantum-names.py [--root DIR]")
|
|
tables, held, findings = registry(root)
|
|
accounted = {n for names in tables.values() for n in names}
|
|
schemas, schema_findings = schema_names(root)
|
|
findings += schema_findings
|
|
accounted |= set(schemas)
|
|
|
|
used_outside = set()
|
|
seen = 0
|
|
for path, line, name, pattern in occurrences(root):
|
|
seen += 1
|
|
if path not in REGISTRY_FILES:
|
|
used_outside.add(name)
|
|
if pattern:
|
|
stem = name + "."
|
|
if not any(a.startswith(stem) for a in accounted):
|
|
findings.append(f"{path}:{line}: {name}.* matches no registered string, label or schema's name")
|
|
continue
|
|
if name not in accounted:
|
|
findings.append(f"{path}:{line}: {name} is not registered, listed as a label or wire identifier, "
|
|
f"or the name of a quantum schema file")
|
|
for n, table in sorted(held.items()):
|
|
if table in ("label", "wire identifier") and n not in used_outside:
|
|
findings.append(f"{EXPORT}: the {table} {n} is used nowhere outside the registry's own files")
|
|
|
|
if findings:
|
|
for f in findings:
|
|
print(f)
|
|
sys.stdout.flush()
|
|
print(f"\n{len(findings)} finding(s). A new `wai.quantum.*` string is registered, or listed with what it "
|
|
"names in UNREGISTERED_LABELS (wai/wai-rs/src/codecs/registry.rs, beside its group), then "
|
|
"`wai registry export` is run; a document's name is its schema file "
|
|
"(`schemas/<document>.v<major>.json`, its `title` the name).", file=sys.stderr)
|
|
return 1
|
|
print(f"check-wai-quantum-names: every wai.quantum string is accounted for "
|
|
f"({seen} occurrences read; {len(schemas)} named by schema files)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|