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.
378 lines
16 KiB
Python
378 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""One word per thing, checked (ADR 0244).
|
|
|
|
The glossary (00-META/glossary.md) is the authority on the mesh's words. It names the words it
|
|
retired on one fixed kind of line, so a program can read them:
|
|
|
|
*Not:* ~~control plane~~, ~~overlay~~ (hq)
|
|
|
|
A struck word with no scope is retired everywhere; `(hq)` retires it in this repository's prose only;
|
|
`(tools)` in the descriptions of the mesh's tools only. And it names code that still carries an old
|
|
name until that code is renamed:
|
|
|
|
*Identifier until renamed:* `mesh-host` — the repository, binary and service unit
|
|
|
|
An identifier is allowed inside a code span and nowhere else.
|
|
|
|
What is enforced:
|
|
|
|
retired no retired word of scope `hq` (or none), and no identifier, in running prose of a
|
|
checked document. Running prose is what is left once code blocks, code spans, block
|
|
quotes, text in quotation marks, struck-through text, link targets, HTML comments and
|
|
frontmatter are taken out: a quotation keeps the words it quotes, a link target is a file
|
|
name, and an identifier lives in a code span. A word's plural is the word: "alerts" fails
|
|
as "alert" does.
|
|
unique no head word (a bolded word opening a glossary entry) heads two entries, and no head
|
|
word is also struck through on a *Not:* line.
|
|
tools list with `--list tools` it prints the words retired in the tools' descriptions, which is the
|
|
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
|
|
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
|
|
|
|
lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09). The operator
|
|
types one command, `nox` (`mesh-cli`); the node-engine (`nox-mesh-host`) and the
|
|
controller (`mesh-controller`) are daemons, and a person never types their names — so a
|
|
document names a `nox` line, never `mesh-controller push` or `mesh-host hand-over`. The
|
|
lines are DAEMON_LINES below: each daemon's commands, and which of them are a line only
|
|
with an argument after them ("the mesh-controller module" is prose; `mesh-controller
|
|
module check …` is a line). A line is looked for in the document's own words — code
|
|
blocks and code spans included, since that is where a line to type is written — with
|
|
quotations, block quotes, link targets and comments taken out. `--list lines` prints the
|
|
list, which the catalogue's own check keeps a copy of (mesh-catalog `retired-lines`); with
|
|
MESH_CATALOG_DIR set it also fails when that copy differs. The node-engine's install-time
|
|
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
|
|
typed at a machine no mesh reaches yet, and the engine's usage says so.
|
|
|
|
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; an issue opened
|
|
on or after FROM (on or after LINES_FROM for the lines). The decision records and research efforts live in
|
|
the knowledge base (ADR 0290, issue 426), which holds a record proposed there to the same words through its
|
|
rules `retired-word` and `daemon-line`, read from this glossary.
|
|
|
|
A document the check fails on that cannot be reworded in the change that found it is named, with a date
|
|
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
|
|
an entry for a document that no longer needs it fails too.
|
|
|
|
python3 00-META/checks/words.py
|
|
python3 00-META/checks/words.py --list tools
|
|
python3 00-META/checks/words.py --list lines
|
|
"""
|
|
|
|
import datetime
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
GLOSSARY = "00-META/glossary.md"
|
|
ALLOWED = "00-META/checks/words-allowed.md"
|
|
|
|
# The day the rule began (ADR 0244): issues opened from it on are held to it.
|
|
FROM = "2026-10-07"
|
|
|
|
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
|
|
|
|
# The daemons' lines (ADR 0272, its insight of 2026-10-09), and the day that rule began. "alone" is a
|
|
# command that is a line by itself; "with an argument" one that is a line only when written as one.
|
|
LINES_FROM = "2026-10-09"
|
|
DAEMON_LINES = {
|
|
"mesh-controller": {
|
|
"alone": ("adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act "
|
|
"hand-acts healers identity kill licence migrate operator pause pin plan prepare push rebuild "
|
|
"rehearse replay resume retire rollout rotate secret settings status token unassign unpin "
|
|
"upgrade").split(),
|
|
"with an argument": ("artifacts ask builds bus collection data delivery images mirrors module node "
|
|
"overlay plans queue seat seats take version").split(),
|
|
},
|
|
"mesh-host": {
|
|
"alone": "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version".split(),
|
|
"with an argument": [],
|
|
},
|
|
"nox-mesh-host": {"alone": ["hand-over"], "with an argument": []},
|
|
}
|
|
|
|
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
|
|
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
|
|
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
|
|
CODE_SPAN = re.compile(r"`([^`]+)`")
|
|
HEAD = re.compile(r"^\s*-\s+\*\*([^*]+)\*\*", re.M)
|
|
|
|
|
|
def rel(path):
|
|
return os.path.relpath(path, ROOT)
|
|
|
|
|
|
def read(path):
|
|
with open(os.path.join(ROOT, path), encoding="utf-8") as handle:
|
|
return handle.read()
|
|
|
|
|
|
def frontmatter_field(text, name):
|
|
if not text.startswith("---\n"):
|
|
return None
|
|
end = text.find("\n---", 4)
|
|
m = re.search(r"^%s:\s*(\S+)" % name, text[4:end], re.M)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def glossary():
|
|
"""The retired words with their scope, the identifiers, and the head words."""
|
|
text = read(GLOSSARY)
|
|
retired = []
|
|
for line in NOT_LINE.findall(text):
|
|
for word, scope in STRUCK.findall(line):
|
|
for one in word.split(" / "):
|
|
retired.append((one.strip(), scope or "all"))
|
|
identifiers = []
|
|
for line in IDENT_LINE.findall(text):
|
|
identifiers += [i.strip() for i in CODE_SPAN.findall(line)]
|
|
heads = []
|
|
for head in HEAD.findall(text):
|
|
heads += [h.strip() for h in head.split(" / ")]
|
|
return retired, identifiers, heads
|
|
|
|
|
|
def pattern(word):
|
|
"""A whole-word, case-insensitive match; a space in the word matches a space, a line break or a hyphen.
|
|
|
|
The plural is the word too: its last part may end in `s` or `es` ("alerts", "control planes"), since
|
|
a retired word does not come back by being counted. Anything else joined on is another word.
|
|
"""
|
|
parts = [re.escape(p) for p in word.split()]
|
|
return re.compile(r"(?i)(?<![\w-])" + r"[\s-]+".join(parts) + r"(?:e?s)?(?![\w-])")
|
|
|
|
|
|
def blank(match):
|
|
return re.sub(r"[^\n]", " ", match.group(0))
|
|
|
|
|
|
def prose(text, glossary_file=False):
|
|
"""The text with everything that is not running prose blanked, keeping offsets and line numbers."""
|
|
if text.startswith("---\n"):
|
|
end = text.find("\n---", 4)
|
|
if end != -1:
|
|
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
|
|
steps = [
|
|
r"(?ms)^\s*```.*?^\s*```", # code blocks
|
|
r"(?s)<!--.*?-->", # comments
|
|
r"(?m)^\s*>.*$", # block quotes
|
|
r"`[^`\n]+`", # code spans
|
|
r"\]\([^)\s]*\)", # link targets
|
|
r"~~[^~\n]+~~", # struck through: a word named as retired
|
|
r"\"[^\"\n]*\"", # "quoted"
|
|
r"\u201c[^\u201d]*\u201d", # curly double quotes
|
|
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
|
|
]
|
|
if glossary_file:
|
|
steps[:0] = [NOT_LINE.pattern, IDENT_LINE.pattern]
|
|
for step in steps:
|
|
text = re.sub(step, blank, text)
|
|
return text
|
|
|
|
|
|
def issues(since=FROM):
|
|
"""The issue reports opened on or after `since`, file by file."""
|
|
out = []
|
|
base = os.path.join(ROOT, "04-ISSUES")
|
|
if not os.path.isdir(base):
|
|
return out
|
|
for issue in sorted(os.listdir(base)):
|
|
head = os.path.join(base, issue, "00-report.md")
|
|
if not os.path.isfile(head):
|
|
continue
|
|
date = frontmatter_field(read(rel(head)), "opened")
|
|
if not date or date < since:
|
|
continue
|
|
for name in sorted(os.listdir(os.path.join(base, issue))):
|
|
if name.endswith(".md"):
|
|
out.append("04-ISSUES/%s/%s" % (issue, name))
|
|
return out
|
|
|
|
|
|
def checked_documents(since=FROM):
|
|
docs = []
|
|
for entry in ALWAYS:
|
|
path = os.path.join(ROOT, entry)
|
|
if os.path.isfile(path):
|
|
docs.append(entry)
|
|
continue
|
|
for base, dirs, files in os.walk(path):
|
|
dirs.sort()
|
|
for name in sorted(files):
|
|
if name.endswith(".md"):
|
|
docs.append(rel(os.path.join(base, name)))
|
|
return docs + issues(since)
|
|
|
|
|
|
def allowed():
|
|
"""words-allowed.md: one row per document — | `document` | until | why |.
|
|
|
|
`until` is a date, after which the allowance fails like the words themselves.
|
|
"""
|
|
out, problems = {}, []
|
|
if not os.path.isfile(os.path.join(ROOT, ALLOWED)):
|
|
return out, problems
|
|
today = datetime.date.today().isoformat()
|
|
for line in read(ALLOWED).splitlines():
|
|
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
if len(cells) != 3 or not cells[0].startswith("`"):
|
|
continue
|
|
doc, until, why = cells[0].strip("`"), cells[1], cells[2]
|
|
if not why:
|
|
problems.append("%s: the allowance for %s gives no reason" % (ALLOWED, doc))
|
|
if not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
|
|
problems.append("%s: the allowance for %s has no date" % (ALLOWED, doc))
|
|
continue
|
|
elif until < today:
|
|
problems.append("%s: the allowance for %s ran out on %s — reword it" % (ALLOWED, doc, until))
|
|
continue
|
|
out[doc] = until
|
|
return out, problems
|
|
|
|
|
|
def check_retired(retired, identifiers):
|
|
problems = []
|
|
allowance, problems_allowed = allowed()
|
|
problems += problems_allowed
|
|
words = [(w, pattern(w), "retired") for w, scope in retired if scope in ("all", "hq")]
|
|
words += [(i, pattern(i), "an identifier, allowed only in a code span") for i in identifiers]
|
|
used = set()
|
|
for doc in checked_documents():
|
|
text = prose(read(doc), glossary_file=(doc == GLOSSARY))
|
|
for word, rx, why in words:
|
|
for m in rx.finditer(text):
|
|
if doc in allowance:
|
|
used.add(doc)
|
|
continue
|
|
line = text.count("\n", 0, m.start()) + 1
|
|
problems.append("%s:%d: %r is %s (glossary)" % (doc, line, m.group(0).replace("\n", " "), why))
|
|
for doc in allowance:
|
|
if doc not in used:
|
|
problems.append("%s: %s no longer uses a retired word — remove its allowance" % (ALLOWED, doc))
|
|
return problems
|
|
|
|
|
|
def lines_list():
|
|
"""The daemons' lines as the catalogue's copy lists them: `<daemon> <command>`, ` …` when an argument must follow."""
|
|
out = []
|
|
for daemon, kinds in DAEMON_LINES.items():
|
|
out += ["%s %s" % (daemon, c) for c in kinds["alone"]]
|
|
out += ["%s %s …" % (daemon, c) for c in kinds["with an argument"]]
|
|
return sorted(out)
|
|
|
|
|
|
def line_pattern(daemon, command, with_argument):
|
|
"""The line whole: the daemon, white space, the command. One that is a line only with an argument is matched
|
|
as a line is written — at the start of a line or a code span, a prompt allowed, an argument after it."""
|
|
d, c = re.escape(daemon), re.escape(command)
|
|
if with_argument:
|
|
return re.compile(r"(?m)(?:^|`)[ \t]*(?:\$[ \t]*)?(" + d + r"\s+" + c + r"\s+[^\s`])")
|
|
return re.compile(r"(?<![\w-])" + d + r"\s+" + c + r"(?![\w-])")
|
|
|
|
|
|
def own_words(text):
|
|
"""The text with what is not the document's own words blanked: as prose(), but a code block and a code span
|
|
are kept, since a line to type is written in one."""
|
|
if text.startswith("---\n"):
|
|
end = text.find("\n---", 4)
|
|
if end != -1:
|
|
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
|
|
for step in (
|
|
r"(?s)<!--.*?-->", # comments
|
|
r"(?m)^\s*>.*$", # block quotes
|
|
r"\]\([^)\s]*\)", # link targets
|
|
r"~~[^~\n]+~~", # struck through
|
|
r"\"[^\"\n]*\"", # "quoted"
|
|
r"\u201c[^\u201d]*\u201d", # curly double quotes
|
|
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
|
|
):
|
|
text = re.sub(step, blank, text)
|
|
return text
|
|
|
|
|
|
def check_lines():
|
|
problems = []
|
|
patterns = []
|
|
for daemon, kinds in DAEMON_LINES.items():
|
|
patterns += [(daemon, c, line_pattern(daemon, c, False)) for c in kinds["alone"]]
|
|
patterns += [(daemon, c, line_pattern(daemon, c, True)) for c in kinds["with an argument"]]
|
|
for doc in checked_documents(LINES_FROM):
|
|
text = own_words(read(doc))
|
|
for daemon, command, rx in patterns:
|
|
for m in rx.finditer(text):
|
|
line = text.count("\n", 0, m.start()) + 1
|
|
problems.append("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)"
|
|
% (doc, line, daemon, command))
|
|
return problems
|
|
|
|
|
|
def check_unique(retired, heads):
|
|
problems = []
|
|
seen = {}
|
|
for head in heads:
|
|
key = head.lower()
|
|
if key in seen:
|
|
problems.append("%s: %r heads two entries" % (GLOSSARY, head))
|
|
seen[key] = True
|
|
for word, _ in retired:
|
|
if word.lower() in seen:
|
|
problems.append("%s: %r is a head word and also retired" % (GLOSSARY, word))
|
|
return problems
|
|
|
|
|
|
def tools_list(retired):
|
|
return sorted({w for w, scope in retired if scope in ("all", "tools")}, key=str.lower)
|
|
|
|
|
|
def check_copy(retired):
|
|
catalogue = os.environ.get("MESH_CATALOG_DIR")
|
|
if not catalogue:
|
|
print("NOT COMPARED: MESH_CATALOG_DIR is not set, so the catalogue's copy of the list was not read")
|
|
return []
|
|
path = os.path.join(catalogue, "retired-words")
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
|
|
except OSError as err:
|
|
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
|
|
want = tools_list(retired)
|
|
problems = []
|
|
if sorted(copy, key=str.lower) != want:
|
|
problems.append("the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want))
|
|
path = os.path.join(catalogue, "retired-lines")
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
|
|
except OSError as err:
|
|
return problems + ["the catalogue's copy of the daemons' lines cannot be read: %s" % err]
|
|
if sorted(copy) != lines_list():
|
|
problems.append("the catalogue's retired-lines differs from this check's list: regenerate it with "
|
|
"`python3 00-META/checks/words.py --list lines`")
|
|
return problems
|
|
|
|
|
|
def main(argv):
|
|
retired, identifiers, heads = glossary()
|
|
if argv[1:2] == ["--list"]:
|
|
scope = argv[2] if len(argv) > 2 else "tools"
|
|
if scope == "lines":
|
|
words = lines_list()
|
|
else:
|
|
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
|
|
print("\n".join(words))
|
|
return 0
|
|
if not retired:
|
|
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
|
return 1
|
|
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_lines() + check_copy(retired)
|
|
for p in problems:
|
|
print(p)
|
|
if problems:
|
|
print("words: %d problem(s)" % len(problems))
|
|
return 1
|
|
print("words: %d retired words and %d identifiers, none in running prose; %d daemons' lines, none written; "
|
|
"%d head words, each once" % (len(retired), len(identifiers), len(lines_list()), len(heads)))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|