Delete merge-check.sh and the four Python checks; the Go tests replace them (issue 1296, H6)

This commit is contained in:
2026-10-24 03:39:11 +02:00
parent 82aa3f6afa
commit 6dc008b5e7
18 changed files with 64 additions and 931 deletions
+30 -31
View File
@@ -1,20 +1,19 @@
# Checks
Since ADR 0527 the same four checks are also Go tests, in this folder (`*_test.go`) with `go.mod` at the root: run
`go test ./...` from the root. Each check is a function over a file tree that returns its problems, tested on hq's tree
and on a deliberately broken tree built in memory. The Python files and `merge-check.sh` run beside them until the
delivery of R4g is done and hq's trunk requires `mesh/check` (ADR 0527 §3); then they go (issue 1296).
The four checks are Go tests (ADR 0527), in this folder (`*_test.go`) with `go.mod` at the root: run
`go test ./...` from the root. The checker runs the same as `mesh/check` on every pull request. Each check is a
function over a file tree that returns its problems, tested on hq's tree and on a deliberately broken tree built
in memory. The Python files and `merge-check.sh` are gone (issue 1296).
```
python3 00-META/checks/records.py structure: links, no record written here, citations in the form "ADR NNNN"
python3 00-META/checks/index.py the reading order's topics are written, and no list of records is stored
python3 00-META/checks/cycle.py the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
records_test.go structure: links, no record written here, citations in the form "ADR NNNN"
index_test.go the reading order's topics are written, and no list of records is stored
cycle_test.go the development cycle: a design's layer, status, decisions and code; an issue report's frontmatter
words_test.go the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
```
Non-zero exit on any problem, so it can be a gate rather than a report.
A failing test fails `go test`, so it can be a gate rather than a report. With `MESH_CATALOG_DIR` set, the words
test also compares the catalogue's copies of the retired words and the daemons' lines with the glossary.
**Why this exists.** Until now nothing in this repository was verified by anything but reading,
which is how a superseded decision stayed live in the constitution for days and in
@@ -31,7 +30,7 @@ indistinguishable from one that cannot.
| `no-records` | `02-DECISIONS/` and `01-RESEARCH/` hold their README and nothing else: a decision record or research effort lives in the knowledge base (ADR 0290 rule 9) | written with the change that removed the records (issue 426); it failed on a record file put back in `02-DECISIONS/` before it passed |
| `decisions-form` | every entry of a design's `decisions:` is "ADR NNNN", four digits | written with issue 426; it failed on an entry `ADR 290` before it passed |
| `mention-form` | every "ADR N" in the text has four digits, and every "research N" three; a code span is an example and is not read | written with issue 426; it failed on `ADR 12` and `research 45` before it passed |
| *(index.py)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| *(index test)* | the reading order's six topics are written in `02-DECISIONS/README.md`, none twice, and the README stores no list of records (ADR 0248). The knowledge base reads these lines and refuses a record whose topic is not one of them | a stored list made every two open decisions conflict ([issue 297](../../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)); it failed on the README of `main` before the list was removed |
| `status-vs-code` | a to-be document naming specific code is not still `designed` | **ten documents**, several with a *What was built* section, describing lab-proven code |
## Where the rules on records went
@@ -44,22 +43,22 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
| Rule here until issue 426 | Where it lives now |
|---|---|
| `records.py` `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
| `records.py` `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
| `records.py` `live-citation`, in a record | the knowledge base's `live-citation` |
| `records.py` `live-citation`, in `00-META/` and `03-DESIGN/01-to-be/` | **nowhere yet**, as above: the reviewer checks a cited record's status with `knowledge-base.get`; issue 428 |
| `records.py` `supersession` | the knowledge base's `supersedes-accepted` and `self-link`, and the whole-set rule `supersession` |
| `records.py` `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
| `records.py` `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
| `records.py` `numbering` | the knowledge base's `title` and `reserved` |
| `records.py` `topics`, `index.py` (a record's topic) | the knowledge base's `topic`, against the reading order `index.py` still checks |
| `records.py` `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
| The records test's `rests-on`, for a record's `extends:` | the knowledge base's `rests-on` at propose, and the whole-set rule `rests-on-live` |
| The records test's `rests-on`, for a design's `decisions:` naming an accepted record | **nowhere yet**: this repository cannot read the knowledge base. The reviewer checks each new entry with `knowledge-base.get`. Designs become the knowledge base's own kind under issue 428 |
| The records test's `live-citation`, in a record | the knowledge base's `live-citation` |
| The records test's `live-citation`, in `00-META/` and `03-DESIGN/01-to-be/` | **nowhere yet**, as above: the reviewer checks a cited record's status with `knowledge-base.get`; issue 428 |
| The records test's `supersession` | the knowledge base's `supersedes-accepted` and `self-link`, and the whole-set rule `supersession` |
| The records test's `partial-supersession` | the knowledge base's `changes` link to a clause, `changes-in-force` and `supersedes-accepted`, and the whole-set rule `partial` |
| The records test's `clauses` | the knowledge base's `has-clause`, `clause-numbering`, `clause-lead` and `clause-size` |
| The records test's `numbering` | the knowledge base's `title` and `reserved` |
| The records test's `topics`, The index test's (a record's topic) | the knowledge base's `topic`, against the reading order The index test's still checks |
| The records test's `insights` | the knowledge base's `insight-form`; an accepted record changes by a new version marked as a progressive insight |
| `index.py --print` | `knowledge-base.holds` with a topic |
| `cycle.py` research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
| `cycle.py` no two decision records share a number | the knowledge base's `reserved`, and the issue tracker's counter behind `knowledge-base.reserve` |
| `cycle.py` an accepted decision cited by nothing | the knowledge base's whole-set rule `cited`: a record accepted there is cited by another record or names a home (`homes` at propose) |
| `cycle.py` a deferral names the issue that tracks it | the knowledge base's `deferral` |
| `words.py` over research efforts from 2026-10-07, and `kept` in `words-allowed.md` | the knowledge base's `retired-word` and `daemon-line`, from this glossary |
| the cycle test research: a known status, `became:` when graduated | the knowledge base's `research-status`, `overview` and `became` |
| the cycle test no two decision records share a number | the knowledge base's `reserved`, and the issue tracker's counter behind `knowledge-base.reserve` |
| the cycle test an accepted decision cited by nothing | the knowledge base's whole-set rule `cited`: a record accepted there is cited by another record or names a home (`homes` at propose) |
| the cycle test a deferral names the issue that tracks it | the knowledge base's `deferral` |
| the words test over research efforts from 2026-10-07, and `kept` in `words-allowed.md` | the knowledge base's `retired-word` and `daemon-line`, from this glossary |
## What is deliberately not checked
@@ -77,7 +76,7 @@ acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, on
State what incident it would have caught, and make it fail before you make it pass. A check
whose failure has never been observed is a guess about its own correctness.
## cycle.py
## The cycle test
The development cycle, checked (ADR 0080):
every design parses, its `layer:` matches its folder and its `status:` is known; a to-be design names a
@@ -89,9 +88,9 @@ and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
that carried them before they were removed. The rules on an issue's status this check held until then — an
owner for a located issue, a fix for a resolved one, and a core issue's `replay` or `replay-none` (ADR 0237)
— are refused at the write by `issue-tracker.move`. Its rules on research efforts and decision records left
with them (the table above). `python3 00-META/checks/cycle.py`
with them (the table above).
## words.py
## The words test
One word per thing, checked (ADR 0244).
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
@@ -106,7 +105,7 @@ of the tools' retired words (`retired-words`) with the glossary.
It also fails on a **daemon's line** (ADR 0272,
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
(`daemonLines` in the words test); a command that is a line only with an argument after it is matched as a line is written. Looked
for in the same documents, from 2026-10-09 for issues, in the document's own words — code blocks and
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
(`retired-lines`) with the list.
-173
View File
@@ -1,173 +0,0 @@
#!/usr/bin/env python3
"""The development cycle, checked.
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
frontmatter, because a status this repository holds lives in frontmatter and nowhere else.
What is enforced:
design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
design without a decision -- and once `in-progress` or `implemented` it names
its owning code (`code:`) -- no development without a design that says where.
issues every report has frontmatter, and no two records share a number -- the number is
how a record is cited. An issue's status, owner, fix and replay live in the issue
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
refuses at the write what this check refused at merge until then: a status outside
the five, `located` without an owner, `resolved` without a fix, a core issue
resolved without its replay (ADR 0237), `wontfix` without a reason. A report
carries none of those keys, only the day it was `opened:` -- the tracker holds
them, and two places holding one fact drift. The keys the reports opened before
the import carried were removed once the import held them; the commit it read
is on every imported issue.
The decision records and research efforts live in the knowledge base (ADR 0290, issue 426), and the
rules this check held over them are the knowledge base's now: a research effort's status and what it
became, and a decision's duplicate number, are refused by `knowledge-base.propose`; an accepted decision
cited by nothing is the knowledge base's whole-set rule `cited`; a deferral that names no issue is its
rule `deferral`. README.md beside this file says where each went.
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
was decided).
python3 00-META/checks/cycle.py
"""
import glob
import os
import re
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
# The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275,
# slice 3 of to-be 53), and `issue-tracker.move` enforces the cycle's rules on them at the write. A report
# carrying one of them is a status written in two places, whenever it was opened.
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved")
def rel(path):
return os.path.relpath(path, ROOT)
def frontmatter(path):
"""The YAML block between the first two --- lines, as {key: raw-value-string}.
Minimal on purpose, like records.py: enough for the fields these checks read. A list
value (block or inline) is joined into its items; a scalar stays a string.
"""
text = open(path, encoding="utf-8").read()
m = re.match(r"^---\n(.*?)\n---", text, re.S)
if not m:
return None
front, out, key = m.group(1), {}, None
for line in front.split("\n"):
item = re.match(r"^\s+-\s*(.+?)\s*$", line)
if item and key:
out[key].append(item.group(1))
continue
kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line)
if not kv:
continue
key, value = kv.group(1), kv.group(2).strip()
if value.startswith("[") and value.endswith("]"):
out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()]
elif value == "":
out[key] = [] # a block list may follow; stays [] if nothing does
else:
out[key] = value
return out
def listy(front, key):
v = front.get(key)
if v is None:
return []
return v if isinstance(v, list) else ([v] if str(v).strip() else [])
def main():
failures = []
def bad(path, why):
failures.append(" %s: %s" % (rel(path), why))
# ---- design ------------------------------------------------------------------------
for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")):
for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))):
if os.path.basename(path) == "README.md":
continue
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
if front.get("layer") != name:
bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name))
status = front.get("status")
if status not in DESIGN_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES)))
if name == "to-be":
if not listy(front, "decisions"):
bad(path, "names no decisions -- no design without a decision")
if status in ("in-progress", "implemented") and not listy(front, "code"):
bad(path, "status %s but code: names no owner -- no development "
"without a design that says where" % status)
# ---- issues ------------------------------------------------------------------------
# Two records may not share a number. Numbers are taken as "next free after main", and work
# sits on unmerged branches for days -- so two people reading the same main allocate the same
# number, and nothing said so. It happened twice in one evening between two machines, and the
# second collision landed on main with all three checks passing (issue 155). An issue number is
# how every other record cites this one; two records answering to it means a pointer that
# resolves to whichever the reader happened to open.
seen = {}
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
name = os.path.basename(os.path.normpath(folder))
number = name.split("-", 1)[0]
if not number.isdigit():
continue
if number in seen:
bad(os.path.join("04-ISSUES", name),
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
"records answering to one means a citation that resolves to whichever the reader "
"opened. Take the next free number across main AND every open pull request"
% (number, seen[number]))
else:
seen[number] = name
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
# tracker, and its `move` refuses at the write what this check refused at merge. A report carries none
# of those keys.
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
opened = str(front.get("opened") or "")
if not opened:
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
carried = [key for key in TRACKER_KEYS if key in front]
if carried:
bad(path, "carries %s, but an issue's status, owner, fix and replay live in the issue tracker "
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
% ", ".join("`%s:`" % key for key in carried))
checked = (
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
)
if failures:
print("cycle: %d document(s) break the development cycle:" % len(failures))
print("\n".join(failures))
return 1
print("cycle: %d documents checked, the chain holds" % checked)
return 0
if __name__ == "__main__":
sys.exit(main())
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""The reading order of the decision records: its topics are written here, its records are not.
A number identifies a record and never changes, so the reading order is not the order of the numbers.
It is the six topics `02-DECISIONS/README.md` writes down, in order, and each record's own topic. The
records live in the knowledge base (ADR 0290, issue 426), which reads these lines through the records
module and refuses a record whose topic is not one of them; the list of records under a topic is its
answer to `knowledge-base.holds` with that topic, never stored here (ADR 0248, hq issue 297).
python3 00-META/checks/index.py check
It fails when the README's reading order is missing or names a topic twice, and when the README stores
a list of records again — the index markers, or a line of a generated list. The last one is what keeps
the conflict of issue 297 from coming back. Whether every record's topic is one of the six is the
knowledge base's rule `topic`, judged when a record is proposed.
"""
import io
import re
import sys
README = "02-DECISIONS/README.md"
# A topic in the README's reading order, e.g.
# 1. **What the mesh is** — `topic: the mesh`. What it is for, ...
# The knowledge base (mesh-knowledge, reference.go) reads the same line, so the form is a contract.
TOPIC_LINE = re.compile(r"^\d+\.\s+\*\*(?P<label>[^*]+)\*\*\s+—\s+`topic:\s*(?P<topic>[^`]+)`", re.M)
# What a stored list looks like: the old markers, or a line of the list index.py used to print.
STORED = [
(re.compile(r"<!--\s*index:(start|end)\s*-->"), "an index marker"),
(re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
]
def reading_order(text=None):
"""The topics, in order, as (topic, label) pairs, from the README."""
if text is None:
text = io.open(README, encoding='utf-8').read()
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
def main():
if "--print" in sys.argv:
print("index: the records live in the knowledge base (ADR 0290). Read a topic's with "
"`knowledge-base.holds` and that topic, through the mesh MCP server.")
return 2
text = io.open(README, encoding='utf-8').read()
topics = reading_order(text)
problems = []
if not topics:
problems.append("%s writes no reading order: no line of the form "
"'1. **Label** — `topic: name`. ...'; the knowledge base refuses every "
"record without it" % README)
names = [t for t, _ in topics]
if len(set(names)) != len(names):
problems.append("%s names a topic twice in its reading order" % README)
for pattern, what in STORED:
if pattern.search(text):
problems.append("%s stores a list of records (%s). The records live in the knowledge base, "
"and a topic's list is `knowledge-base.holds` with that topic" % (README, what))
for p in problems:
print("index: " + p)
if problems:
return 1
print("index: %d topics in the reading order, no stored list" % len(topics))
return 0
if __name__ == "__main__":
sys.exit(main())
-227
View File
@@ -1,227 +0,0 @@
#!/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())
+2 -2
View File
@@ -1,6 +1,6 @@
# Words allowed — documents `words.py` does not hold to the glossary's retired words
# Words allowed — documents the words test does not hold to the glossary's retired words
Read by [`words.py`](words.py) (ADR 0244).
Read by the words test (`words_test.go`) (ADR 0244).
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
themselves. A row whose document no longer uses a retired word fails too, so this list only shrinks. The
research efforts this list kept as written (research 034 and 042) live in the knowledge base now (ADR 0290),
-377
View File
@@ -1,377 +0,0 @@
#!/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))
+7 -7
View File
@@ -14,7 +14,7 @@ and the domain decides what that word means. Other domains use the word as it is
change its meaning. Every word is defined once, in the domain that owns it; a section lists the words
it uses from other domains under *Uses*, with no second definition.
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them:
Two fixed lines follow an entry where they apply, and the words test in `00-META/checks/` reads them:
- *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope
is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the
@@ -35,7 +35,7 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
names, except *observability*, which is now **Health and repair**. Where a domain's records live in
the controller, the domain owns that store alone (ADR 0008).
*Context* in that sense is not used in new writing; the word stays ordinary English, so this is
checked by review, not by `words.py`.
checked by review, not by the words test.
- **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of
the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on
it before the mesh — says machine.
@@ -680,7 +680,7 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272, 0383, 03
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*. *nox* is also the agent (ADR 0414 §7): see
[Homonyms](#homonyms), and say which is meant. It is the one command the operator types: every line the mesh addresses to the operator — a
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse
or the node-engine's binary followed by a command), which the words test and the catalogue's check refuse
(ADR 0272,
its insight of 2026-10-09).
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
@@ -912,12 +912,12 @@ Not a domain of the mesh but of the way it is built, listed because its words me
- **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The
**tracking line** is the line the router says in every message about a condition: the issues that track it and
where each stands, or that none does (ADR 0285).
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
- **hq check** — one of the Go tests in `00-META/checks/`, run by `go test ./...` and by the checker as `mesh/check`.
## Homonyms
A word below means different things in different domains. In a governing document it is never bare;
it is always the qualified form. **Checked by review, not by `words.py`:** a word list cannot tell one
it is always the qualified form. **Checked by review, not by the words test:** a word list cannot tell one
sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by
renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
@@ -991,12 +991,12 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
A new word for an existing thing lands here first, in the domain that owns it, in the same change that
introduces it in code. A word moves to another domain only with a decision record. A retired word goes
on a *Not:* line and nowhere else on this page, and `words.py` then fails on it in running prose. A
on a *Not:* line and nowhere else on this page, and the words test then fails on it in running prose. A
decision record keeps whatever word it was written with — those are immutable — so a word
retired here may still appear in one, and the *Not:* lines are how to read it.
**How it is checked** (ADR 0244):
`python3 00-META/checks/words.py`, run by `merge-check.sh` on every pull request, fails on a retired
The words test in `00-META/checks/` (`go test ./...`, run by the checker as `mesh/check` on every pull request) fails on a retired
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
issues opened from 2026-10-07 — code spans, quotations and link targets excepted — and on a
head word that heads two entries or is also retired. The words retired with no scope or with *(tools)*
+3 -3
View File
@@ -67,11 +67,11 @@ A drawn overview of them, the cycle, a feature's steps, the review loop and an i
## The cycle is checked
The flow above is a rule, and a rule states how it is checked:
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no
the cycle test in [`00-META/checks/`](../checks/) refuses a to-be design that names no
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
sharing a number or an issue report carrying a status key; a `graduated` research effort that
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with `records.py`, `index.py` and
`words.py` before any HQ merge (`go test ./...` at the root runs all four as Go tests, and `sh merge-check.sh` runs the Python ones until the delivery of R4g is done: ADR 0527). The issue rules the check
does not say what it became is refused by the knowledge base's `propose` (ADR 0290). Run it with the records, index and
words tests before any HQ merge: `go test ./...` at the root runs all four, and the checker runs the same as `mesh/check` (ADR 0527). The issue rules the check
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
refused at the write by the issue tracker's `move`
(ADR 0275). What the checks cannot see — that code work actually started from a
+2 -2
View File
@@ -119,7 +119,7 @@ for the operator's Approve on a channel that proves who answered.
- Do not put status in prose. It is the effort's `status`, and the prose must not restate it.
- Do not give a `became` link while the effort is open — playbook 02 gives it at closure.
- Do not write into `03-DESIGN` from an open effort.
- Do not write an effort, or a copy of one, into this repository: `records.py` refuses a new path under
- Do not write an effort, or a copy of one, into this repository: the records test refuses a new path under
`01-RESEARCH/`.
## Closing
@@ -136,5 +136,5 @@ after every acceptance and raise a condition when one fails. The habits of steps
against the rules, one version per settled set, the cheapest level, nothing left open) are the lead's, read by
the reviewer; of the pre-check, `propose` refuses a link to nothing and a retired word, and the review returns
a record named but not linked, an untracked deferral and a figure not as measured. That this repository holds
no effort is `records.py`'s `no-records` check, on every pull request. An active effort with no new version
no effort is the records test's `no-records` check, on every pull request. An active effort with no new version
for seven days is the smell of research left behind: its lead closes or continues it.
+3 -3
View File
@@ -153,7 +153,7 @@ Implementation state is a third axis, independent of both design and decision.
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
the decision stands, is a progressive insight — marked and dated in the record, never silent.
- Do not write a decision record, or a copy of one, into this repository: `records.py` refuses a new path
- Do not write a decision record, or a copy of one, into this repository: the records test refuses a new path
under `02-DECISIONS/` other than its README.
## How it is checked
@@ -162,8 +162,8 @@ The rules on a record are the knowledge base's: `propose` refuses one that break
record (its module's table test holds one case per rule), and the four rules that judge the whole record set
(an accepted decision cited by nothing, a supersession named by one side only, a changed clause nothing
changes, an accepted record extending a superseded one) run after every acceptance and raise a condition
when one fails. In this repository, `records.py` checks that a design's `decisions:` entries and every
"ADR NNNN" in the text are well formed, and that no record is written here; `index.py` checks the reading
when one fails. In this repository, the records test checks that a design's `decisions:` entries and every
"ADR NNNN" in the text are well formed, and that no record is written here; the index test checks the reading
order's six topics, which the knowledge base reads. The three habits of graduating step 2 (side by side, clauses
from the direction, the pre-check) are the lead's, read by the reviewer; `propose` refuses a link to nothing,
a deferral no issue tracks and a retired word, and the review returns a record named but not linked.
+3 -3
View File
@@ -66,7 +66,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
```
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
report. **No status key**: the cycle test refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
`replay:` or `replay-none:` in any report, because the tracker holds them and two places holding one fact
drift.
@@ -107,7 +107,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
is must not have to already know there was a sequel.
- A number never moves. A folder carries the number the tracker gave; a folder whose number collides with
another (one numbered before the tracker, or by hand) is a fault, and `cycle.py` refuses two records
another (one numbered before the tracker, or by hand) is a fault, and the cycle test refuses two records
sharing a number.
- An issue whose answer is a general lesson should also have that lesson written where a search by
the symptom's literal words finds it, so the next person searching it finds it. Both, not either.
@@ -117,6 +117,6 @@ import read, which every imported issue names (`imported.commit` in `issue-track
- **A rule states how it is checked.** The cycle's rules on an issue — a status one of the six, `diagnosing` and
`fixing` with a worker and no other current one, `located` with an owner, `resolved` only from `fixing` and with a
fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). `cycle.py` checks what
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). the cycle test checks what
this repository still holds: no two folders share a number, every report has frontmatter with the day it was
`opened:`, and no report carries a status key.
+1 -1
View File
@@ -66,7 +66,7 @@ A design becomes **milestones**, and each milestone becomes **issues**:
## How it is checked
Read by the lead and the reviewer today; a check in hq's merge-check is tracked by issue 845.
Read by the lead and the reviewer today; a check among hq's Go tests is tracked by issue 845.
- A design in `status: in-progress` has a *Milestones* section, each milestone with its outcome and its
issues; an in-progress design without one is the smell of building without a breakdown.
+3 -3
View File
@@ -421,7 +421,7 @@ const P = [
prod:"A research effort in the knowledge base, status active, every version kept. Nothing in hq.",
next:"02",bounds:"Setup 30 min · proposing 15 min · each experiment 30 min · direction 30 min · report every 10 min",
prev:["Research that measured nothing","A recommendation the operator must argue back","Efforts copied into hq"],
check:"propose refuses rule breaks; the knowledge base reviews; records.py on every PR."},
check:"propose refuses rule breaks; the knowledge base reviews; the records test on every PR."},
{n:"02",name:"Graduation & design change",kind:"idea",c:"--dec",
trig:"A research effort concludes, or an existing design must change.",
who:"The lead, on the larger model; the decision is recorded before the design moves.",
@@ -429,7 +429,7 @@ const P = [
prod:"Decision records; a to-be design with frontmatter; glossary entries; later the as-is.",
next:"04",bounds:"One sitting of about an hour",
prev:["A decision rewritten silently","A deferral nothing tracks","A design amended without a decision"],
check:"propose refuses; records.py and index.py; set-wide rules raise conditions."},
check:"propose refuses; the records and index tests; set-wide rules raise conditions."},
{n:"03",name:"Issues",kind:"symptom",c:"--sym",
trig:"Something is wrong: an unenforced rule, a stated behaviour that does not happen, a silent failure.",
who:"Anyone opens; a worker claims it by moving it.",
@@ -437,7 +437,7 @@ const P = [
prod:"A tracker record with notes and links; for a design issue, its narrative folder in hq.",
next:"02 · 07 · 08",bounds:"A worker idle for a day is stale",
prev:["Numbers that collide","Two agents starting the same work","Status held in two places"],
check:"The tracker's move refuses at the write; cycle.py checks the folders."},
check:"The tracker's move refuses at the write; the cycle test checks the folders."},
{n:"04",name:"Work breakdown & handoff",kind:"idea",c:"--dec",
trig:"A to-be design is settled and its building is about to start.",
who:"The lead who will run the feature, on the larger model.",
+2 -2
View File
@@ -1,7 +1,7 @@
---
layer: to-be
status: in-progress
code: [hq 00-META/checks/words.py, mesh-catalog merge-check.sh]
code: [hq 00-META/checks/words_test.go, mesh-catalog merge-check.sh]
updated: 2026-10-20
decisions:
- "ADR 0429"
@@ -127,7 +127,7 @@ rule stands with the new word: where a domain's records live in the controller,
## How it is checked
- **In this repository**, the hq check `words.py` runs on every pull request as part of the repository
- **In this repository**, the hq check (the words test, `go test ./...`) runs on every pull request as part of the repository
check. It reads the glossary's *Not:* and *Identifier* lines and fails on a retired word or a bare
identifier in running prose — quotations, code and link targets excepted — in `00-META/`, both layers of
the designs, `AGENTS.md`, `README.md`, and research and issues dated from the decision onward. It fails
@@ -61,7 +61,7 @@ On 2026-10-09 the operator took the rename as long implemented and live. It is n
2. **The rule**, from 2026-10-09: a decision that defers work names the issue that tracks it, with an
owner, or the deferral is not accepted. Playbooks [02](../../00-META/process/02-graduation.md)
(graduation) and [04](../../00-META/process/04-build-handoff.md) (handoff) say it;
[`cycle.py`](../../00-META/checks/cycle.py) checks it: an accepted record dated from 2026-10-09, or an
the cycle test (`cycle.py` then) checks it: an accepted record dated from 2026-10-09, or an
older one corrected under the rule, whose text puts work off ("until renamed", "left for later", "not
decided here", "postponed", "future work", "deferred", "a later change") names the issue as tracking it
("tracked by issue N"), in that paragraph or list item, or in a dated progressive insight in the same
+6 -7
View File
@@ -22,8 +22,9 @@ Work enters as an **idea** (playbook [01 — research](00-META/process/01-resear
[04](00-META/process/04-build-handoff.md), on a feature branch per playbook
[07](00-META/process/07-feature-branches.md)) — and on shipping the as-is is updated and the
design flips to `implemented`. **No design without a decision; no development without a design
that names its owner.** Enforced by [`00-META/checks/cycle.py`](00-META/checks/cycle.py)
alongside `records.py`, `index.py` and `words.py` — run all four before any merge here.
that names its owner.** Enforced by the cycle test in
[`00-META/checks/`](00-META/checks/) alongside the records, index and words tests — run `go test ./...` at the
root before any merge here.
## Where to look (before assuming anything)
@@ -49,16 +50,14 @@ One name per thing. [`00-META/glossary.md`](00-META/glossary.md) is the authorit
vocabulary, organised by the mesh's domains (ADR 0244):
*controller* (not "control plane"), *foundation* (not "substrate"), *node-engine* (not "the host"), a
*node* is a machine the mesh has adopted, *seat* / *bench* / *claim*, *package* vs *artifact*. Use those
words. A retired word is named on its replacement's *Not:* line, and `00-META/checks/words.py` fails on it
words. A retired word is named on its replacement's *Not:* line, and the words test in `00-META/checks/` fails on it
in running prose; a quotation keeps its words.
## Ground rules
- **Markdown only** — but for the four checks, Go tests in the package `00-META/checks/` with `go.mod` at the
root and no other Go code (ADR 0527), which the checker runs on every pull request as `mesh/check`;
until they replace them, the Python checks beside them and `merge-check.sh` at the root keep running as
`mesh/repo-check` (ADR 0238); and
`00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
until they replace them, `00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
2026-10-19); a change to a playbook's steps updates it in the same pull request. Nothing checks that
yet: issue 1222 asks for the check.
A to-be design may also keep its reference mockup, the static HTML page the operator picked, in a
@@ -72,7 +71,7 @@ in running prose; a quotation keeps its words.
Never create a central status file; cross-cutting views are generated when they are read.
- **No decision record or research effort is written here.** They live whole in the knowledge base and
are written with `knowledge-base.reserve` and `knowledge-base.propose` (playbooks
[01](00-META/process/01-research.md) and [02](00-META/process/02-graduation.md)); `records.py` refuses a
[01](00-META/process/01-research.md) and [02](00-META/process/02-graduation.md)); the records test refuses a
new path under `02-DECISIONS/` or `01-RESEARCH/`. A record is cited as "ADR NNNN" or "research NNN".
- **Decision records hold their meaning.** Supersede with a new record rather than rewriting
what was decided, the options weighed, or a consequence another record relies on. A **progressive
+1 -1
View File
@@ -40,7 +40,7 @@ Implementation lives in the code repositories — see
## Rules
- Markdown only, but for the checks and one drawn overview of the playbooks (`00-META/process/playbooks.html`).
- Markdown only, but for the checks (Go tests, `go test ./...` at the root, run by the checker as `mesh/check`) and one drawn overview of the playbooks (`00-META/process/playbooks.html`).
- No new top-level folders without explicit confirmation.
- Knowledge flows `GENESIS → RESEARCH → DESIGN`. Research graduates into design only after
analysis against GENESIS confirms alignment, and only through a recorded decision.
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
# mesh-check-toolchain: go
#
# This repository's own check (ADR 0238): the second layer of a pull request's merge check,
# `mesh/repo-check`, run by the build seat in the mesh's Go toolchain, which carries Python for it. No
# module is built from here, so no gate runs (`mesh/merge-gate` says the change touches none). The four
# checks every merge here must pass (AGENTS.md): the documents' structure (and that no decision record or
# research effort is written here: they live in the knowledge base, ADR 0290), the reading order, the cycle,
# and the words (ADR 0244: no retired word in running prose, no word defined twice).
set -eu
python3 00-META/checks/records.py
python3 00-META/checks/index.py
python3 00-META/checks/cycle.py
python3 00-META/checks/words.py