Remove the decision records and research efforts, which the knowledge base now holds whole (issue 426)
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
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.
This commit is contained in:
+3
-3
@@ -35,7 +35,7 @@ comparing it against the code rather than by anyone noticing:
|
||||
|
||||
- It described the pipeline as having a separate builder process and a build stage that
|
||||
packages. Neither was true after 2026-08-04; the documents stayed stale until 2026-08-06
|
||||
([ADR 0010](../02-DECISIONS/0010-delivery.md)).
|
||||
(ADR 0010).
|
||||
- It listed the mesh as spanning a fixed number of named machines, which is exactly the
|
||||
content this repository cannot carry.
|
||||
|
||||
@@ -45,10 +45,10 @@ symlinks at all — the rule is not merely "only the installer may link", and a
|
||||
elevating linking to a principle points the opposite way from where this is going.
|
||||
|
||||
What exists today is that the installer owns and reconciles every link
|
||||
([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)) — an as-is fact, recorded in
|
||||
(ADR 0012) — an as-is fact, recorded in
|
||||
[`03-DESIGN/00-as-is/05-runtime-and-installation.md`](../03-DESIGN/00-as-is/05-runtime-and-installation.md).
|
||||
Centralising who may link narrowed the incident class; it did not close it. The intent is to
|
||||
remove the mechanism, recorded as [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md).
|
||||
remove the mechanism, recorded as ADR 0012.
|
||||
|
||||
A founding document contradicting the direction of travel is precisely the failure this folder
|
||||
exists to prevent.
|
||||
|
||||
+60
-58
@@ -1,9 +1,9 @@
|
||||
# Checks
|
||||
|
||||
```
|
||||
python3 00-META/checks/records.py structure: links, citations, supersession, topics
|
||||
python3 00-META/checks/index.py every record's topic is in the reading order, and no list of records is stored
|
||||
python3 00-META/checks/index.py --print the records, by topic, in reading order (generated, never written)
|
||||
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
|
||||
@@ -23,31 +23,49 @@ indistinguishable from one that cannot.
|
||||
| Check | Asserts | Found |
|
||||
|---|---|---|
|
||||
| `links` | every relative link resolves | — (run ad hoc during authoring; now permanent) |
|
||||
| `rests-on` | `decisions:` and `extends:` name records that exist and are **accepted** | the class behind both incidents |
|
||||
| `live-citation` | a governing document citing a **superseded** record names its replacement in the same paragraph | `01-to-be/README.md` citing ADR 0022 as live guidance |
|
||||
| `supersession` | if A says it was superseded by B, B says it supersedes A | ADR 0012 never declared that it superseded 0011 |
|
||||
| `partial-supersession` | a record's `supersedes-in-part:` names records that exist, each carrying a dated *Superseded in part* note naming it under its heading; and every such note is named back by its superseder's field | 11 of 21 partial supersessions had no note on the record they change, and 6 more were said only in prose ([research 045](../../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md)) |
|
||||
| `clauses` | an accepted record dated from 2026-10-11 writes its `## Decision` section as numbered clauses with a bold lead ([ADR 0290](../../02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md) rule 3) | 226 of 263 decision records wrote their decision as prose, so the knowledge base must draft their clauses |
|
||||
| `numbering` | the number in the filename is the number in the heading | — |
|
||||
| `topics` | every record names a topic of the reading order in `02-DECISIONS/README.md` | — |
|
||||
| *(index.py)* | the reading order is written, every record's topic is one of it, and the README stores no list of records ([ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)) | 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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
The decision records and research efforts left this repository for the knowledge base (ADR 0290, issue 426),
|
||||
and the rules these checks held over them left with them. The knowledge base holds each as a rule of its own,
|
||||
named in its refusal: `knowledge-base.propose` refuses a record that breaks one of the rules that judge one
|
||||
record, naming every reason at once, and the four rules that judge the whole record set run after every
|
||||
acceptance and raise a condition when one fails (mesh-knowledge's `rules.go`, one table-test case per rule).
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
## What is deliberately not checked
|
||||
|
||||
- **`02-DECISIONS/` and `01-RESEARCH/` may cite superseded records freely.** A decision record
|
||||
discusses history; research records what was observed. Flagging those would produce noise on
|
||||
correct documents, and a check that cries wolf gets suppressed — which costs more than not
|
||||
having it.
|
||||
- **`03-DESIGN/00-as-is/` may rest on a superseded record.** It describes what runs, and what
|
||||
runs was built under whatever was decided at the time
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md):
|
||||
*as-is describing a superseded decision is exactly what as-is is for*).
|
||||
- **Whether a citation's prose is still true.** Only whether the record it points at is live.
|
||||
A document can cite an accepted record and describe it wrongly, and nothing here notices.
|
||||
|
||||
So "governing" means `00-META/` and `03-DESIGN/01-to-be/` — the documents that tell somebody
|
||||
what to do.
|
||||
- **Whether a record a document cites exists, or is still accepted.** A checkout cannot read the knowledge
|
||||
base, and a check that called the mesh would make every merge here depend on it. Until designs become the
|
||||
knowledge base's own kind (issue 428), the reviewer asks `knowledge-base.get` for each record a change
|
||||
cites in `00-META/` or `03-DESIGN/01-to-be/` (the documents that tell somebody what to do). An as-is
|
||||
document may rest on a superseded record: it describes what runs, and what runs was built under whatever
|
||||
was decided at the time (ADR 0006: *as-is describing a superseded decision is exactly what as-is is for*).
|
||||
- **Whether a citation's prose is still true.** A document can cite an accepted record and describe it
|
||||
wrongly, and nothing here notices.
|
||||
|
||||
## Adding a check
|
||||
|
||||
@@ -56,51 +74,35 @@ whose failure has never been observed is a guess about its own correctness.
|
||||
|
||||
## cycle.py
|
||||
|
||||
The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)):
|
||||
a to-be design names a decision, an in-progress/implemented design names its owning code, a graduated
|
||||
research overview says what it became, and no two issue records share a number (issue 155 — the
|
||||
number is how a record is cited, and `main` lags every open pull request, so two people reading it
|
||||
allocate the same one). An issue report carries the day it was `opened:` and no status key — `status:`,
|
||||
`located-in:`, `fixed-by:`, `amended-design:`, `replay:`, `replay-none:` or `resolved:` — because an issue's
|
||||
status lives in the issue tracker ([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md));
|
||||
it failed on all 361 reports 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](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md))
|
||||
— are refused at the write by `issue-tracker.move`. And an accepted record that puts work
|
||||
off — "until renamed", "left for later", "not decided here", "postponed", "future work", "deferred", "a later
|
||||
change" — names the issue that tracks it, as tracking it ("tracked by issue N", or "issue N tracks"), in that
|
||||
paragraph or list item, or in a dated progressive insight that speaks about it: one in the same section, or one
|
||||
that quotes it. An issue cited for another reason tracks nothing, and one insight does not excuse every deferral
|
||||
in a record. The tracking issue counts when it has a folder in `04-ISSUES/`, or when its number is above every
|
||||
folder's: since ADR 0275 an issue from 365 on lives only in the issue tracker, which a checkout cannot read, and the
|
||||
tracker hands out numbers in order (issue 436). A made-up number above the folders therefore passes; the reviewer
|
||||
checks that the issue exists, with `issue-tracker.get`. A rejected option is not work put off and is not held, judged by its `##` section, so an option under a
|
||||
`###` subheading of "Considered Options" is skipped too
|
||||
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md):
|
||||
ADR 0244 deferred the code renames with no issue, and two days later the operator believed them done). Held to
|
||||
it: a record dated from 2026-10-09, and an older one corrected under the rule. Measured from ADR 0244's date
|
||||
it found deferrals without an issue in ADR 0241, 0244 and 0270; held from 2026-10-09 it failed on 0270 and 0273
|
||||
(whose "not decided here" sits beside a citation of issue 340 that tracks none of it), and on 0244's four
|
||||
deferral places once its insight brought it under the rule — on three of them while the insight, in §6, named
|
||||
the issue without quoting them — before each named its issue (355, 357, 354). `python3 00-META/checks/cycle.py`
|
||||
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
|
||||
decision in `decisions:`; an in-progress or implemented design names its owning code; and no two issue
|
||||
records share a number (issue 155 — the number is how a record is cited, and `main` lags every open pull
|
||||
request, so two people reading it allocate the same one). An issue report carries the day it was `opened:`
|
||||
and no status key — `status:`, `located-in:`, `fixed-by:`, `amended-design:`, `replay:`, `replay-none:` or
|
||||
`resolved:` — because an issue's status lives in the issue tracker (ADR 0275); it failed on all 361 reports
|
||||
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`
|
||||
|
||||
## words.py
|
||||
|
||||
One word per thing, checked ([ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
|
||||
One word per thing, checked (ADR 0244).
|
||||
It reads the glossary's *Not:* lines (the retired words, each with its scope) and *Identifier until
|
||||
renamed* lines, and fails on a retired word or a bare identifier in running prose — what is left once code,
|
||||
quotations, struck-through text, link targets, comments and frontmatter are taken out — in `00-META/`,
|
||||
`03-DESIGN/`, `AGENTS.md`, `README.md`, and research and issues dated from 2026-10-07. Decision records are
|
||||
never checked. It also fails when a glossary head word heads two entries or is also retired.
|
||||
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date, or
|
||||
a graduated research effort kept as written. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||
`03-DESIGN/`, `AGENTS.md`, `README.md`, and issues opened from 2026-10-07. Decision records and research
|
||||
efforts live in the knowledge base, which holds a proposed record to the same words (the table above). It
|
||||
also fails when a glossary head word heads two entries or is also retired.
|
||||
[`words-allowed.md`](words-allowed.md) names a document that could not be reworded at once, with a date. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||
of the tools' retired words (`retired-words`) with the glossary.
|
||||
|
||||
It also fails on a **daemon's line** ([ADR 0272](../../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md),
|
||||
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
|
||||
for in the same documents, from 2026-10-09 for research and issues, in the document's own words — code blocks and
|
||||
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.
|
||||
|
||||
|
||||
+6
-181
@@ -22,12 +22,12 @@ What is enforced:
|
||||
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.
|
||||
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
||||
target it names exists.
|
||||
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
||||
frontmatter, a research overview, an issue report, a 00-META document, or another
|
||||
record's extends/supersedes chain. A decision nothing points at is one nobody will
|
||||
find by following pointers -- which is how records go stale in people's heads.
|
||||
|
||||
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).
|
||||
@@ -43,89 +43,12 @@ import sys
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
|
||||
RESEARCH_STATUSES = {"active", "graduated", "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")
|
||||
|
||||
# A decision that defers work names the issue that tracks it (issue 354). ADR 0244 left the code
|
||||
# renames of the node-engine and the tool runner "for a later rename", with no issue, owner or date;
|
||||
# two days later the operator believed them done, and nothing in the record said otherwise. A deferral
|
||||
# nothing tracks is indistinguishable from work that was done. Held to it: an accepted record dated
|
||||
# from DEFERRALS_FROM, and an older one corrected under the rule (a progressive insight dated from then
|
||||
# on): an edit made under the rule is held to it. The phrases are the ones records use to put work off;
|
||||
# "for later" alone is not one, because records say "never queued for later" and "too narrow for later".
|
||||
# The tracking issue is named as tracking it, beside the deferral or in a dated insight in the same section
|
||||
# or quoting it (ISSUE_REF, covers).
|
||||
DEFERRALS_FROM = "2026-10-09"
|
||||
DEFERRAL = re.compile(r"(?i)until (?:it is |they are |the code (?:is )?)?renamed|until the code rename|"
|
||||
r"\ba later rename|not renamed here|identifier to rename|"
|
||||
r"left (?:for|to) (?:a )?later|not decided here|\bpostponed\b|future work|"
|
||||
r"\bdeferred\b|later work|not built here|\ba later change")
|
||||
# The issue that tracks a deferral is named as tracking it -- "tracked by issue N", or "issue N tracks" --
|
||||
# because a record cites issues for many reasons, and an issue cited as context beside a deferral (ADR 0273
|
||||
# names issue 340 in the very sentence that leaves a channel undecided) tracks nothing.
|
||||
ISSUE_REF = re.compile(r"(?i)tracked (?:by|in|as)\s+\[?issue (\d{3})|\[?issue (\d{3})\]?(?:\([^)]*\))?\s+tracks\b")
|
||||
INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2}-\d{2})")
|
||||
|
||||
|
||||
def paragraphs(path):
|
||||
"""A record's body as (first line number, section heading, chapter, text) paragraphs: frontmatter and fenced
|
||||
code left out, a block quote's markers stripped, so a quoted insight reads as the paragraph it is. The
|
||||
section is the nearest heading above the paragraph; a heading is its own paragraph and its own section.
|
||||
The chapter is the nearest `#` or `##` heading, which a `###` subheading inside it does not change."""
|
||||
text = open(path, encoding="utf-8").read()
|
||||
m = re.match(r"^---\n.*?\n---\n?", text, re.S)
|
||||
body, first = (text[m.end():], text[: m.end()].count("\n") + 1) if m else (text, 1)
|
||||
body = re.sub(r"(?ms)^\s*```.*?^\s*```", lambda x: re.sub(r"[^\n]", " ", x.group(0)), body)
|
||||
out, line, section, chapter = [], first, "", ""
|
||||
for chunk in body.split("\n\n"):
|
||||
stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip()
|
||||
if stripped:
|
||||
heading = re.match(r"^#{1,6}\s+(.*)", stripped)
|
||||
if heading:
|
||||
section = heading.group(1).strip()
|
||||
if len(stripped) - len(stripped.lstrip("#")) <= 2:
|
||||
chapter = section
|
||||
# A list's top-level items are units of their own: a Consequences list is one paragraph, and an
|
||||
# issue named in one item tracks nothing in the next.
|
||||
items = re.split(r"\n(?=[-*] |\d+\. )", stripped)
|
||||
at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n")
|
||||
for item in items:
|
||||
out.append((at, section, chapter, item))
|
||||
at += item.count("\n") + 1
|
||||
line += chunk.count("\n") + 2
|
||||
return out
|
||||
|
||||
|
||||
QUOTE = re.compile(r"\"([^\"]{12,})\"|\u201c([^\u201d]{12,})\u201d")
|
||||
|
||||
|
||||
def flat(text):
|
||||
"""Text compared for a quotation: markup and case dropped, whitespace collapsed."""
|
||||
return re.sub(r"\s+", " ", re.sub(r"[*`_]", "", text)).strip().lower()
|
||||
|
||||
|
||||
def covers(insight_section, insight_text, section, text):
|
||||
"""An insight speaks about a deferral paragraph when it stands in the same section, or quotes it."""
|
||||
if insight_section == section:
|
||||
return True
|
||||
body = flat(text)
|
||||
return any(flat(a or b) in body for a, b in QUOTE.findall(insight_text))
|
||||
|
||||
|
||||
def issue_refs(text, issues):
|
||||
"""The issues a text names as tracking it: one with a folder here, or one numbered above every folder.
|
||||
|
||||
Since ADR 0275 an issue from 365 on lives only in the issue tracker, and ADR 0291 removes the folders; a
|
||||
checkout alone cannot see those, so a number above the highest folder counts as one the tracker holds
|
||||
(issue 436). The tracker hands out numbers in order, so such a number was issued after the last folder.
|
||||
"""
|
||||
top = max((int(k) for k in issues if k.isdigit()), default=0)
|
||||
return {n for a, b in ISSUE_REF.findall(text) for n in (a, b) if n and (n in issues or int(n) > top)}
|
||||
|
||||
|
||||
def rel(path):
|
||||
return os.path.relpath(path, ROOT)
|
||||
@@ -216,22 +139,6 @@ def main():
|
||||
else:
|
||||
seen[number] = name
|
||||
|
||||
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
|
||||
# on main from two sessions within the hour, and every check passed.
|
||||
seen_records = {}
|
||||
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
|
||||
name = os.path.basename(path)
|
||||
number = name.split("-", 1)[0]
|
||||
if not number.isdigit():
|
||||
continue
|
||||
if number in seen_records:
|
||||
bad(os.path.join("02-DECISIONS", name),
|
||||
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
|
||||
"free number across main AND every open pull request; the branch that lands last "
|
||||
"renumbers" % (number, seen_records[number]))
|
||||
else:
|
||||
seen_records[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.
|
||||
@@ -250,91 +157,9 @@ def main():
|
||||
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
|
||||
% ", ".join("`%s:`" % key for key in carried))
|
||||
|
||||
# ---- research ----------------------------------------------------------------------
|
||||
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
|
||||
front = frontmatter(path)
|
||||
if front is None:
|
||||
bad(path, "no frontmatter")
|
||||
continue
|
||||
status = front.get("status")
|
||||
if status not in RESEARCH_STATUSES:
|
||||
bad(path, "status %r is not one of %s" % (status, sorted(RESEARCH_STATUSES)))
|
||||
if status == "graduated":
|
||||
became = listy(front, "became")
|
||||
if not became:
|
||||
bad(path, "graduated but became: names nothing")
|
||||
for target in became:
|
||||
if not os.path.exists(os.path.join(ROOT, target)):
|
||||
bad(path, "became names %s, which does not exist" % target)
|
||||
|
||||
# ---- decisions -------------------------------------------------------------------
|
||||
records = {}
|
||||
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
|
||||
front = frontmatter(path)
|
||||
records[os.path.basename(path)] = (path, (front or {}).get("status"))
|
||||
|
||||
cited = set()
|
||||
sources = (glob.glob(os.path.join(ROOT, "03-DESIGN", "*", "*.md"))
|
||||
+ glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))
|
||||
+ glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))
|
||||
+ glob.glob(os.path.join(ROOT, "00-META", "**", "*.md"), recursive=True))
|
||||
for path in sources:
|
||||
text = open(path, encoding="utf-8").read()
|
||||
if os.sep + "03-DESIGN" + os.sep in path:
|
||||
# A design doc's governing citations live in frontmatter; a prose mention is
|
||||
# commentary, not a home.
|
||||
m = re.match(r"^---\n(.*?)\n---", text, re.S)
|
||||
text = m.group(1) if m else ""
|
||||
for m in re.finditer(r"([0-9]{4}-[^\s\)\],#]+\.md)", text):
|
||||
cited.add(os.path.basename(m.group(1)))
|
||||
for name in records:
|
||||
front = frontmatter(records[name][0]) or {}
|
||||
for key in ("extends", "supersedes", "superseded-by"):
|
||||
v = front.get(key)
|
||||
if isinstance(v, str) and v:
|
||||
cited.add(os.path.basename(v))
|
||||
for name, (path, status) in records.items():
|
||||
if status == "accepted" and name not in cited:
|
||||
bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
|
||||
"design doc's decisions:, a research became:, an issue, or 00-META")
|
||||
|
||||
# ---- deferrals -------------------------------------------------------------------
|
||||
# A paragraph or list item that puts work off names the issue that tracks it, or a dated
|
||||
# progressive insight that speaks about it (its section, or a quotation of it) does. An issue that has
|
||||
# since resolved still counts: the deferred work landed, which is what the pointer was for.
|
||||
for name, (path, status) in records.items():
|
||||
front = frontmatter(path) or {}
|
||||
decided = str(front.get("date") or "")
|
||||
if status != "accepted":
|
||||
continue
|
||||
paras = paragraphs(path)
|
||||
insights = [(d, sec, text) for line, sec, _, text in paras for d in INSIGHT.findall(text)]
|
||||
held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _, _ in insights)
|
||||
if not held:
|
||||
continue
|
||||
# A dated insight naming an issue covers the deferrals it speaks about -- those in its own section,
|
||||
# or ones it quotes -- and no other: one insight must not excuse every deferral in the record.
|
||||
tracking = [(sec, text) for d, sec, text in insights if issue_refs(text, seen)]
|
||||
for line, section, chapter, text in paras:
|
||||
m = DEFERRAL.search(text)
|
||||
# A rejected option is a decision not to do it, not work put off -- judged by its chapter, so an
|
||||
# option under a `###` subheading of "## Considered Options" is skipped as well.
|
||||
if re.match(r"(?i)(options? rejected|considered options|rejected)", chapter):
|
||||
continue
|
||||
if not m or issue_refs(text, seen):
|
||||
continue
|
||||
if any(covers(isec, itext, section, text) for isec, itext in tracking):
|
||||
continue
|
||||
bad(path, "line %d defers work (%r) and names no issue that tracks it -- a deferral nothing "
|
||||
"tracks reads as done (issue 354). Name the issue beside it, with its owner, or in a "
|
||||
"dated progressive insight that says what it tracks"
|
||||
% (line + text.count("\n", 0, m.start()), m.group(0)))
|
||||
|
||||
checked = (
|
||||
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
|
||||
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
|
||||
+ len(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md")))
|
||||
+ len(records)
|
||||
)
|
||||
if failures:
|
||||
print("cycle: %d document(s) break the development cycle:" % len(failures))
|
||||
|
||||
+24
-64
@@ -1,23 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The reading order of the decision records: its topics are written, its list is generated.
|
||||
"""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 folder listing is creation order rather
|
||||
than reading order. The reading order is the six topics `02-DECISIONS/README.md` writes down, in
|
||||
order, and each record's own `topic:`. The list of records under each topic is generated from
|
||||
that frontmatter when somebody reads it, and never stored (ADR 0248, hq issue 297): a stored
|
||||
list was a line every decision pull request added to one file, so any two open at once conflicted.
|
||||
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
|
||||
python3 00-META/checks/index.py --print print the list, by topic, in reading order
|
||||
|
||||
It fails when the README's reading order is missing, when a record's topic is not one of its
|
||||
topics, and when the README stores a list of records again — the index markers, or a line of
|
||||
the generated list. The last one is what keeps the conflict from coming back.
|
||||
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 glob
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -25,13 +23,14 @@ 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 records module's `records_decisions` reads the same line, so the form is a contract.
|
||||
# The knowledge base (mesh-knowledge, reference.go) and the records module's `records_decisions` read 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 this script prints.
|
||||
# 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 the generated list"),
|
||||
(re.compile(r"^- \*\*\d{4}\*\* — \[", re.M), "a line of a generated list"),
|
||||
]
|
||||
|
||||
|
||||
@@ -42,72 +41,33 @@ def reading_order(text=None):
|
||||
return [(m.group("topic").strip(), m.group("label").strip()) for m in TOPIC_LINE.finditer(text)]
|
||||
|
||||
|
||||
def field(text, name):
|
||||
m = re.search(r'^%s:\s*(.+)$' % name, text, re.M)
|
||||
return m.group(1).strip() if m else None
|
||||
|
||||
|
||||
def records():
|
||||
out = []
|
||||
for path in sorted(glob.glob('02-DECISIONS/0*.md')):
|
||||
text = io.open(path, encoding='utf-8').read()
|
||||
heading = re.search(r'^# \d+\.\s*(.+)$', text, re.M)
|
||||
out.append({
|
||||
"file": os.path.basename(path),
|
||||
"number": os.path.basename(path)[:4],
|
||||
"title": heading.group(1).strip() if heading else "(no heading)",
|
||||
"topic": field(text, "topic"),
|
||||
"status": field(text, "status"),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def render(topics, rs):
|
||||
lines = []
|
||||
for topic, label in topics:
|
||||
rows = [r for r in rs if r["topic"] == topic]
|
||||
if not rows:
|
||||
continue
|
||||
lines.append("### %s" % label)
|
||||
lines.append("")
|
||||
for r in rows:
|
||||
mark = "" if r["status"] == "accepted" else " *(%s)*" % r["status"]
|
||||
lines.append("- **%s** — [%s](%s)%s" % (r["number"], r["title"], r["file"], mark))
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
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)
|
||||
rs = records()
|
||||
|
||||
if "--print" in sys.argv:
|
||||
print(render(topics, rs))
|
||||
return 0
|
||||
|
||||
problems = []
|
||||
if not topics:
|
||||
problems.append("%s writes no reading order: no line of the form "
|
||||
"'1. **Label** — `topic: name`. ...'" % README)
|
||||
"'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 the list of records (%s). The list is generated when read "
|
||||
"(ADR 0248): `python3 00-META/checks/index.py --print`" % (README, what))
|
||||
known = set(names)
|
||||
for r in rs if known else []: # with no reading order, every record would be named; one line says why
|
||||
if r["topic"] not in known:
|
||||
problems.append("%s: topic %r is not one of the reading order's: %s"
|
||||
% (r["file"], r["topic"], ", ".join(names)))
|
||||
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, %d records, no stored list" % (len(topics), len(rs)))
|
||||
print("index: %d topics in the reading order, no stored list" % len(topics))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+69
-356
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural checks over HQ's own records.
|
||||
"""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:
|
||||
@@ -7,6 +7,11 @@ 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
|
||||
@@ -15,14 +20,17 @@ import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Where a citation is *guidance* rather than history. A document here tells somebody what to
|
||||
# do, so a link to a superseded record is an instruction to follow a withdrawn decision.
|
||||
# 02-DECISIONS and 01-RESEARCH are deliberately absent: they record what was decided and what
|
||||
# was observed, and both legitimately discuss superseded records at length.
|
||||
GOVERNING = ("00-META/", "03-DESIGN/01-to-be/")
|
||||
|
||||
LINK = re.compile(r"\[[^\]]*\]\((?!https?:|mailto:)([^)]+)\)")
|
||||
ADR_FILE = re.compile(r"^(\d{4})-")
|
||||
|
||||
# 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():
|
||||
@@ -45,7 +53,7 @@ def read(path):
|
||||
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 four
|
||||
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"):
|
||||
@@ -74,24 +82,11 @@ def frontmatter(text):
|
||||
return fields
|
||||
|
||||
|
||||
def load_records():
|
||||
"""Every decision record, by its four-digit number."""
|
||||
records = {}
|
||||
folder = os.path.join(ROOT, "02-DECISIONS")
|
||||
for name in sorted(os.listdir(folder)):
|
||||
match = ADR_FILE.match(name)
|
||||
if not name.endswith(".md") or not match:
|
||||
continue
|
||||
path = os.path.join(folder, name)
|
||||
text = read(path)
|
||||
records[match.group(1)] = {
|
||||
"number": match.group(1),
|
||||
"name": name,
|
||||
"path": path,
|
||||
"text": text,
|
||||
"front": frontmatter(text),
|
||||
}
|
||||
return records
|
||||
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:
|
||||
@@ -129,337 +124,62 @@ def check_links(failures):
|
||||
failures.add("links", f"{rel(path)}:{number}", f"link does not resolve: {target}")
|
||||
|
||||
|
||||
def check_rests_on(failures, records):
|
||||
"""A document's `decisions:` and a record's `extends:` must name a live record.
|
||||
def check_no_records(failures):
|
||||
"""No decision record or research effort is written into this repository again (ADR 0290 rule 9).
|
||||
|
||||
These are the load-bearing citations: the document declares that it rests on that
|
||||
decision. Resting on a withdrawn one is the defect this whole check set exists for.
|
||||
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 path in markdown_files():
|
||||
front = frontmatter(read(path))
|
||||
cited = list(front.get("decisions", []) or [])
|
||||
extends = front.get("extends")
|
||||
if isinstance(extends, str) and extends:
|
||||
cited.append(extends)
|
||||
|
||||
for entry in cited:
|
||||
match = ADR_FILE.match(os.path.basename(entry))
|
||||
if not match:
|
||||
failures.add("rests-on", rel(path), f"not a decision record: {entry}")
|
||||
continue
|
||||
number = match.group(1)
|
||||
if number not in records:
|
||||
failures.add("rests-on", rel(path), f"no such record: {entry}")
|
||||
continue
|
||||
status = records[number]["front"].get("status")
|
||||
if status != "accepted":
|
||||
# A proposed record may extend another proposed one. Decisions are drafted in
|
||||
# chains -- 0059 extends 0057 while both await review -- and refusing that would
|
||||
# mean either drafting out of order or marking records accepted to satisfy a
|
||||
# check, which is the failure this repository already made once.
|
||||
if frontmatter(read(path)).get("status") == "proposed":
|
||||
continue
|
||||
# An as-is document describes what runs, and what runs was built under
|
||||
# whatever was decided at the time. ADR 0056: "as-is describing a superseded
|
||||
# decision is exactly what as-is is for."
|
||||
if rel(path).startswith("03-DESIGN/00-as-is/"):
|
||||
continue
|
||||
# A withdrawn record's citations are history. It instructs nobody -- every reader
|
||||
# is sent to its superseder -- so what it was built on may itself be withdrawn.
|
||||
# Refusing that would mean rewriting the lineage of a record whose reasoning is
|
||||
# the thing the immutability rule protects.
|
||||
if frontmatter(read(path)).get("status") == "superseded":
|
||||
continue
|
||||
# An extension that supersedes legitimately names what it replaced.
|
||||
this = ADR_FILE.match(os.path.basename(path))
|
||||
supersedes = records[number]["front"].get("superseded-by", "")
|
||||
if this and supersedes and os.path.basename(path) in str(supersedes):
|
||||
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(
|
||||
"rests-on",
|
||||
rel(path),
|
||||
f"rests on ADR {number}, which is '{status}' — a document may not rest on a "
|
||||
f"record that is not accepted",
|
||||
"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_live_citations(failures, records):
|
||||
"""In a governing document, a link to a superseded record must name its replacement.
|
||||
def check_decisions_entries(failures):
|
||||
"""A design's `decisions:` names each record as "ADR NNNN".
|
||||
|
||||
The reader of a rule needs to know the rule was withdrawn, and needs somewhere to go.
|
||||
Naming the superseder in the same paragraph is both, and it is what a person would
|
||||
write anyway.
|
||||
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.
|
||||
"""
|
||||
superseded = {
|
||||
number: record["front"].get("superseded-by", "")
|
||||
for number, record in records.items()
|
||||
if record["front"].get("status") == "superseded"
|
||||
}
|
||||
|
||||
for path in markdown_files():
|
||||
if not any(rel(path).startswith(prefix) for prefix in GOVERNING):
|
||||
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)
|
||||
offset = 0
|
||||
for paragraph in text.split("\n\n"):
|
||||
line_no = text[:offset].count("\n") + 1
|
||||
offset += len(paragraph) + 2
|
||||
targets = LINK.findall(paragraph)
|
||||
for target in targets:
|
||||
match = ADR_FILE.match(os.path.basename(target.split("#")[0]))
|
||||
if not match or match.group(1) not in superseded:
|
||||
continue
|
||||
number = match.group(1)
|
||||
replacement = os.path.basename(str(superseded[number]))
|
||||
if not replacement:
|
||||
failures.add(
|
||||
"live-citation",
|
||||
f"{rel(path)}:{line_no}",
|
||||
f"cites superseded ADR {number}, which names no superseder",
|
||||
)
|
||||
continue
|
||||
if not any(replacement in t for t in targets):
|
||||
failures.add(
|
||||
"live-citation",
|
||||
f"{rel(path)}:{line_no}",
|
||||
f"cites superseded ADR {number} without naming its replacement "
|
||||
f"({replacement}) in the same paragraph",
|
||||
)
|
||||
|
||||
|
||||
def check_supersession_symmetry(failures, records):
|
||||
"""If A says it was superseded by B, B must say it supersedes A."""
|
||||
for number, record in records.items():
|
||||
front = record["front"]
|
||||
status = front.get("status")
|
||||
by = os.path.basename(str(front.get("superseded-by", "")))
|
||||
|
||||
if status == "superseded" and not by:
|
||||
failures.add("supersession", rel(record["path"]), "marked superseded but names no superseder")
|
||||
continue
|
||||
if by and status != "superseded":
|
||||
failures.add("supersession", rel(record["path"]), f"names a superseder but status is '{status}'")
|
||||
if not by:
|
||||
continue
|
||||
|
||||
match = ADR_FILE.match(by)
|
||||
if not match or match.group(1) not in records:
|
||||
failures.add("supersession", rel(record["path"]), f"superseder does not exist: {by}")
|
||||
continue
|
||||
other = records[match.group(1)]
|
||||
# `supersedes:` may name one record or several. One decision replacing two is a real
|
||||
# situation -- two records that built and refined the same wrong mechanism are withdrawn
|
||||
# by the one record that removes it -- and a check that allows only one would force
|
||||
# either a chain of pro-forma records or an unmarked supersession.
|
||||
claimed = other["front"].get("supersedes", "")
|
||||
if isinstance(claimed, str):
|
||||
claimed = [claimed] if claimed else []
|
||||
claims = [os.path.basename(str(entry)) for entry in claimed]
|
||||
if record["name"] not in claims:
|
||||
failures.add(
|
||||
"supersession",
|
||||
rel(other["path"]),
|
||||
f"ADR {number} says this supersedes it; this record does not say so "
|
||||
f"(supersedes: {', '.join(claims) or 'absent'})",
|
||||
)
|
||||
|
||||
|
||||
PARTIAL_MARK = re.compile(
|
||||
r"^> \*\*Superseded in part — (\d{4}-\d{2}-\d{2}), by \[ADR (\d{4})\]\(", re.M
|
||||
)
|
||||
|
||||
|
||||
# The form these notes had before the check: it named the later record but nothing read it.
|
||||
NARROWED_MARK = re.compile(r"^> \*\*Narrowed, not replaced — [^\n]*\[ADR \d{4}\]\(", re.M)
|
||||
|
||||
|
||||
def check_partial_supersession(failures, records):
|
||||
"""A record superseded in part says so under its heading, and its superseder names it.
|
||||
|
||||
A partial supersession was written only in the newer record's prose six times, and 11 of the
|
||||
21 `supersedes-in-part:` links had no note on the record they change, so a reader who arrived
|
||||
at the older record by a citation read a rule that no longer held. Both directions are
|
||||
checked: the field on the newer record and the dated note on the older one, naming each other.
|
||||
The note is dated by the newer record's `date:`, and the older form of the note, which named a
|
||||
later record and was read by nothing, is refused so it cannot be a way round this check.
|
||||
"""
|
||||
for number, record in records.items():
|
||||
claimed = record["front"].get("supersedes-in-part", [])
|
||||
if isinstance(claimed, str):
|
||||
claimed = [claimed] if claimed else []
|
||||
for entry in claimed:
|
||||
match = ADR_FILE.match(os.path.basename(entry))
|
||||
if not match or match.group(1) not in records:
|
||||
failures.add("partial-supersession", rel(record["path"]), f"no such record: {entry}")
|
||||
continue
|
||||
older = records[match.group(1)]
|
||||
marks = [by for _, by in PARTIAL_MARK.findall(older["text"])]
|
||||
if number not in marks:
|
||||
failures.add(
|
||||
"partial-supersession",
|
||||
rel(older["path"]),
|
||||
f"ADR {number} supersedes this in part; this record carries no "
|
||||
f"'> **Superseded in part — YYYY-MM-DD, by [ADR {number}](…).**' note",
|
||||
)
|
||||
for line in NARROWED_MARK.findall(record["text"]):
|
||||
failures.add(
|
||||
"partial-supersession",
|
||||
rel(record["path"]),
|
||||
f"a note names a later record in the old form ({line.strip()[:60]}…); write it as "
|
||||
f"'> **Superseded in part — YYYY-MM-DD, by [ADR NNNN](…).**' and declare it in that "
|
||||
f"record's `supersedes-in-part:`",
|
||||
)
|
||||
for date, by in PARTIAL_MARK.findall(record["text"]):
|
||||
if by in records and date != str(records[by]["front"].get("date", "")):
|
||||
failures.add(
|
||||
"partial-supersession",
|
||||
rel(record["path"]),
|
||||
f"the note for ADR {by} is dated {date}; ADR {by} is dated "
|
||||
f"{records[by]['front'].get('date', 'nowhere')}",
|
||||
)
|
||||
if by not in records:
|
||||
failures.add("partial-supersession", rel(record["path"]), f"note names no such record: ADR {by}")
|
||||
continue
|
||||
names = records[by]["front"].get("supersedes-in-part", [])
|
||||
if isinstance(names, str):
|
||||
names = [names] if names else []
|
||||
if record["name"] not in [os.path.basename(str(n)) for n in names]:
|
||||
failures.add(
|
||||
"partial-supersession",
|
||||
rel(records[by]["path"]),
|
||||
f"ADR {number} says this supersedes it in part; this record's "
|
||||
f"`supersedes-in-part:` does not name it",
|
||||
)
|
||||
|
||||
|
||||
CLAUSES_FROM = "2026-10-11"
|
||||
DECISION_SECTION = re.compile(r"^## Decision[^\n]*\n(.*?)(?=^## |\Z)", re.M | re.S)
|
||||
CLAUSE = re.compile(r"^\d+\. \*\*", re.M)
|
||||
|
||||
|
||||
def check_clauses(failures, records):
|
||||
"""A record dated from 2026-10-11 writes its decision as numbered clauses (ADR 0290 rule 3).
|
||||
|
||||
The knowledge base reads a decision's clauses from the merged text, so a clause's wording is the reviewed
|
||||
one. 226 of 263 records written before then are prose, and their clauses had to be drafted and confirmed
|
||||
instead; a new record is not added to them.
|
||||
"""
|
||||
for number, record in records.items():
|
||||
front = record["front"]
|
||||
if front.get("status") != "accepted" or str(front.get("date", "")) < CLAUSES_FROM:
|
||||
continue
|
||||
section = DECISION_SECTION.search(record["text"])
|
||||
if not section or not CLAUSE.search(section.group(1)):
|
||||
failures.add(
|
||||
"clauses",
|
||||
rel(record["path"]),
|
||||
"dated from 2026-10-11, and its `## Decision` section has no numbered clause "
|
||||
"('1. **…**'); write the decision as numbered clauses with a bold lead",
|
||||
)
|
||||
|
||||
|
||||
def check_topics(failures, records):
|
||||
"""Every record names a topic of the reading order.
|
||||
|
||||
The topic is what puts a record in the reading order, so a record without one — or with one
|
||||
nobody defined — disappears from the generated list rather than appearing in the wrong place.
|
||||
That is the quiet failure, so it is the one checked. The topics are the ones
|
||||
`02-DECISIONS/README.md` writes down, read the way `index.py` reads them, so there is one list.
|
||||
"""
|
||||
from index import reading_order
|
||||
known = {topic for topic, _ in reading_order()}
|
||||
for number, record in sorted(records.items()):
|
||||
topic = record["front"].get("topic")
|
||||
if not topic:
|
||||
failures.add("topics", rel(record["path"]),
|
||||
"no topic, so it has no place in the reading order")
|
||||
elif topic not in known:
|
||||
failures.add("topics", rel(record["path"]),
|
||||
"topic %r is not one of: %s" % (topic, ", ".join(sorted(known))))
|
||||
|
||||
|
||||
def check_numbering(failures, records):
|
||||
"""The number in the filename is the number in the heading."""
|
||||
for number, record in records.items():
|
||||
heading = re.search(r"^# (\d+)\.", record["text"], re.M)
|
||||
if not heading:
|
||||
failures.add("numbering", rel(record["path"]), "no '# N. Title' heading")
|
||||
elif heading.group(1) != number.lstrip("0"):
|
||||
failures.add(
|
||||
"numbering",
|
||||
rel(record["path"]),
|
||||
f"filename says {number}, heading says {heading.group(1)}",
|
||||
)
|
||||
|
||||
|
||||
def check_progressive_insights(failures, records):
|
||||
"""A correction made inside a record is marked and dated, or it is a silent rewrite.
|
||||
|
||||
A record may be corrected in place when a *fact* in it went stale and the decision still
|
||||
stands (`02-DECISIONS/README.md`, "Progressive insight"). The whole safety of that allowance
|
||||
is that the correction is legible in the record rather than only in a diff nobody reads, so
|
||||
the form is what is checked here: every mention of an insight is the marker, the marker
|
||||
carries an ISO date, and that date is not earlier than the decision's own — an insight
|
||||
predating the decision it corrects is a copied marker, not a correction.
|
||||
|
||||
What this cannot check is an edit made with no marker at all. Nothing mechanical can; that
|
||||
one is the reviewer's, reading the diff. The check keeps the *marked* path honest so that an
|
||||
unmarked change stands out as the anomaly it is.
|
||||
"""
|
||||
phrase = re.compile(r"progressive insight", re.I)
|
||||
# Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
|
||||
# newlines will happily join an unrelated `**` far above to the marker below, reporting the
|
||||
# whole span between them. It did exactly that the first time this ran.
|
||||
# Trailing words after the date are allowed — "— 2026-09-26, correcting the one above." — so
|
||||
# an insight can say what it relates to. Only the date's presence and position are fixed.
|
||||
marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*")
|
||||
loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*")
|
||||
iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
for number, record in sorted(records.items()):
|
||||
text = record["text"]
|
||||
if not phrase.search(text):
|
||||
continue
|
||||
decided = str(record["front"].get("date", ""))
|
||||
good = [(m.start(), m.end(), m.group(1)) for m in marker.finditer(text)]
|
||||
|
||||
for m in loose.finditer(text):
|
||||
if any(s <= m.start() and m.end() <= e for s, e, _ in good):
|
||||
continue
|
||||
# A bold run carrying a link is discussing an insight — usually another record's —
|
||||
# rather than marking one. A marker never needs to cite anything.
|
||||
if "](" in m.group(0):
|
||||
continue
|
||||
failures.add("insights", rel(record["path"]),
|
||||
"a progressive insight is not in the dated marked form "
|
||||
"'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0))
|
||||
|
||||
for _, _, stamp in good:
|
||||
if decided and iso.match(decided) and stamp < decided:
|
||||
failures.add("insights", rel(record["path"]),
|
||||
"a progressive insight dated %s predates the decision (%s)"
|
||||
% (stamp, decided))
|
||||
|
||||
covered = [(s, e) for s, e, _ in good]
|
||||
for m in phrase.finditer(text):
|
||||
if any(s <= m.start() and m.end() <= e for s, e in covered):
|
||||
continue
|
||||
line = text.rfind("\n", 0, m.start()) + 1
|
||||
end = text.find("\n", m.end())
|
||||
whole = text[line:end if end != -1 else len(text)]
|
||||
if whole.lstrip().startswith("#"):
|
||||
continue
|
||||
# A line that also carries a link is discussing the rule, not marking a correction:
|
||||
# a marker never needs to cite anything, and a record that reasons about the policy
|
||||
# must be able to name it. Bare prose with no citation is the informal marking this
|
||||
# is here to catch.
|
||||
if "](" in whole:
|
||||
continue
|
||||
if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)):
|
||||
continue
|
||||
failures.add("insights", rel(record["path"]),
|
||||
"'progressive insight' appears unmarked; a correction is marked and "
|
||||
"dated, or it is a silent rewrite")
|
||||
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):
|
||||
@@ -495,18 +215,11 @@ def check_status_against_code(failures):
|
||||
|
||||
def main():
|
||||
failures = Failures()
|
||||
records = load_records()
|
||||
check_links(failures)
|
||||
check_rests_on(failures, records)
|
||||
check_live_citations(failures, records)
|
||||
check_supersession_symmetry(failures, records)
|
||||
check_partial_supersession(failures, records)
|
||||
check_clauses(failures, records)
|
||||
check_numbering(failures, records)
|
||||
check_topics(failures, records)
|
||||
check_no_records(failures)
|
||||
check_decisions_entries(failures)
|
||||
check_mentions(failures)
|
||||
check_status_against_code(failures)
|
||||
check_progressive_insights(failures, records)
|
||||
print(f"records: {len(records)} decision records checked")
|
||||
return failures.report()
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
# Words allowed — documents `words.py` does not hold to the glossary's retired words
|
||||
|
||||
Read by [`words.py`](words.py) ([ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
|
||||
Read by [`words.py`](words.py) (ADR 0244).
|
||||
One row per document. *Until* is the date by which it is reworded — past it, the row fails like the words
|
||||
themselves — or `kept` for a graduated research effort, which records what was said and keeps its words
|
||||
the way a decision record does. A row whose document no longer uses a retired word fails too, so this
|
||||
list only shrinks.
|
||||
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),
|
||||
which keeps a record's words as it was imported.
|
||||
|
||||
| Document | Until | Why |
|
||||
|---|---|---|
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/00-overview.md` | kept | the effort that studied these words; its findings quote and propose them as they stood before ADR 0244 |
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/01-the-concepts-in-use.md` | kept | as above: the inventory of the words in use |
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/02-the-domains.md` | kept | as above: the proposed domains, in the words of the day |
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md` | kept | as above: every clash names the words that clashed |
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md` | kept | as above: the check's own examples |
|
||||
| `01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md` | kept | as above: the proposed glossary, with the entries of the day |
|
||||
| `01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md` | kept | the effort that settled the word: it records the operator's ask in the words it was made in |
|
||||
|
||||
+25
-32
@@ -42,14 +42,14 @@ What is enforced:
|
||||
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; a research
|
||||
effort initiated, or an issue opened, on or after FROM (on or after LINES_FROM for the lines). Never
|
||||
02-DECISIONS/: a decision record keeps the words it was written with.
|
||||
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. `kept` instead of a date is allowed only for a
|
||||
graduated research effort, which is a record of what was said, like a decision record.
|
||||
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
|
||||
@@ -65,7 +65,7 @@ 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): research and issues from it on are held to it.
|
||||
# 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")
|
||||
@@ -168,21 +168,22 @@ def prose(text, glossary_file=False):
|
||||
return text
|
||||
|
||||
|
||||
def research_and_issues(since=FROM):
|
||||
"""The research efforts and issue reports dated on or after `since`, file by file."""
|
||||
def issues(since=FROM):
|
||||
"""The issue reports opened on or after `since`, file by file."""
|
||||
out = []
|
||||
for folder, key, first in (("01-RESEARCH", "initiated", "00-overview.md"), ("04-ISSUES", "opened", "00-report.md")):
|
||||
base = os.path.join(ROOT, folder)
|
||||
for effort in sorted(os.listdir(base)):
|
||||
head = os.path.join(base, effort, first)
|
||||
if not os.path.isfile(head):
|
||||
continue
|
||||
date = frontmatter_field(read(rel(head)), key)
|
||||
if not date or date < since:
|
||||
continue
|
||||
for name in sorted(os.listdir(os.path.join(base, effort))):
|
||||
if name.endswith(".md"):
|
||||
out.append("%s/%s/%s" % (folder, effort, name))
|
||||
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
|
||||
|
||||
|
||||
@@ -198,15 +199,13 @@ def checked_documents(since=FROM):
|
||||
for name in sorted(files):
|
||||
if name.endswith(".md"):
|
||||
docs.append(rel(os.path.join(base, name)))
|
||||
return docs + research_and_issues(since)
|
||||
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, or `kept` for a
|
||||
document that records what was said and must keep its words: only a research effort that has
|
||||
graduated may be kept, because once it has become a decision it is a record like one.
|
||||
`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)):
|
||||
@@ -219,14 +218,8 @@ def allowed():
|
||||
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 until == "kept":
|
||||
overview = os.path.join(os.path.dirname(doc), "00-overview.md")
|
||||
status = frontmatter_field(read(overview), "status") if os.path.isfile(os.path.join(ROOT, overview)) else None
|
||||
if not doc.startswith("01-RESEARCH/") or status != "graduated":
|
||||
problems.append("%s: %s is kept, but only a graduated research effort may be" % (ALLOWED, doc))
|
||||
continue
|
||||
elif not re.match(r"^\d{4}-\d{2}-\d{2}$", until):
|
||||
problems.append("%s: the allowance for %s has neither a date nor `kept`" % (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))
|
||||
|
||||
+86
-84
@@ -4,7 +4,7 @@ One name per thing. This page is the authority; where an older record says somet
|
||||
record keeps its words and this page says how to read them. It exists because the words kept drifting
|
||||
in conversation — "control plane", "controller" and "master" for one thing, "substrate" and
|
||||
"foundation" for another — and a mesh you cannot name precisely is a mesh two people describe differently. The rule and
|
||||
its check are [ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md);
|
||||
its check are ADR 0244;
|
||||
the domains are drawn in [to-be 49](../03-DESIGN/01-to-be/49-the-mesh-in-domains.md).
|
||||
|
||||
## How to read this page
|
||||
@@ -30,33 +30,33 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
|
||||
- **mesh** — the whole: the nodes, the modules assigned to them, the controller that decides and the
|
||||
records it keeps. One mesh per operator; a second mesh is a second everything.
|
||||
- **domain** — an area of the mesh that owns a set of concepts and the one word for each, as described
|
||||
in the section above ([ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
|
||||
in the section above (ADR 0244).
|
||||
ADR 0006 called seven of them the controller's *contexts*; they carry over as domains under the same
|
||||
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](../02-DECISIONS/0008-a-context-owns-its-store.md)).
|
||||
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`.
|
||||
- **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.
|
||||
- **node** — a machine the mesh has adopted and owns. A machine becomes a node when it joins
|
||||
([ADR 0004](../02-DECISIONS/0004-a-node-and-how-it-joins.md)), and from then on it runs the node-engine
|
||||
(ADR 0004), and from then on it runs the node-engine
|
||||
and is either **adopted** (the mesh holds what it found there as found) or **converged** (the mesh made
|
||||
it what it is) — [ADR 0100](../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).
|
||||
it what it is) — ADR 0100.
|
||||
Prose about a member of the mesh says node. A node is just a machine that has joined; being one
|
||||
implies nothing about what it runs.
|
||||
- **module** — the unit the mesh assigns: one named thing, defined by its manifest, that a node runs —
|
||||
a service, a container, files, packages, its own code — and the tools it serves
|
||||
([ADR 0040](../02-DECISIONS/0040-what-a-module-is.md)). Everything configurable on a node is a module.
|
||||
(ADR 0040). Everything configurable on a node is a module.
|
||||
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a **closed
|
||||
set** the mesh defines: a claim naming a seat outside the set is refused. A seat may **deliver a
|
||||
provision**, and its holder is then the mesh's answer for it when several modules provide it
|
||||
([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). The set, with who holds each
|
||||
(ADR 0126). The set, with who holds each
|
||||
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
|
||||
A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first
|
||||
promised as optional, then served, then required ([ADR 0246](../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)).
|
||||
promised as optional, then served, then required (ADR 0246).
|
||||
A node seat may also name **the paths its holder owns**, such as `/etc/hosts`: a claim whose module does
|
||||
not declare them is refused, and only a claimant declares them ([ADR 0287](../02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md)).
|
||||
not declare them is refused, and only a claimant declares them (ADR 0287).
|
||||
- **operator** — the person who runs the mesh, and the one the mesh talks to.
|
||||
*Not:* ~~master~~ (hq)
|
||||
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
|
||||
@@ -81,15 +81,15 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
|
||||
- **claim** — a module taking a spot on a seat: `claims: [{name, scope}]` in a manifest. A mesh-scoped
|
||||
exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the role
|
||||
it guards: the `mesh-controller`, `postgres` and `nats` modules claim the `mesh-controller`,
|
||||
`mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md),
|
||||
[ADR 0116](../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)).
|
||||
`mesh-store` and `mesh-broker` seats (ADR 0079,
|
||||
ADR 0116).
|
||||
- **setting** — a value a manifest declares and an assignment gives, one of the two ways a node varies
|
||||
a module ([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||
a module (ADR 0174).
|
||||
What a "flavor" once varied is a setting or a separate module.
|
||||
*Not:* ~~flavor~~
|
||||
- **preference** — a setting whose manifest gives it a default, because its value is true of the
|
||||
software and only tuned per node: a font size, a width
|
||||
([ADR 0262](../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)).
|
||||
(ADR 0262).
|
||||
A setting with no default is the operator's own value: a domain, an identity.
|
||||
*Not:* ~~tunable~~ (hq)
|
||||
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
|
||||
@@ -98,7 +98,7 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
|
||||
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a
|
||||
daemon — in any language the mesh has a toolchain for; never an image. A **tools bundle** speaks MCP to
|
||||
the tool runner. One module may declare several
|
||||
([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
(ADR 0188).
|
||||
- **tool** — one capability a module serves through the tool runner, addressed `<module>.<tool>`. A
|
||||
seat's **verb** is the same thing carried by a seat rather than a module.
|
||||
- **mesh-sdk** — the library a module's own code is written against, including the harness that serves a
|
||||
@@ -108,7 +108,7 @@ are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0
|
||||
one. A grant on the publish side and nothing else; a module that declares none calls nothing.
|
||||
- **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then
|
||||
the rest), `together`, or `record` (register the build, send it nowhere)
|
||||
([ADR 0236](../02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)).
|
||||
(ADR 0236).
|
||||
The controller's verb `upgrade` shows it.
|
||||
|
||||
## Core — the mesh's own machinery
|
||||
@@ -142,7 +142,7 @@ every other domain has something to run on. **Recorded by** the controller and t
|
||||
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
|
||||
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
|
||||
supervises it as a process beside the services it runs, never a container
|
||||
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
(ADR 0175,
|
||||
which called it "node tools"). Its mode on loopback is the mesh MCP server.
|
||||
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
|
||||
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account; the rename is
|
||||
@@ -151,21 +151,21 @@ every other domain has something to run on. **Recorded by** the controller and t
|
||||
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
|
||||
*Not:* ~~substrate~~
|
||||
- **genesis** — raising the foundation and the controller on an empty mesh, by the one installation
|
||||
done by hand ([ADR 0067](../02-DECISIONS/0067-genesis-is-a-pivot.md)).
|
||||
done by hand (ADR 0067).
|
||||
- **store** — the one postgres server. It holds the controller's own databases (`inventory`,
|
||||
`identity`, `licences`, each owned by one domain, ADR 0008) and every module's own database. One server,
|
||||
many databases — never one shared mesh database. Bare *store* means this and nothing else
|
||||
(see [Homonyms](#homonyms)).
|
||||
- **bus** — the mesh's own nervous system: NATS, one per mesh, carrying every link the mesh has —
|
||||
control, declarations, builds, events, tool calls ([ADR 0106](../02-DECISIONS/0106-the-bus-is-nats.md)).
|
||||
A module reaches it by requiring `mesh-bus` ([ADR 0128](../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md));
|
||||
control, declarations, builds, events, tool calls (ADR 0106).
|
||||
A module reaches it by requiring `mesh-bus` (ADR 0128);
|
||||
one that does not require it has no account on it. Held by the `mesh-broker` seat, which is named
|
||||
after the role rather than the server, so the server can change without the seat doing so. The `nats`
|
||||
module holds it.
|
||||
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It keeps
|
||||
running as an **ordinary provider** of the `amqp` provision, for modules that need a message broker
|
||||
of their own the way something needs a database
|
||||
([ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — it claims no
|
||||
(ADR 0131) — it claims no
|
||||
seat, is not foundation, is never raised at genesis, and a mesh that never installs it is complete.
|
||||
Say *the deprecated broker*, not *the compatibility broker* (it serves the mesh's own modules, not only
|
||||
the predecessor's) and not *the AMQP broker* (naming it after a protocol invites describing the bus by
|
||||
@@ -175,13 +175,13 @@ every other domain has something to run on. **Recorded by** the controller and t
|
||||
this; new prose says layer (see [Homonyms](#homonyms)).
|
||||
- **lease** and **epoch** — which controller may send (the lease, held in the bus and renewed), and the
|
||||
order of what it sent (the epoch, which a node reads before it accepts a declaration)
|
||||
([ADR 0229](../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)).
|
||||
(ADR 0229).
|
||||
|
||||
- **record mirror** — a key-value bucket on the bus that the controller writes from one kind of its
|
||||
records (nodes, assignments, walks, builds, settings), after each change commits in its store and only
|
||||
when the value differs, with one event per write. The store stays the record; the self-check compares
|
||||
the two and raises `mirror-drifted` on any difference
|
||||
([ADR 0289](../02-DECISIONS/0289-the-controller-mirrors-its-records-into-buckets-written-only-on-a-change-and-audited-against-its-store.md)). Shortened to *mirror* where the
|
||||
(ADR 0289). Shortened to *mirror* where the
|
||||
text is about the controller's records; the builder's *mirrored* `upstream` image (Change and delivery) is
|
||||
a different thing, a copy of an outside image in the artifact store.
|
||||
|
||||
@@ -195,23 +195,23 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
|
||||
- **assignment** — a module put on a node, with the settings that node gives it.
|
||||
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
|
||||
controller when that build registers the module, or ended with why when it does not
|
||||
([ADR 0261](../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md)).
|
||||
(ADR 0261).
|
||||
- **scope** — where a seat or a claim holds: `node`, `site` or `mesh`. `node` is also the scope's
|
||||
value in a manifest.
|
||||
- **capacity** — how many holders a seat takes. A capacity-1 seat is exclusive. A higher-capacity seat
|
||||
is a **bench**: several holders coexist. A **replicated** bench has one holder per node, each on record
|
||||
and each answering the same, like `mesh-dns-resolver`
|
||||
([ADR 0223](../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). A
|
||||
(ADR 0223). A
|
||||
**kinded** bench has holders that are different modules, each claiming one **kind**, and a verb's
|
||||
subject carries the kind; `channel` and `intake` are the only ones (ADR 0234).
|
||||
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
|
||||
holding the seat its family declares; *holding* is being the one — the login shell, the display session
|
||||
— on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||
— on that node (ADR 0176).
|
||||
The module that holds a seat is its **holder**.
|
||||
- **depends on a seat** — a module needing a seat held on its node by some module, without holding it.
|
||||
Derived from the resources it declares, never stated: a `service` depends on `node-service-manager`, a
|
||||
`package` on `node-package-manager`, a `container` on `node-container-runtime`
|
||||
([ADR 0207](../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)).
|
||||
(ADR 0207).
|
||||
Not a claim: a module claims a seat it holds and declares resources.
|
||||
- **declaration** — what the controller sends one node: every resource of every module assigned there,
|
||||
composed. The noun is this domain's; what a manifest says is that it *declares* (Module), and a manifest
|
||||
@@ -224,16 +224,16 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
|
||||
- **operator account** — the login name of the person who works on a node, stated on the node record;
|
||||
empty for a node nobody logs into. Everything the mesh places under a person's home is resolved
|
||||
against this account's home and owned by it
|
||||
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||
(ADR 0181).
|
||||
Not a name a manifest carries.
|
||||
- **agent account** — the login every coding agent on a node runs as, when the node names one of its own;
|
||||
stated on the node record at the controller's terminal, never by a verb or a setting. Empty means agents
|
||||
run as the operator account. It cannot become root without a person, and the node-engine judges that on
|
||||
every look ([ADR 0266](../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)).
|
||||
every look (ADR 0266).
|
||||
- **account group** — a group of the machine's group database that a module puts an account in, by
|
||||
declaring the account with that group and nothing else of it. Several modules may each add one to the
|
||||
same account; the mesh takes back only an account group it added
|
||||
([ADR 0252](../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md)).
|
||||
(ADR 0252).
|
||||
Never *membership*, which is the bus's word.
|
||||
|
||||
## Provisioning — one module serving another
|
||||
@@ -249,15 +249,15 @@ and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 02
|
||||
a module *the* provider of it.
|
||||
- **provider** and **consumer** — the module that provides a provision, and the module that requires
|
||||
it. Which of several providers serves a consumer is resolved
|
||||
([ADR 0084](../02-DECISIONS/0084-which-provider-serves-a-consumer.md)).
|
||||
(ADR 0084).
|
||||
- **pin** — a person's choice of provider for a consumer, which resolution then respects.
|
||||
- **endpoint** — where a consumer reaches its provider; an assignment binds it and says how far it
|
||||
reaches ([ADR 0138](../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)).
|
||||
reaches (ADR 0138).
|
||||
- **retire** — a provider stops serving a consumer the mesh no longer asks for; what it kept is deleted
|
||||
only by a person ([ADR 0230](../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)).
|
||||
only by a person (ADR 0230).
|
||||
- **share** — a folder one node offers to the mesh's other nodes over the private network: the provision
|
||||
`nfs-share`, provided by the holder of the node seat `node-nfs-server`, one share per folder
|
||||
([ADR 0263](../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md)).
|
||||
(ADR 0263).
|
||||
- **mount** — a share, or an occasional source, made visible at a path on one node by the holder of the
|
||||
node seat `node-mounts`, as a mount unit and an automount unit, mounted on access (ADR 0263).
|
||||
- **occasional source** — a share on a device that is not a node and comes and goes, such as a USB disk;
|
||||
@@ -274,31 +274,31 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
|
||||
- **credential** — anything that proves an identity: a password, a key, a token. A **pair credential**
|
||||
is the two ends of one credential, the consumer's and the provider's.
|
||||
- **secret** — a credential or other value the vault keeps sealed and the node-engine unseals into a
|
||||
file at apply ([ADR 0085](../02-DECISIONS/0085-a-secret-is-a-provision.md)).
|
||||
file at apply (ADR 0085).
|
||||
- **secret family** — a module's own secrets declared as one name ending in `-*`; each member is the prefix
|
||||
and a name (`smb-password-<source>`, one per source the module's settings name), given by its full name
|
||||
and never made by the mesh
|
||||
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
|
||||
(ADR 0283).
|
||||
- **vault** — the module that owns every secret (`mesh-vault`,
|
||||
[24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)).
|
||||
- **grant** — what an identity may call or reach, bounded by the provisions it requires
|
||||
([ADR 0225](../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)).
|
||||
(ADR 0225).
|
||||
- **bus account** and **membership** — a module's or a node's account on the bus, and the subjects it
|
||||
is issued. The bus server calls its accounts *users*; the mesh says bus account.
|
||||
- **rotate** — replacing a credential without a consumer holding one the provider does not know.
|
||||
- **licence** — a model-access account the licence manager holds and **binds** to a consumer, the
|
||||
coding agent on a node ([ADR 0183](../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)).
|
||||
coding agent on a node (ADR 0183).
|
||||
Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still
|
||||
`release`, an identifier to rename.
|
||||
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
|
||||
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
|
||||
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
|
||||
verb, so a change that is the terminal's alone is taken (issue 339, issue 340) — or, since
|
||||
[ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md),
|
||||
ADR 0277,
|
||||
proposed through a verb and set on the operator's warrant. In the controller's code it is a
|
||||
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
|
||||
agent can run as that account: the control-node, and a node whose agents run under an account of their own
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
|
||||
(ADR 0272).
|
||||
|
||||
## Change and delivery — a commit on its way to the nodes
|
||||
|
||||
@@ -311,10 +311,10 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
|
||||
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
|
||||
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
|
||||
([ADR 0238](../02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)).
|
||||
(ADR 0238).
|
||||
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
|
||||
**builder**. Any node may hold the seat, so there is no build node by nature
|
||||
([ADR 0237](../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)).
|
||||
(ADR 0237).
|
||||
*Not:* ~~build machine~~
|
||||
- **build queue** — the controller's queue of builds to run; one entry in it is a **build request**.
|
||||
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing
|
||||
@@ -324,14 +324,14 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
|
||||
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
|
||||
**artifact store**, an OCI registry that holds every kind as content-addressed blobs
|
||||
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
|
||||
(ADR 0156).
|
||||
An image is one kind of artifact, and a module is not an image. A package and an artifact are two
|
||||
protocols, not one store being weak ([ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md)).
|
||||
protocols, not one store being weak (ADR 0075).
|
||||
- **build source** — the files one artifact is built from, in one or more repositories: for a Go program
|
||||
its import closure (the directories of its packages, its Go module files) and its recipe; for any other
|
||||
artifact the paths its manifest names, by default its module's own directory. Each artifact of a build
|
||||
has its own, and a change rebuilds the artifacts whose build source it touches
|
||||
([ADR 0267](../02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md)).
|
||||
(ADR 0267).
|
||||
Older records (ADR 0015, ADR 0111) and designs say *build source* for the repository a module
|
||||
is built from; this entry narrows it to the files within, and that use reads as *the repository holding
|
||||
the build source*. Sharing a repository is not a dependency, and no build source is a whole repository
|
||||
@@ -341,20 +341,20 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
*Not:* ~~catalog~~ (hq)
|
||||
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
|
||||
being announced to one of its final states: one pull request, one status, one note
|
||||
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
||||
(ADR 0239).
|
||||
From its merge on it is delivered by the walk of the batch that answers it, and ends with that walk
|
||||
([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||
(ADR 0276).
|
||||
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
|
||||
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
|
||||
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
||||
[ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
ADR 0250,
|
||||
ADR 0270). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
mechanism, which the as-is designs describe; it is never a word for a delivery.
|
||||
*Not:* ~~deployment~~ (hq)
|
||||
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
|
||||
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
|
||||
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
|
||||
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
|
||||
ends in ([ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)).
|
||||
ends in (ADR 0270).
|
||||
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
|
||||
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
|
||||
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
|
||||
@@ -365,7 +365,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
*Not:* ~~change plan~~, ~~release plan~~
|
||||
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each
|
||||
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
|
||||
10 minutes after its first merge ([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||
10 minutes after its first merge (ADR 0276).
|
||||
- **batch** — the merges one merge window collected, planned once as one walk. For a repository merged more
|
||||
than once in it, the walk takes the latest commit, and the earlier merges are answered by it. Not *group*,
|
||||
which is a delivery group.
|
||||
@@ -373,7 +373,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
|
||||
- **walk** — the controller's sending of one batch's builds across nodes, at most one trunk commit per
|
||||
repository, tier by tier, one node first and judged at the first-node gate, then the rest (ADR 0236,
|
||||
[ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
|
||||
ADR 0276). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
|
||||
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
|
||||
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
|
||||
a module *rolls out* by its `roll` policy.
|
||||
@@ -383,7 +383,7 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
- **core module** and **leaf module** — a core module is one on the controller's own path (the controller,
|
||||
the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects
|
||||
the mesh's ability to reach a node at all; a leaf module is every other module. Decided by
|
||||
[ADR 0282](../02-DECISIONS/0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).
|
||||
ADR 0282.
|
||||
Not a *core component* (ADR 0236's controller, node-engine, tool runner and bus, each with a health probe):
|
||||
the core modules add mesh-delivery and the resolver.
|
||||
- **delivery time** — from a delivery's merge to every node of its deploy plan that is heard from running its
|
||||
@@ -405,7 +405,7 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
|
||||
**Uses:** node, module, node-engine (Core).
|
||||
|
||||
- **health** — a module's or a core component's statement of how it is alive and ready, which the
|
||||
node-engine judges ([ADR 0240](../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md)).
|
||||
node-engine judges (ADR 0240).
|
||||
- **relogin needed** — an account's health when the group database lists it in an account group and its
|
||||
running session, started before, does not have that group yet. Said by the node-engine as the module's
|
||||
resource of kind `account`; it clears at the first look after a new login (ADR 0252).
|
||||
@@ -413,14 +413,14 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
|
||||
secret or setting of the module, not given yet, without which a part cannot work. Not a fault: the
|
||||
controller checks it and says it as the condition `needs-operator`, naming the act, and the first-node gate
|
||||
reads it as *waits for a person*
|
||||
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
|
||||
(ADR 0283).
|
||||
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
|
||||
**finding**, before it becomes a condition.
|
||||
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
|
||||
and the watcher that raises a condition when it stops (to-be 45 §3).
|
||||
- **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity;
|
||||
raised and cleared only by observation
|
||||
([ADR 0231](../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)).
|
||||
(ADR 0231).
|
||||
What a wrapped dashboard raises is its own; the mesh's notion is a condition.
|
||||
*Not:* ~~alert~~ (hq)
|
||||
- **self-check** — the controller's own examination of the mesh, run on demand and on a schedule
|
||||
@@ -432,7 +432,7 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
|
||||
never counted as a repair.
|
||||
- **rehearsal** — a question the operator starts at the controller's terminal (`rehearse`) and answers on a
|
||||
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
|
||||
operator's decision ([ADR 0259](../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)).
|
||||
operator's decision (ADR 0259).
|
||||
Not a drill: nothing is broken.
|
||||
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened.
|
||||
- **witness** — what rolls a core component back when its new build does not become healthy: never the
|
||||
@@ -447,13 +447,13 @@ becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
|
||||
|
||||
- **data class** — how precious an item of data is, ranked by the operator: `irreplaceable`,
|
||||
`valuable`, `rebuildable`, `cache`; and `none` for a provision that keeps nothing of anybody's
|
||||
([ADR 0233](../02-DECISIONS/0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md)).
|
||||
(ADR 0233).
|
||||
- **backup** — a copy kept elsewhere by the node's `node-backup` holder; **restore point** — one backup
|
||||
at one moment; **restore** — bringing it back beside the live data, never over it.
|
||||
- **stream snapshot** — the bus's own backup of each stream, taken by the module holding `mesh-broker`
|
||||
([ADR 0235](../02-DECISIONS/0235-the-bus-is-backed-up-by-its-own-snapshot-of-each-stream.md)).
|
||||
(ADR 0235).
|
||||
- **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a
|
||||
person ([ADR 0232](../02-DECISIONS/0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md)).
|
||||
person (ADR 0232).
|
||||
|
||||
## Connectivity — how nodes and modules reach one another
|
||||
|
||||
@@ -463,19 +463,19 @@ it should not. **Recorded by** the controller. **Upstream of** Provisioning and
|
||||
**Uses:** node, machine, assignment and endpoint.
|
||||
|
||||
- **private network** — the mesh's own encrypted network between its nodes, on which every node has an
|
||||
address and a mesh name ([ADR 0226](../02-DECISIONS/0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md)).
|
||||
address and a mesh name (ADR 0226).
|
||||
The network the machine sits on without it is the **underlay**.
|
||||
*Not:* ~~overlay~~ (hq)
|
||||
- **anchor** and **hub** — the roles a node plays for the private network: the anchor is reachable from
|
||||
outside and every node reaches it; a hub relays for nodes that cannot reach each other directly.
|
||||
- **resolver** — a seat holder that answers the mesh's names; a node lists only the mesh's resolvers
|
||||
(ADR 0223), or, where it holds one, its own resolver, which asks them. **uplink** — a node's
|
||||
connection to the outside network, a seat ([ADR 0117](../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)). **hostname** — a node's own name,
|
||||
connection to the outside network, a seat (ADR 0117). **hostname** — a node's own name,
|
||||
a seat.
|
||||
- **split DNS** — resolving names by domain on one machine: a VPN's domains through the VPN's servers
|
||||
over its link, every other name through the mesh's resolvers. The provision `split-dns`, provided by
|
||||
the holder of the node seat `node-resolver`, the machine's **own resolver**, which exists only where
|
||||
something requires it ([ADR 0247](../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)).
|
||||
something requires it (ADR 0247).
|
||||
- **proxy** and **public name** — the module that answers a public name and forwards it to an
|
||||
endpoint on the private network.
|
||||
- **packet filter** — what the mesh enforces on a node about which packets pass, the
|
||||
@@ -504,26 +504,26 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
||||
`/usr/local/bin/mesh-cli <command line>` asks the controller to run one of its command lines. It asks the
|
||||
node-engine on its own machine, which says to the controller which account is asking; the controller runs the
|
||||
line as the **controller's terminal** or as an ordinary call
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
|
||||
(ADR 0272).
|
||||
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
|
||||
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
|
||||
alias alone. 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
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md),
|
||||
(ADR 0272,
|
||||
its insight of 2026-10-09).
|
||||
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
|
||||
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
|
||||
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
|
||||
holder of kind `desktop`) and not *bot* (that is one service's account).
|
||||
- **router** — the module that holds the `operator-channel` seat, orders the channels and checks every
|
||||
sender ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)).
|
||||
sender (ADR 0234).
|
||||
- **headline / verdict / explanation / resolved line** — what a condition says to the operator in plain
|
||||
words: a few words naming the thing and what is wrong; "Nothing for you to do." or "Needs you:" and the
|
||||
one thing the operator can do themselves (its **needs**); one or two sentences on what happened and what
|
||||
it means; the line said when it clears. Its **actions** are the answers a notification offers. Carried by
|
||||
the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer
|
||||
([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)).
|
||||
(ADR 0253).
|
||||
The router may override "Nothing for you to do." in what it says, from the **tracking line** (ADR 0285).
|
||||
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
|
||||
date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router
|
||||
@@ -534,12 +534,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
||||
published for that asker alone. The only thing an asker acts on (ADR 0259).
|
||||
- **proposal** — a change an agent, or anyone the bus admits, puts forward through a verb and that takes
|
||||
effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR
|
||||
0274), a trusted setting's layer ([ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)).
|
||||
0274), a trusted setting's layer (ADR 0277).
|
||||
A proposal changes nothing by itself; declined, expired or refused, it is discarded.
|
||||
- **approval mark** — a tool's or verb's declaration in its manifest (`needs-approval`) that each call to it runs
|
||||
only on the operator's warrant for that one call, with its exact arguments; the tool runner that serves it, or
|
||||
the controller for its own verbs, asks and checks
|
||||
([ADR 0286](../02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md)).
|
||||
(ADR 0286).
|
||||
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
|
||||
against its own list of the operator's identities: a **trusted** one is an operator message,
|
||||
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
|
||||
@@ -552,12 +552,12 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
||||
- **instruction section** — one named section of the managed instruction file, which every agent session on the
|
||||
nodes it is for reads first, as the mesh's word: set at the controller's terminal in the agent module's
|
||||
settings, or **proposed** by an agent and made active on each node only by the operator's warrant, which that node
|
||||
proves itself ([ADR 0274](../02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)). A hand-over between sessions is one.
|
||||
proves itself (ADR 0274). A hand-over between sessions is one.
|
||||
- **personal rule** — one short text in the operator's own words about how the operator wants their agent
|
||||
to work with them, registered for the operator (the **operator scope** of the agent module) and rendered
|
||||
into the agent's home on every node that names an operator account. Not a mesh-wide instruction section,
|
||||
which is the mesh's word about the mesh, set at the controller's terminal
|
||||
([ADR 0273](../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
|
||||
(ADR 0273).
|
||||
- **lesson** — an agent's proposal, recorded when the operator expresses frustration or corrects a way of
|
||||
working: the trigger (the operator's words), the cause (what the agent did) and the rule that would
|
||||
prevent it. A lesson is `proposed`, `accepted` or `declined`; an accepted lesson is a personal rule, and a
|
||||
@@ -569,15 +569,16 @@ Not a domain of the mesh but of the way it is built, listed because its words me
|
||||
**Recorded in** this repository, the issue tracker and the knowledge base. **Decided by** ADR 0019, 0080, 0244, 0275,
|
||||
0284, 0290, 0291.
|
||||
|
||||
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation in `01-RESEARCH/`;
|
||||
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation, held whole in the
|
||||
knowledge base and named "research NNN";
|
||||
its closing into a decision and a design; a design given to a code repository; a documented workflow
|
||||
in `00-META/process/`.
|
||||
- **decision record** — one numbered file in `02-DECISIONS/`, never rewritten: **superseded** by a later
|
||||
record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
|
||||
- **decision record** — one numbered record in the knowledge base, named "ADR NNNN", never rewritten:
|
||||
**superseded** by a later record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
|
||||
*record* has other meanings (see [Homonyms](#homonyms)).
|
||||
- **clause** — one numbered statement a decision record makes, with its own status (`proposed`, `in force`,
|
||||
`changed`, `superseded`): a later record may change one clause and leave the rest
|
||||
([ADR 0290](../02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md)). Not a *rule*, which is a line of
|
||||
(ADR 0290). Not a *rule*, which is a line of
|
||||
`00-META/how-we-build.md`.
|
||||
- **knowledge base** — the role that holds the decision records and research efforts whole (number, status, topic,
|
||||
typed links, clauses and reasoning, with every version), takes them as proposals, has them reviewed and accepted,
|
||||
@@ -586,7 +587,7 @@ Not a domain of the mesh but of the way it is built, listed because its words me
|
||||
of this repository.
|
||||
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
|
||||
- **issue** — a question about the mesh that someone must answer, numbered and tracked by the issue tracker, which
|
||||
holds it whole: its report, diagnosis and resolution included ([ADR 0291](../02-DECISIONS/0291-an-issue-lives-whole-in-the-issue-tracker.md));
|
||||
holds it whole: its report, diagnosis and resolution included (ADR 0291);
|
||||
the folders in `04-ISSUES/` are imported there and removed.
|
||||
An **incident** is a past event that taught a rule.
|
||||
- **issue tracker** — the role that holds the mesh's issues and hands out their numbers: the seat `issue-tracker`,
|
||||
@@ -595,20 +596,20 @@ Not a domain of the mesh but of the way it is built, listed because its words me
|
||||
*Not:* ~~ticketing system~~ (hq), ~~ticket system~~ (hq)
|
||||
- **duplicate check** — the issue tracker's judgement, when an issue is opened, of whether it asks a question an
|
||||
older issue not resolved already asks; a certain duplicate is closed `wontfix` with its report kept on the older
|
||||
issue ([ADR 0278](../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)).
|
||||
issue (ADR 0278).
|
||||
Its pass over the backlog is the verb `groom`.
|
||||
- **triage** — the issue tracker's judgement of an opened issue, made by the agent with no tools: whether it is a
|
||||
duplicate (the **duplicate check**), its **severity** and what it **needs**, a suggested owner, and the issues it
|
||||
depends on or relates to; it fills only what nobody gave ([ADR 0279](../02-DECISIONS/0279-the-issue-tracker-triages-every-new-issue-and-an-issue-is-ready-when-what-it-depends-on-is-delivered.md)).
|
||||
depends on or relates to; it fills only what nobody gave (ADR 0279).
|
||||
- **ready** — of an issue: every issue it depends on is wontfix, or resolved with its fix delivered.
|
||||
- **worker** — of an issue: the agent or person, with its node (who@node), that moved it to `diagnosing` or `fixing`
|
||||
and works on it; another is refused while the worker is current, unless it gives a reason to **take it over**. A
|
||||
worker with nothing done on the issue for a day is **stale**. **Fixing** is the status an issue is at while its fix
|
||||
is worked on; an issue is resolved only from it ([ADR 0284](../02-DECISIONS/0284-an-agent-moves-an-issue-to-fixing-under-its-own-name-and-an-issue-is-resolved-only-from-fixing.md)).
|
||||
is worked on; an issue is resolved only from it (ADR 0284).
|
||||
Not a *claim* or a *holder*, which are a module's on a seat.
|
||||
- **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](../02-DECISIONS/0285-an-issue-tracks-a-condition-by-its-key-and-every-message-about-the-condition-says-the-issue-and-where-it-stands.md)).
|
||||
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.
|
||||
|
||||
## Homonyms
|
||||
@@ -667,14 +668,15 @@ 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
|
||||
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a word
|
||||
retired here may still appear there, and the *Not:* lines are how to read it.
|
||||
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](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)):
|
||||
**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
|
||||
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
|
||||
research and issues dated from 2026-10-07 — code spans, quotations and link targets excepted — and on a
|
||||
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)*
|
||||
are copied into the catalogue as `retired-words`, whose own repository check holds the descriptions of
|
||||
the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery.
|
||||
Homonyms are checked by review.
|
||||
A decision record or research effort proposed to the knowledge base is held to the same words by its
|
||||
rules `retired-word` and `daemon-line`, which read this page (ADR 0290). Homonyms are checked by review.
|
||||
|
||||
+13
-13
@@ -3,7 +3,7 @@ status: canonical
|
||||
updated: 2026-08-23
|
||||
derives: knowledge-base constitution page
|
||||
decisions:
|
||||
- 02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md
|
||||
- "ADR 0020"
|
||||
---
|
||||
|
||||
# How we build
|
||||
@@ -37,13 +37,13 @@ incident behind it is not written down, and the fix is to write it down, not to
|
||||
| Rule | What it means |
|
||||
|---|---|
|
||||
| **Never write to a production database directly** | No insert, update, delete or schema statement executed against production by hand. Schema changes go through numbered migrations; data changes go through application code or the module's own capabilities. Raw statements skip every side effect the proper path has — events, audit, cache invalidation, fan-out. |
|
||||
| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. [ADR 0013](../02-DECISIONS/0013-schema-changes-are-numbered-migrations.md) |
|
||||
| **Every schema change is a migration** | Numbered, in the module's own language, compiled with it. Both a baseline for a fresh installation *and* an incremental migration for installations that already exist. If code references a column, the migration creating it must exist. ADR 0013 |
|
||||
| **Never bypass the pipeline** | No manual database edit, no manual restart as a workaround. Fix the cause and deploy. A workaround that works is a workaround that is never removed, and the next person cannot tell the node from its declaration. |
|
||||
| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** ([ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md), which supersedes [ADR 0012](../02-DECISIONS/0012-the-mesh-creates-no-symlinks.md)). The links the installer still reconciles are a migration, not a permission. |
|
||||
| **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** (ADR 0012, which supersedes ADR 0012). The links the installer still reconciles are a migration, not a permission. |
|
||||
| **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception — **including in this repository**. A documentation repository is not a lower tier of care; a decision record lands the same way a service does. |
|
||||
| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. [ADR 0023](../02-DECISIONS/0023-approval-is-the-checkpoint.md) |
|
||||
| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. ADR 0023 |
|
||||
| **Never open a pull request unprompted** | A permissions list saying it is allowed is not a request. |
|
||||
| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. [ADR 0010](../02-DECISIONS/0010-delivery.md), and §5. |
|
||||
| **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. ADR 0010, and §5. |
|
||||
|
||||
### A failed step must stop the steps after it — how it was earned
|
||||
|
||||
@@ -69,7 +69,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
|
||||
- **Every runtime variable is declared.** A variable the module reads and the manifest does not
|
||||
declare is invisible to the mesh: it will not be generated, injected, or audited.
|
||||
- **Provisioned credentials arrive through declared requirements**, never hardcoded in code,
|
||||
compose files or scripts. [ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
|
||||
compose files or scripts. ADR 0009
|
||||
- **Never install a package by hand.** A package is declared in the manifest and arrives the
|
||||
way every other package does. A hand-installed package is invisible to the mesh: it is not
|
||||
declared, not reproduced on the next node, and not present after a rebuild — and the node
|
||||
@@ -82,7 +82,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
|
||||
- **Every standalone application gets its own repository**, with a manifest at its root,
|
||||
registered as a build source. Creating an application directory in the monorepo is a
|
||||
convention violation and reviewers reject it.
|
||||
[ADR 0015](../02-DECISIONS/0015-applications-live-in-their-own-repository.md)
|
||||
ADR 0015
|
||||
|
||||
### Migrations
|
||||
|
||||
@@ -96,7 +96,7 @@ reported failure, nothing stopped, and the damage happened somewhere nobody was
|
||||
surface is regenerated from the mesh database; a local edit survives one synchronisation and is
|
||||
then silently overwritten, bringing back whatever it fixed. Use the mesh operation that owns
|
||||
the value. If unsure whether a file is managed, ask the tooling — the answer is not visible
|
||||
from the file. [ADR 0011](../02-DECISIONS/0011-managed-files-are-generated-never-edited.md)
|
||||
from the file. ADR 0011
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +121,7 @@ for them. Do not merge them into one module: they are delivered to different nod
|
||||
that must be assigned where half of it is unwanted is not a boundary either.
|
||||
|
||||
Coherence is a context. Delivery is a module. Relationships are edges, not folders.
|
||||
[ADR 0009](../02-DECISIONS/0009-modules-and-the-graph.md)
|
||||
ADR 0009
|
||||
|
||||
### Contexts integrate through the record, never through a shared schema
|
||||
|
||||
@@ -214,7 +214,7 @@ This document is governed. It does not change by commit message or unilateral de
|
||||
1. **Propose** — a change stating what rule is changing, why the current wording is
|
||||
inadequate, and what reviewed it.
|
||||
2. **Review** — sign-off by reviewers who are not the proposer.
|
||||
3. **Record** — the change is a decision and gets a record in [`02-DECISIONS/`](../02-DECISIONS/), because a
|
||||
3. **Record** — the change is a decision and gets a decision record in the knowledge base (playbook [02](process/02-graduation.md)), because a
|
||||
rule the mesh enforces is architecturally significant.
|
||||
4. **Sync** — playbook [`process/05-constitution-sync.md`](process/05-constitution-sync.md)
|
||||
publishes the derived page. An unsynced rule is a rule the mesh does not enforce, whatever
|
||||
@@ -226,7 +226,7 @@ No drive-by edits. Every change traces to a recorded decision.
|
||||
a design meeting with at least two node operators — which has never been met and cannot be, as
|
||||
there is one operator. A rule that cannot be satisfied is not a high standard; it is a rule
|
||||
everything silently violates. Recorded here as resolved in favour of what is achievable, and
|
||||
what has in fact been practised ([ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md)).
|
||||
what has in fact been practised (ADR 0022).
|
||||
|
||||
---
|
||||
|
||||
@@ -243,7 +243,7 @@ process. Absence of an override means these rules apply unmodified.
|
||||
## 8. Code quality
|
||||
|
||||
*Absorbed 2026-08-26 from the enforced page, which carried these rules while this document did
|
||||
not — [ADR 0022](../02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md).*
|
||||
not — ADR 0022.*
|
||||
|
||||
**These rules are recorded because they are enforced, not because this repository earned them.**
|
||||
Every other rule here states the incident or measurement behind it. These state nothing,
|
||||
@@ -272,7 +272,7 @@ Data access, business logic and the interface layer are separate.
|
||||
|
||||
*Scope: the mesh's services and surfaces. Tier 0 is a statically linked binary that must depend
|
||||
on nothing installed first, and is written in Go —
|
||||
[ADR 0005](../02-DECISIONS/0005-the-node-host.md).*
|
||||
ADR 0005.*
|
||||
|
||||
- TypeScript throughout; no new untyped JavaScript.
|
||||
- Strict, with no implicit `any` and no unchecked index access.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
How work moves through HQ, and who may do what. In industry terms this is **spec-driven
|
||||
development, with provenance**: the decision is the why, the design doc is the spec, `code:`
|
||||
names the implementation, and the lab beds are the conformance tests — and unlike the common
|
||||
form, the chain itself is checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md),
|
||||
[0081](../../02-DECISIONS/0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)). Every other document in this folder is a
|
||||
form, the chain itself is checked (ADR 0080,
|
||||
ADR 0081). Every other document in this folder is a
|
||||
playbook: trigger, who runs it, steps, outputs. Engineers and agents follow the same
|
||||
playbooks; agents must not act outside them.
|
||||
|
||||
@@ -19,11 +19,13 @@ playbooks; agents must not act outside them.
|
||||
## The knowledge flow
|
||||
|
||||
```
|
||||
idea ──► 01-RESEARCH ──► decision (02-DECISIONS/) ──► 03-DESIGN/01-to-be ──► built (code repo)
|
||||
│ │ │
|
||||
│ │ └─► 03-DESIGN/00-as-is once shipped
|
||||
idea ──► research ──► decision ──► 03-DESIGN/01-to-be ──► built (code repo)
|
||||
│ │ │
|
||||
│ │ └─► 03-DESIGN/00-as-is once shipped
|
||||
│ └────► abandoned (recorded, kept)
|
||||
└─(small/obvious, still recorded in 02-DECISIONS)──────────► 03-DESIGN directly
|
||||
└─(small/obvious, still a decision record)──────────► 03-DESIGN directly
|
||||
|
||||
research and decisions: in the knowledge base (knowledge-base.reserve, propose, ask, get, holds)
|
||||
|
||||
symptom ──► 04-ISSUES ──► diagnosis ──► code-repo fix and/or design amendment
|
||||
|
||||
@@ -37,7 +39,7 @@ how-we-build.md ──► constitution sync ──► knowledge base ──► i
|
||||
| Layer | What it is | Changes when |
|
||||
|---|---|---|
|
||||
| `00-as-is/` | The mesh that exists today. Describes shipped behaviour, including behaviour nobody would choose again. | Something ships, or an as-is claim is found to be wrong. |
|
||||
| `01-to-be/` | The mesh being built toward. Every statement traceable to a record in `02-DECISIONS/`. | A decision is taken or amended. |
|
||||
| `01-to-be/` | The mesh being built toward. Every statement traceable to a decision record in the knowledge base. | A decision is taken or amended. |
|
||||
|
||||
A to-be document that ships does not move. Its as-is counterpart is written or updated, the
|
||||
to-be document's frontmatter goes to `implemented`, and both stand — one describing what runs,
|
||||
@@ -61,23 +63,24 @@ the expensive half.
|
||||
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
|
||||
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, and a `graduated`
|
||||
research overview that does not say what it became. Run it with `records.py`, `index.py` and
|
||||
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 (`sh merge-check.sh` runs all four). 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](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)). What the checks cannot see — that code work actually started from a
|
||||
(ADR 0275). What the checks cannot see — that code work actually started from a
|
||||
handoff — is held by playbooks [04](04-build-handoff.md) and [07](07-feature-branches.md):
|
||||
a feature branch exists because a design or an issue sent it.
|
||||
|
||||
## Status lives in frontmatter, an issue's in the tracker
|
||||
|
||||
Research overviews, design docs and decision records each carry their status as
|
||||
YAML frontmatter (schemas in the section READMEs and playbooks). An issue's status lives in the
|
||||
Design docs carry their status as YAML frontmatter (schema in playbook [02](02-graduation.md)).
|
||||
Research efforts and decision records carry theirs in the knowledge base, which refuses a wrong one at
|
||||
`propose` (ADR 0290). An issue's status lives in the
|
||||
issue tracker, which refuses a wrong status at the write, and its report here carries only
|
||||
the day it was opened (playbook [03](03-issues.md)). There are **no central status
|
||||
files** and no decision ledger. **Every decision is a record in
|
||||
[`02-DECISIONS`](../../02-DECISIONS/)** — if it is worth recording it is worth a record, and if
|
||||
it is not worth a record it is not recorded. Cross-cutting views, the decision index included,
|
||||
are generated on demand by the `hq-status` skill and never written to disk
|
||||
([ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)).
|
||||
the knowledge base** — if it is worth recording it is worth a record, and if
|
||||
it is not worth a record it is not recorded. Cross-cutting views are generated on demand by the
|
||||
`hq-status` skill and never written to disk (ADR 0248); the decisions of a topic are
|
||||
`knowledge-base.holds` with that topic.
|
||||
|
||||
@@ -5,25 +5,34 @@ design. Also: an as-is document that raises a question nobody can answer.
|
||||
|
||||
**Who runs it.** Anyone.
|
||||
|
||||
**Where it lives.** A research effort lives whole in the knowledge base, the seat `knowledge-base`, reached
|
||||
through the mesh MCP server from any node and any folder (ADR 0290). This repository holds none: an effort
|
||||
is written with the knowledge base's verbs, never as a folder here.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Take the next free number: highest `01-RESEARCH/NNN-*` plus one, zero-padded to three
|
||||
digits. Never skip or reuse a number.
|
||||
2. Create `01-RESEARCH/NNN-descriptive-name/` and a `00-overview.md` in it carrying:
|
||||
1. **Read what is known first.** `knowledge-base.ask` with the question in plain words returns the decision
|
||||
records and research efforts that bear on it; `knowledge-base.get` with `kind: research` and a number
|
||||
reads one effort, and `reasoning: true` (or `section` with a document's name) reads its documents.
|
||||
2. **Take a number.** `knowledge-base.reserve` with `kind: research` and `for`: the effort's title. The
|
||||
number comes from the issue tracker's counter and is spent whether or not the effort is ever proposed;
|
||||
one never proposed is withdrawn after a week. Never pick a number by hand.
|
||||
3. **Propose it.** `knowledge-base.propose` with `kind: research`, the `number` reserve gave, a one-line
|
||||
`title`, `status: active`, `date` (the day it began), and `sections`: its documents in order, each a
|
||||
`name` and its markdown `text`. The first is `00-overview.md`: **what** is being investigated, **why**,
|
||||
and **what it touches**. Name what it touches as `links` of kind `cites` (`"0276"`, `"research 045"`,
|
||||
`"issue 365"`, or a design's path), not only in prose.
|
||||
4. **Do the research** in further documents of the same effort: notes, option analyses, evidence, draft
|
||||
designs. Each change is `knowledge-base.propose` again with the effort whole and `why`, one line saying
|
||||
why this version. Every version is kept; `knowledge-base.history` lists them and gives the difference
|
||||
between two.
|
||||
|
||||
```yaml
|
||||
---
|
||||
status: active
|
||||
initiated: YYYY-MM-DD
|
||||
touches: [] # areas, subsystems or as-is design docs the effort bears on
|
||||
---
|
||||
```
|
||||
|
||||
Then a short prose summary: **what** is being investigated, **why**, and **what it
|
||||
touches**.
|
||||
3. Do the research in further documents in the same folder — notes, option analyses, evidence,
|
||||
draft designs. Anything goes. Keep the summary in `00-overview.md` current as the effort changes
|
||||
shape.
|
||||
`propose` refuses an effort that breaks a rule that judges one record (an unknown status, a link to
|
||||
nothing, a retired word of the glossary, a daemon's line), naming every reason at once and writing nothing.
|
||||
The knowledge base then reviews the proposal itself, through a judgement that sees only the record, the
|
||||
records it links and the rules; on `changes` the effort stays proposed with the findings, and its author
|
||||
proposes it again. An effort that needs the operator's word is proposed with `needs-operator: true` and
|
||||
waits for the operator's Approve on a channel that proves who answered.
|
||||
|
||||
## What makes research worth reading
|
||||
|
||||
@@ -37,12 +46,21 @@ carries the whole lesson without naming anything.
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not put status in prose. It lives in `00-overview.md`'s frontmatter, and the prose must not restate it.
|
||||
- Do not set `became:` while the effort is open — playbook 02 sets it at closure.
|
||||
- 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: the knowledge base is the only place one
|
||||
is read, and `records.py` refuses a new path under `01-RESEARCH/`.
|
||||
|
||||
## Closing
|
||||
|
||||
An effort never just stops. It closes through playbook [02](02-graduation.md) as `graduated`
|
||||
or `abandoned`, always with `became:` pointing at what it turned into. Nothing is deleted —
|
||||
or `abandoned`, always with a `became` link to what it turned into. Nothing is deleted —
|
||||
what was rejected, and why, is the more expensive half to rediscover.
|
||||
|
||||
## How it is checked
|
||||
|
||||
The rules on an effort are the knowledge base's: `propose` refuses one that breaks a rule that judges one
|
||||
record (the module's table test holds one case per rule), and the rules that judge the whole record set run
|
||||
after every acceptance and raise a condition when one fails. That this repository holds no effort is
|
||||
`records.py`'s `no-records` check, on every pull request.
|
||||
|
||||
@@ -4,22 +4,20 @@
|
||||
|
||||
**Who runs it.** Anyone, with the decision recorded before the design moves.
|
||||
|
||||
**Where a decision lives.** A decision record lives whole in the knowledge base, the seat `knowledge-base`,
|
||||
reached through the mesh MCP server from any node and any folder (ADR 0290): its number, title, topic,
|
||||
status, date, deciders, its clauses, its typed links, its reasoning and every version. This repository holds
|
||||
none, and keeps no copy. What was decided is read with `knowledge-base.ask` (a question in plain words),
|
||||
`knowledge-base.get` (one record by number; `reasoning: true` for its why) and `knowledge-base.holds` (the
|
||||
clauses in force for a topic or a record).
|
||||
|
||||
## Graduating research
|
||||
|
||||
1. **Check it against GENESIS.** An effort graduates only if its conclusion is traceable to
|
||||
[`mission.md`](../mission.md), [`context.md`](../context.md) and
|
||||
[`effect.md`](../effect.md). If it conflicts, either the effort is wrong or GENESIS is —
|
||||
say which, in writing, before proceeding.
|
||||
2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free
|
||||
number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence,
|
||||
not assertion, and record the options that were rejected — that is the half worth keeping.
|
||||
**A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
|
||||
tracks it and the owner who will do it, in the sentence that defers it ("tracked by issue N"), or the
|
||||
deferral is not accepted:
|
||||
a deferral nothing tracks reads as done
|
||||
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
|
||||
ADR 0244's code renames). [`cycle.py`](../checks/cycle.py) holds every accepted record dated from
|
||||
2026-10-09, and an older one corrected under the rule, to it.
|
||||
2. **Record the decision** (below, *Writing a decision record*).
|
||||
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
|
||||
|
||||
```yaml
|
||||
@@ -28,27 +26,83 @@
|
||||
status: designed
|
||||
code: [] # owning code repo(s); set at build handoff, empty before
|
||||
updated: YYYY-MM-DD
|
||||
decisions: [02-DECISIONS/NNNN-....md]
|
||||
decisions:
|
||||
- "ADR NNNN"
|
||||
---
|
||||
```
|
||||
|
||||
4. **Close the effort.** Set the effort's `00-overview.md` frontmatter to `status: graduated` and
|
||||
`became:` pointing at the design document and the decision record.
|
||||
4. **Close the effort.** `knowledge-base.propose` the effort again, whole, with `status: graduated` and a
|
||||
`became` link to the decision record (`"NNNN"`) and to the design document (its path). An effort that
|
||||
stops without a decision is proposed with `status: abandoned` and a `became` link to what replaced it.
|
||||
|
||||
## Writing a decision record
|
||||
|
||||
1. **Take a number.** `knowledge-base.reserve` with `kind: decision` and `for`: the record's title. The
|
||||
number comes from the issue tracker's counter and is spent whether or not the record is accepted; one
|
||||
never proposed is withdrawn after a week. Never pick a number by hand.
|
||||
2. **Propose it.** `knowledge-base.propose` with the `number`, a one-line `title` in plain language, its
|
||||
`topic` (one of the six of the reading order, [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)),
|
||||
`date` (when the decision was taken, not when it was written down), `deciders`, and:
|
||||
- `clauses`: what was decided, in order, each one statement opening with a bold lead
|
||||
(`"**What is decided.** …"`);
|
||||
- `sections`: the reasoning, in order: `Context` (what was true, with evidence), `Considered Options`
|
||||
(numbered, each with why it was rejected), `Consequences` (what follows, including what got harder),
|
||||
`How it is checked` (a rule states how it is checked), `References`;
|
||||
- `links`: `extends` for a record it builds on, `supersedes` for one it replaces whole, `changes` with
|
||||
the clause it replaces part of (`"0276 §4"`, and `from` for the clause of this record that does it),
|
||||
`cites` for any record, effort, issue or design it names;
|
||||
- `homes`: the documents of this repository that cite it, such as the design's path;
|
||||
- `needs-operator: true` when it holds only once the operator approves it.
|
||||
|
||||
State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"* outranks *"the
|
||||
dependency rule is not followed"*. Record the options that were rejected — that is the half worth keeping.
|
||||
3. **A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
|
||||
tracks it, in the sentence that defers it ("tracked by issue N"), or the deferral is not accepted: a
|
||||
deferral nothing tracks reads as done
|
||||
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
|
||||
ADR 0244's code renames). `propose` refuses it (rule `deferral`).
|
||||
4. **Review and acceptance.** `propose` refuses, naming every reason at once and writing nothing, a record
|
||||
that breaks one of the rules that judge one record: a link to nothing, a clause without a bold lead, a
|
||||
superseded record cited alone, an unknown topic, a retired word of the glossary, a daemon's line, a
|
||||
deferral no issue tracks. The knowledge base then reviews the proposal itself, through a judgement that
|
||||
sees only the record, the records it links and the rules, and answers `approve` or `changes`. On
|
||||
`changes` the record stays proposed with the findings: revise it by proposing it again, whole, with `why`.
|
||||
On `approve` it is accepted, or, when it says `needs-operator`, it waits for the operator's Approve on a
|
||||
channel that proves who answered. Nothing proposed is in force. `knowledge-base.get` shows a record's
|
||||
reviews and the operator's ask when it waits for one.
|
||||
|
||||
## Changing a decision
|
||||
|
||||
**A record's meaning is never rewritten.** A decision is changed by a new record, and a correction of fact
|
||||
by a progressive insight.
|
||||
|
||||
- **Reversing a decision, changing its scope, rejecting an option it accepted, or making a consequence false
|
||||
that a later record cites** is a new record that `supersedes` the old one whole, or `changes` the clauses
|
||||
it replaces. On its acceptance the old record or clauses become `superseded` or `changed`, and every
|
||||
answer says by what. The test is not how large the edit looks; it is whether a reader who acted on the
|
||||
old text would now be wrong about *what was decided* rather than about *a detail the decision did not
|
||||
rest on*.
|
||||
- **A progressive insight** corrects a fact a record asserted that turned out wrong while the decision it
|
||||
supports stays right: a count taken before anyone measured, a file named that does not exist. It is
|
||||
`knowledge-base.propose` of the accepted record with `insight`: what was found, what the record said
|
||||
before, and what it says instead. It is a new version, marked as one, dated, and reviewed like a
|
||||
proposal; it adds and never quietly replaces; the decision, the options weighed and the consequences
|
||||
stand untouched. If the correction changes what was decided, it is not an insight — write the new record.
|
||||
- **Where a later record changes the mechanism an older one states** without reversing it, the later
|
||||
record `changes` the older one's clause, so a reader who arrives at the older record by a citation sees
|
||||
what changed it since.
|
||||
|
||||
## Amending an existing design
|
||||
|
||||
A design changes only through a decision.
|
||||
|
||||
1. Write the decision record. If it reverses an earlier one, the earlier record's `status:`
|
||||
becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its reasoning is never rewritten**. If the
|
||||
earlier record is sound and only a *fact* in it went stale, that is a **progressive insight**,
|
||||
corrected in place and marked in the record rather than superseded
|
||||
([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)).
|
||||
2. Edit the to-be design document and set `updated:` to today.
|
||||
1. Write the decision record (above). If it reverses an earlier one, it `supersedes` it.
|
||||
2. Edit the to-be design document, add the record to its `decisions:` as `"ADR NNNN"`, and set `updated:`
|
||||
to today.
|
||||
3. If the amendment came from an issue, give the issue the document path in the issue tracker:
|
||||
`amended-design` on its `issue-tracker.move` to `resolved`, or `issue-tracker.link` with kind
|
||||
`design` before then — never in the report's frontmatter
|
||||
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
|
||||
(ADR 0275).
|
||||
|
||||
## When something ships
|
||||
|
||||
@@ -68,3 +122,15 @@ 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
|
||||
under `02-DECISIONS/` other than its README.
|
||||
|
||||
## How it is checked
|
||||
|
||||
The rules on a record are the knowledge base's: `propose` refuses one that breaks a rule that judges one
|
||||
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
|
||||
order's six topics, which the knowledge base reads.
|
||||
|
||||
@@ -22,7 +22,7 @@ incident someone must **clear**.
|
||||
## The issue tracker holds the number and the status
|
||||
|
||||
Every issue is opened in the **issue tracker**, the module `mesh-issues` holding the seat `issue-tracker` on the
|
||||
control-node ([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md),
|
||||
control-node (ADR 0275,
|
||||
[to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md)). Its number comes from one counter advanced by
|
||||
compare-and-set, so two authors opening at the same moment never get the same one. **A number is never taken by
|
||||
reading `main` or the open pull requests** — that is how numbers collided (issue 155: two records sharing a number
|
||||
@@ -73,7 +73,7 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
**An issue about a condition** links the condition's key at once: `issue-tracker.link` with kind `condition` and
|
||||
the key `mesh-controller.conditions` gives. Every message about that condition then names the issue and where it
|
||||
stands; one no issue links says "No issue tracks this yet." An agent that silences a condition because an issue
|
||||
tracks it links that issue first ([ADR 0285](../../02-DECISIONS/0285-an-issue-tracks-a-condition-by-its-key-and-every-message-about-the-condition-says-the-issue-and-where-it-stands.md)).
|
||||
tracks it links that issue first (ADR 0285).
|
||||
|
||||
**An issue wholly in the tracker** is done at this step.
|
||||
2. Investigate. **Before you start, move the issue to `diagnosing` with `by`**: who works on it and its node, as
|
||||
@@ -87,14 +87,14 @@ import read, which every imported issue names (`imported.commit` in `issue-track
|
||||
`take-over` saying why, which is noted on the issue. A stale worker may be taken over from without a reason.
|
||||
Name the fix in `fixed-by` when it merges, with a move to `fixing` that gives `by` again; the issue stays
|
||||
`fixing` until the acceptance is proven live
|
||||
([ADR 0284](../../02-DECISIONS/0284-an-agent-moves-an-issue-to-fixing-under-its-own-name-and-an-issue-is-resolved-only-from-fixing.md),
|
||||
[ADR 0281](../../02-DECISIONS/0281-an-issue-knows-whether-its-fix-is-delivered-and-a-fix-stuck-after-its-merge-is-loud.md)).
|
||||
(ADR 0284,
|
||||
ADR 0281).
|
||||
4. Resolve. `issue-tracker.move` to `resolved`, which the tracker takes only from `fixing`, with `fixed-by`, and, if the root cause was a design gap,
|
||||
run playbook [02](02-graduation.md) and give `amended-design`. **A core issue** — its `located-in`
|
||||
names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build
|
||||
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
|
||||
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
|
||||
([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md));
|
||||
(ADR 0237);
|
||||
`issue-tracker.move` refuses the move without it. `wontfix` needs a `reason`.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
## Steps
|
||||
|
||||
1. **Confirm the design is settled.** Its frontmatter reads `status: designed`, and every
|
||||
claim in it traces to a record in [`02-DECISIONS/`](../../02-DECISIONS/). An open question in the text is a
|
||||
claim in it traces to a decision record in the knowledge base (`knowledge-base.get` for each entry of
|
||||
its `decisions:`). An open question in the text is a
|
||||
reason to run playbook [01](01-research.md), not to start building around it.
|
||||
2. **Name the owner.** Set `code:` in the design's frontmatter to the repositories from
|
||||
[`repos.md`](../repos.md). If the repository does not exist yet, add it to `repos.md` in
|
||||
@@ -26,7 +27,7 @@
|
||||
- Every merge is a human checkpoint, without exception.
|
||||
- What the handoff leaves for later is an issue with an owner, named in the design or the decision that
|
||||
leaves it — never a sentence saying "later". The design's `code:` names who owns what is built; the issue
|
||||
names who owns what is not. [`cycle.py`](../checks/cycle.py) checks the decision record
|
||||
names who owns what is not. the knowledge base's rule `deferral` checks the decision record when it is proposed
|
||||
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
|
||||
- Never open a pull request unprompted. A permissions list saying it is allowed is not a
|
||||
request.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Playbook 05 — Constitution sync
|
||||
|
||||
Implements [ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md): HQ is
|
||||
Implements ADR 0021: HQ is
|
||||
the source of the constitution, and the knowledge-base page is derived, never edited.
|
||||
|
||||
**Trigger.** [`how-we-build.md`](../how-we-build.md) changed a rule that the mesh enforces at
|
||||
|
||||
@@ -25,7 +25,7 @@ Three questions, answered from the machine:
|
||||
## The steps
|
||||
|
||||
1. **Name what it provides and requires**, if anything. A name is what a consumer is coupled to,
|
||||
not the role it plays ([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
||||
not the role it plays (ADR 0027):
|
||||
`postgres-database`, not `database`. Most modules provide nothing and require nothing — an
|
||||
application is usually a leaf.
|
||||
|
||||
@@ -103,5 +103,5 @@ time and the scaffolding was not.**
|
||||
there and nowhere else.
|
||||
- **A failing test is kept, not skipped.** It is the reproduction.
|
||||
- **Never rotate during an adoption.** Rotation is a separate act, afterwards, deliberately.
|
||||
- **A data directory is never removed by the mesh** ([ADR 0030](../../02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md)),
|
||||
- **A data directory is never removed by the mesh** (ADR 0030),
|
||||
and that protects against the mesh only — not against a disk or a mistaken command.
|
||||
|
||||
@@ -43,7 +43,7 @@ One feature is **one branch name**, **one worktree per repo**, **one MR per repo
|
||||
4. **Finish, then publish.** When the whole feature is done — every repo, tests green — push
|
||||
every branch and open **one MR per touched repo**, together.
|
||||
5. **Merge promptly, once approved.** Every merge into `main` is notified and approved
|
||||
([ADR 0023](../../02-DECISIONS/0023-approval-is-the-checkpoint.md)); once it is, merge —
|
||||
(ADR 0023); once it is, merge —
|
||||
do not leave it sitting. The branch is deleted on merge.
|
||||
6. **Leave nothing behind.** After the MRs merge, no `feat/<slug>` branch and no `.work/<slug>`
|
||||
worktree survive.
|
||||
|
||||
+9
-9
@@ -15,30 +15,30 @@ and a forge address is an operational detail (see [`README`](../README.md)).
|
||||
| Repository | Owns |
|
||||
|---|---|
|
||||
| `hal` | The monorepo — the node runtime, the module catalogue, the delivery machinery, and the bootstrap scripts. Every core module lives here. |
|
||||
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped ([ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md)); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
|
||||
| `hq` | This repository, under the company organisation — mission, research, design, decisions, issue diagnosis. Company-scoped (ADR 0019); the mesh is its first product. The source of truth for *why*. Carries no implementation. |
|
||||
| *(one per application)* | Every standalone application, site or side-project gets its own repository, with `module.yml` at the root. Registered with the mesh as a build source; built and deployed by the same pipeline as anything in the monorepo. |
|
||||
| `migration` | **Private.** The record of one installation replacing the predecessor mesh with this one: the runbook, a dated log of every step and what it cost, the per-service data procedures, the readiness checks, and the scripts. Private because it is the opposite of this repository in every way that matters — it names machines, addresses, ports and paths, because a procedure that cannot be followed is not one. Where hq asks *what did we decide and why*, that repository answers *what happened on the machines, in what order, and what to do next*. Its `HANDOFF.md` is where somebody picking the work up starts. |
|
||||
|
||||
## What the mesh becomes
|
||||
|
||||
[ADR 0019](../02-DECISIONS/0019-how-this-repository-works.md) records the repositories the
|
||||
ADR 0019 records the repositories the
|
||||
monorepo decomposes into. **`mesh-host`, `mesh-controller`, `mesh-catalog`, `mesh-lab`, `mesh-sdk` and `mesh-tools` exist
|
||||
so far** — the lab was built first ([ADR 0016](../02-DECISIONS/0016-the-lab.md)). The tiered
|
||||
so far** — the lab was built first (ADR 0016). The tiered
|
||||
decomposition below is the planned shape; the repositories built to date do not map onto it
|
||||
one-for-one — `mesh-catalog`, `mesh-sdk` and `mesh-tools` exist where the table names
|
||||
`mesh-foundation` and `mesh-surfaces`, and reconciling the two is itself still ahead.
|
||||
|
||||
| Repository | Tier | Holds |
|
||||
|---|---|---|
|
||||
| `mesh-host` | 0 | **exists.** The node-engine — one statically linked binary, requiring nothing present ([ADR 0005](../02-DECISIONS/0005-the-node-host.md)) |
|
||||
| `mesh-host` | 0 | **exists.** The node-engine — one statically linked binary, requiring nothing present (ADR 0005) |
|
||||
| `mesh-foundation` | 1 | the four pinned services, as declarations |
|
||||
| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built ([ADR 0006](../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| `mesh-controller` | 2 | **exists.** The controller and its contexts — one of seven built (ADR 0006) |
|
||||
| `mesh-surfaces` | 3 | tools, web, cli |
|
||||
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile ([ADR 0039](../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)). |
|
||||
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile (ADR 0039). |
|
||||
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
|
||||
|
||||
Tier 4's shape is open, and deliberately so: see ADR 0019 and
|
||||
[research 005](../01-RESEARCH/005-domain-grouping/00-overview.md).
|
||||
research 005.
|
||||
|
||||
## What lives where inside the monorepo
|
||||
|
||||
@@ -56,7 +56,7 @@ Named by role, because the layout is itself part of the as-is design — see
|
||||
## Why applications do not live in the monorepo
|
||||
|
||||
A standalone application in the monorepo is a convention violation, and reviewers reject it.
|
||||
The reasoning is recorded in [`02-DECISIONS/0010`](../02-DECISIONS/0015-applications-live-in-their-own-repository.md):
|
||||
The reasoning is recorded in ADR 0015:
|
||||
the mesh installs, provisions for, and ships an application through exactly the same machinery
|
||||
whether or not its source sits beside the mesh's own — so co-location buys nothing and costs
|
||||
the monorepo's review cadence.
|
||||
@@ -67,7 +67,7 @@ Each module is a standalone package that consumes its dependencies from the priv
|
||||
not from a sibling directory. The workspace was removed after it caused build-versus-development
|
||||
divergence — a workspace member importing another resolved to local unbuilt source in the
|
||||
pipeline and to a published version in development. Recorded in
|
||||
[`02-DECISIONS/0007`](../02-DECISIONS/0014-no-npm-workspace.md).
|
||||
ADR 0014.
|
||||
|
||||
Consequence, and it is a real one: a cross-package change is two steps — publish, then consume
|
||||
— and a repository-wide `npm install` does not exist.
|
||||
|
||||
Reference in New Issue
Block a user