diff --git a/00-META/checks/README.md b/00-META/checks/README.md index 69716711..de1cb680 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -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. diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py deleted file mode 100644 index c47ec031..00000000 --- a/00-META/checks/cycle.py +++ /dev/null @@ -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()) diff --git a/00-META/checks/index.py b/00-META/checks/index.py deleted file mode 100644 index af833186..00000000 --- a/00-META/checks/index.py +++ /dev/null @@ -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