mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery superseded: a newer head of the same pull request
ADR 0290 rule 9: once the import is checked record by record, hq keeps no copy. The records and efforts are removed; every link or path to them reads "ADR NNNN" or "research NNN"; a design's decisions: lists "ADR NNNN". 02-DECISIONS keeps a pointer README with the reading order's six topics, which the knowledge base reads through the records module; 01-RESEARCH keeps a pointer README. The checks drop the rules that judged records (the knowledge base holds each at propose; checks/README.md says where) and refuse a record written here again. Playbooks 01 and 02 and the skills write and read records with the knowledge base's verbs.
228 lines
9.0 KiB
Python
228 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Structural checks over HQ's own documents.
|
|
|
|
Every check here exists because the thing it checks for actually happened. See README.md
|
|
for which incident is behind which check. Run from the repository root:
|
|
|
|
python3 00-META/checks/records.py
|
|
|
|
Exits non-zero if anything fails, so it can be a gate rather than a report.
|
|
|
|
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
|
|
rules that judged them here are the knowledge base's now: `propose` refuses a record that breaks one.
|
|
README.md beside this file says where each rule went. What stays here judges what this repository
|
|
still holds, and keeps the records from coming back.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
|
|
|
|
# The two folders the records left. Each keeps a README that points at the knowledge base, and
|
|
# 02-DECISIONS/README.md also writes the reading order's topics, which the knowledge base reads.
|
|
RECORD_FOLDERS = ("02-DECISIONS", "01-RESEARCH")
|
|
POINTERS = {"02-DECISIONS/README.md", "01-RESEARCH/README.md"}
|
|
|
|
# A decision record is named "ADR NNNN", four digits, and a research effort "research NNN", three.
|
|
DECISION_ENTRY = re.compile(r"^ADR \d{4}$")
|
|
ADR_MENTION = re.compile(r"\bADRs?\s+(\d+)\b")
|
|
RESEARCH_MENTION = re.compile(r"\b[Rr]esearch\s+(\d+)\b")
|
|
|
|
|
|
def markdown_files():
|
|
for base, dirs, files in os.walk(ROOT):
|
|
dirs[:] = [d for d in dirs if d not in (".git", ".claude")]
|
|
for name in sorted(files):
|
|
if name.endswith(".md"):
|
|
yield os.path.join(base, name)
|
|
|
|
|
|
def rel(path):
|
|
return os.path.relpath(path, ROOT)
|
|
|
|
|
|
def read(path):
|
|
with open(path, encoding="utf-8") as handle:
|
|
return handle.read()
|
|
|
|
|
|
def frontmatter(text):
|
|
"""Minimal frontmatter reader — enough for the fields these checks use.
|
|
|
|
Not a YAML parser on purpose: a dependency in a repository that has none, to read a few
|
|
scalar fields and one list, would cost more than it returns.
|
|
"""
|
|
if not text.startswith("---\n"):
|
|
return {}
|
|
end = text.find("\n---", 4)
|
|
if end == -1:
|
|
return {}
|
|
fields, key = {}, None
|
|
for line in text[4:end].split("\n"):
|
|
item = re.match(r"^\s+-\s+(.*)$", line)
|
|
if item and key:
|
|
fields.setdefault(key, []).append(item.group(1).strip())
|
|
continue
|
|
pair = re.match(r"^([A-Za-z_-]+):\s*(.*)$", line)
|
|
if pair:
|
|
key = pair.group(1)
|
|
value = pair.group(2).strip()
|
|
if value.startswith("[") and value.endswith("]"):
|
|
# inline list: `decisions: [a, b]`, `code: []`
|
|
inner = value[1:-1].strip()
|
|
fields[key] = [v.strip() for v in inner.split(",") if v.strip()]
|
|
elif value:
|
|
fields[key] = value
|
|
else:
|
|
fields[key] = []
|
|
return fields
|
|
|
|
|
|
def unquote(value):
|
|
value = value.strip()
|
|
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
return value[1:-1]
|
|
return value
|
|
|
|
|
|
class Failures:
|
|
def __init__(self):
|
|
self.items = []
|
|
|
|
def add(self, check, location, message):
|
|
self.items.append((check, location, message))
|
|
|
|
def report(self):
|
|
if not self.items:
|
|
print("records: all checks passed")
|
|
return 0
|
|
by_check = {}
|
|
for check, location, message in self.items:
|
|
by_check.setdefault(check, []).append((location, message))
|
|
for check in sorted(by_check):
|
|
print(f"\n{check} — {len(by_check[check])} problem(s)")
|
|
for location, message in by_check[check]:
|
|
print(f" {location}\n {message}")
|
|
print(f"\nrecords: {len(self.items)} problem(s)")
|
|
return 1
|
|
|
|
|
|
def check_links(failures):
|
|
"""Every relative link resolves to something that exists."""
|
|
for path in markdown_files():
|
|
folder = os.path.dirname(path)
|
|
for number, line in enumerate(read(path).split("\n"), 1):
|
|
for target in LINK.findall(line):
|
|
target = target.split("#")[0].strip()
|
|
if not target:
|
|
continue
|
|
if not os.path.exists(os.path.normpath(os.path.join(folder, target))):
|
|
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
|
|
|
|
|
|
def check_no_records(failures):
|
|
"""No decision record or research effort is written into this repository again (ADR 0290 rule 9).
|
|
|
|
The knowledge base is the only place a record is read; a file here would be a second copy that
|
|
drifts from it, and nothing would read it. The folders keep their pointer READMEs and nothing else.
|
|
"""
|
|
for folder in RECORD_FOLDERS:
|
|
base = os.path.join(ROOT, folder)
|
|
if not os.path.isdir(base):
|
|
failures.add("no-records", folder, "the folder is gone; it keeps a README that points at the "
|
|
"knowledge base (and, for 02-DECISIONS, the reading order the knowledge base reads)")
|
|
continue
|
|
for current, dirs, files in os.walk(base):
|
|
for name in sorted(files + [d + "/" for d in dirs]):
|
|
path = rel(os.path.join(current, name))
|
|
if path.rstrip("/") in POINTERS:
|
|
continue
|
|
failures.add(
|
|
"no-records",
|
|
path,
|
|
"a decision record or research effort lives in the knowledge base, never here: "
|
|
"write it with knowledge-base.reserve and knowledge-base.propose (playbooks 01 and 02)",
|
|
)
|
|
|
|
|
|
def check_decisions_entries(failures):
|
|
"""A design's `decisions:` names each record as "ADR NNNN".
|
|
|
|
A to-be design rests on the records it names, and the knowledge base answers for a record by its
|
|
number; an entry in another form is a citation nothing can follow.
|
|
"""
|
|
for path in markdown_files():
|
|
if not rel(path).startswith("03-DESIGN/"):
|
|
continue
|
|
for entry in frontmatter(read(path)).get("decisions", []) or []:
|
|
if not DECISION_ENTRY.match(unquote(entry)):
|
|
failures.add("decisions-form", rel(path),
|
|
f"decisions: entry {entry!r} is not 'ADR NNNN' (four digits)")
|
|
|
|
|
|
def check_mentions(failures):
|
|
"""In running text, a decision record is "ADR NNNN" (four digits) and a research effort "research NNN"
|
|
(three): the form the knowledge base and the issue tracker read a citation in. A code span is an example,
|
|
and is not read."""
|
|
for path in markdown_files():
|
|
text = read(path)
|
|
for number, line in enumerate(text.split("\n"), 1):
|
|
line = re.sub(r"`[^`]*`", "", line)
|
|
for m in ADR_MENTION.finditer(line):
|
|
if len(m.group(1)) != 4:
|
|
failures.add("mention-form", f"{rel(path)}:{number}",
|
|
f"{m.group(0)!r}: a decision record is named 'ADR NNNN', four digits")
|
|
for m in RESEARCH_MENTION.finditer(line):
|
|
if len(m.group(1)) != 3:
|
|
failures.add("mention-form", f"{rel(path)}:{number}",
|
|
f"{m.group(0)!r}: a research effort is named 'research NNN', three digits")
|
|
|
|
|
|
def check_status_against_code(failures):
|
|
"""A design document naming specific code may not still call itself `designed`.
|
|
|
|
**Naming a file is a claim that the file implements this**, so the two fields have to agree.
|
|
They drifted: ten to-be documents named working, lab-proven code — several with a *What was
|
|
built* or *Raised, and observed* section — while still saying nothing had been built.
|
|
|
|
Deliberately weak, and that is the point of it being mechanical. It cannot tell whether the
|
|
prose is true, only that a document has stopped claiming to be unbuilt once it points at
|
|
something. `code: [mesh-controller]` — a repository with no path — is a plan and stays
|
|
`designed`.
|
|
"""
|
|
for path in markdown_files():
|
|
if not rel(path).startswith("03-DESIGN/01-to-be/") or path.endswith("README.md"):
|
|
continue
|
|
front = frontmatter(read(path))
|
|
if front.get("status") != "designed":
|
|
continue
|
|
for entry in front.get("code") or []:
|
|
named = re.sub(r"\s*\(.*\)$", "", entry).strip().split(None, 1)
|
|
if len(named) > 1:
|
|
failures.add(
|
|
"status-vs-code",
|
|
rel(path),
|
|
f"`designed`, but names {named[1]!r} in {named[0]}. Naming a file claims "
|
|
f"it implements this — use `in-progress`, or `implemented` once it is "
|
|
f"defensible from that repository's main branch.",
|
|
)
|
|
break
|
|
|
|
|
|
def main():
|
|
failures = Failures()
|
|
check_links(failures)
|
|
check_no_records(failures)
|
|
check_decisions_entries(failures)
|
|
check_mentions(failures)
|
|
check_status_against_code(failures)
|
|
return failures.report()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|