diff --git a/00-META/checks/README.md b/00-META/checks/README.md index db781d6..4a1e7c2 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -1,7 +1,9 @@ # Checks ``` -python3 00-META/checks/records.py +python3 00-META/checks/records.py structure: links, citations, supersession, topics +python3 00-META/checks/index.py the reading order in 02-DECISIONS/README.md is current +python3 00-META/checks/index.py --write regenerate it ``` Non-zero exit on any problem, so it can be a gate rather than a report. @@ -22,6 +24,8 @@ indistinguishable from one that cannot. | `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 | | `numbering` | the number in the filename is the number in the heading | — | +| `topics` | every record names a topic the index knows | — | +| *(index.py)* | the written reading order matches what the records say | — | ## What is deliberately not checked diff --git a/00-META/checks/index.py b/00-META/checks/index.py new file mode 100644 index 0000000..22a2f80 --- /dev/null +++ b/00-META/checks/index.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Generate the decision index, and check the written one still matches. + +A number identifies a record and never changes, so the folder listing is creation order rather +than reading order. The index is what carries the path — and it is written rather than only +generated on demand, because a reader on a forge sees the folder and not a command. + +The objection to a written index is that it drifts. That objection is answered by checking it +rather than by refusing to write one, which is `how-we-build` §5: a rule states how it is +checked. + + python3 00-META/checks/index.py --write regenerate it + python3 00-META/checks/index.py fail if it is stale +""" + +import glob +import io +import os +import re +import sys + +README = "02-DECISIONS/README.md" +START = "" +END = "" + +# The reading order. Topics a record may belong to, in the order somebody would learn the system. +TOPICS = [ + ("the mesh", "What the mesh is"), + ("the tiers", "Its tiers, from the bottom up"), + ("what runs on it", "What runs on them, and how it gets there"), + ("building it", "How it is built"), + ("checking it", "How it is checked"), + ("how we work", "How we work"), +] + + +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(rs): + known = {t for t, _ in TOPICS} + lines = [START, ""] + 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("") + stray = [r for r in rs if r["topic"] not in known] + if stray: + lines.append("### Unfiled") + lines.append("") + for r in stray: + lines.append("- **%s** — [%s](%s) — `topic:` is %r, which is not one of %s" % ( + r["number"], r["title"], r["file"], r["topic"], ", ".join(sorted(known)))) + lines.append("") + lines.append(END) + return "\n".join(lines) + + +def main(): + text = io.open(README, encoding='utf-8').read() + wanted = render(records()) + + if START not in text or END not in text: + print("index: %s has no index markers (%s / %s)" % (README, START, END)) + return 1 + + current = text[text.index(START):text.index(END) + len(END)] + if "--write" in sys.argv: + if current == wanted: + print("index: already current") + return 0 + io.open(README, 'w', encoding='utf-8').write(text.replace(current, wanted, 1)) + print("index: written") + return 0 + + if current != wanted: + print("index: %s is stale. Regenerate it:\n" + " python3 00-META/checks/index.py --write" % README) + return 1 + print("index: current") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/00-META/checks/records.py b/00-META/checks/records.py index 4730d40..e36f96e 100644 --- a/00-META/checks/records.py +++ b/00-META/checks/records.py @@ -251,6 +251,25 @@ def check_supersession_symmetry(failures, records): ) +def check_topics(failures, records): + """Every record names a topic the index knows. + + The topic is what puts a record in the reading order, so a record without one — or with one + nobody defined — disappears from the index rather than appearing in the wrong place. That is + the quiet failure, so it is the one checked. + """ + known = {"the mesh", "the tiers", "what runs on it", "building it", "checking it", + "how we work"} + 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(): @@ -273,6 +292,7 @@ def main(): check_live_citations(failures, records) check_supersession_symmetry(failures, records) check_numbering(failures, records) + check_topics(failures, records) print(f"records: {len(records)} decision records checked") return failures.report() diff --git a/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md b/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md index 6b82f90..0e437fa 100644 --- a/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md +++ b/02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md @@ -1,4 +1,5 @@ --- +topic: the mesh status: accepted date: 2026-08-22 deciders: jochen diff --git a/02-DECISIONS/0002-nodes-communicate-over-a-broker.md b/02-DECISIONS/0002-nodes-communicate-over-a-broker.md index 564a1ff..b548c9b 100644 --- a/02-DECISIONS/0002-nodes-communicate-over-a-broker.md +++ b/02-DECISIONS/0002-nodes-communicate-over-a-broker.md @@ -1,4 +1,5 @@ --- +topic: the mesh status: accepted date: 2026-02-25 deciders: jochen diff --git a/02-DECISIONS/0003-agents-are-persistent-employees.md b/02-DECISIONS/0003-agents-are-persistent-employees.md index f6683a5..5bbe516 100644 --- a/02-DECISIONS/0003-agents-are-persistent-employees.md +++ b/02-DECISIONS/0003-agents-are-persistent-employees.md @@ -1,4 +1,5 @@ --- +topic: the mesh status: accepted date: 2026-07-12 deciders: jochen diff --git a/02-DECISIONS/0004-a-node-and-how-it-joins.md b/02-DECISIONS/0004-a-node-and-how-it-joins.md index 094a971..1725bbc 100644 --- a/02-DECISIONS/0004-a-node-and-how-it-joins.md +++ b/02-DECISIONS/0004-a-node-and-how-it-joins.md @@ -1,4 +1,5 @@ --- +topic: the tiers status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0005-the-node-host.md b/02-DECISIONS/0005-the-node-host.md index ac7875e..7afc763 100644 --- a/02-DECISIONS/0005-the-node-host.md +++ b/02-DECISIONS/0005-the-node-host.md @@ -1,4 +1,5 @@ --- +topic: the tiers status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0006-the-substrate-and-the-control-plane.md b/02-DECISIONS/0006-the-substrate-and-the-control-plane.md index 875c67b..2401471 100644 --- a/02-DECISIONS/0006-the-substrate-and-the-control-plane.md +++ b/02-DECISIONS/0006-the-substrate-and-the-control-plane.md @@ -1,4 +1,5 @@ --- +topic: the tiers status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0007-connectivity.md b/02-DECISIONS/0007-connectivity.md index d4cd531..086d889 100644 --- a/02-DECISIONS/0007-connectivity.md +++ b/02-DECISIONS/0007-connectivity.md @@ -1,4 +1,5 @@ --- +topic: the tiers status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0008-a-context-owns-its-store.md b/02-DECISIONS/0008-a-context-owns-its-store.md index 155962d..b3d1015 100644 --- a/02-DECISIONS/0008-a-context-owns-its-store.md +++ b/02-DECISIONS/0008-a-context-owns-its-store.md @@ -1,4 +1,5 @@ --- +topic: the tiers status: accepted date: 2026-08-26 deciders: jochen diff --git a/02-DECISIONS/0009-modules-and-the-graph.md b/02-DECISIONS/0009-modules-and-the-graph.md index e9db517..d127ba9 100644 --- a/02-DECISIONS/0009-modules-and-the-graph.md +++ b/02-DECISIONS/0009-modules-and-the-graph.md @@ -1,4 +1,5 @@ --- +topic: what runs on it status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0010-delivery.md b/02-DECISIONS/0010-delivery.md index 716586a..74f1b73 100644 --- a/02-DECISIONS/0010-delivery.md +++ b/02-DECISIONS/0010-delivery.md @@ -1,4 +1,5 @@ --- +topic: what runs on it status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0011-managed-files-are-generated-never-edited.md b/02-DECISIONS/0011-managed-files-are-generated-never-edited.md index f7752ef..6bcb1cd 100644 --- a/02-DECISIONS/0011-managed-files-are-generated-never-edited.md +++ b/02-DECISIONS/0011-managed-files-are-generated-never-edited.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-04-03 deciders: jochen diff --git a/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md b/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md index 40fe34e..6d513a6 100644 --- a/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md +++ b/02-DECISIONS/0012-the-mesh-creates-no-symlinks.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-08-23 deciders: jochen diff --git a/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md b/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md index 8f42d2e..cb7519e 100644 --- a/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md +++ b/02-DECISIONS/0013-schema-changes-are-numbered-migrations.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-05-14 deciders: jochen diff --git a/02-DECISIONS/0014-no-npm-workspace.md b/02-DECISIONS/0014-no-npm-workspace.md index 8914acb..1140d37 100644 --- a/02-DECISIONS/0014-no-npm-workspace.md +++ b/02-DECISIONS/0014-no-npm-workspace.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-06-04 deciders: jochen diff --git a/02-DECISIONS/0015-applications-live-in-their-own-repository.md b/02-DECISIONS/0015-applications-live-in-their-own-repository.md index 565fb55..8ad1396 100644 --- a/02-DECISIONS/0015-applications-live-in-their-own-repository.md +++ b/02-DECISIONS/0015-applications-live-in-their-own-repository.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-07-10 deciders: jochen diff --git a/02-DECISIONS/0016-the-lab.md b/02-DECISIONS/0016-the-lab.md index 4de914d..4b0b6b0 100644 --- a/02-DECISIONS/0016-the-lab.md +++ b/02-DECISIONS/0016-the-lab.md @@ -1,4 +1,5 @@ --- +topic: building it status: accepted date: 2026-08-28 deciders: jochen diff --git a/02-DECISIONS/0017-a-test-defends-a-decision.md b/02-DECISIONS/0017-a-test-defends-a-decision.md index d2e43de..57ce326 100644 --- a/02-DECISIONS/0017-a-test-defends-a-decision.md +++ b/02-DECISIONS/0017-a-test-defends-a-decision.md @@ -1,4 +1,5 @@ --- +topic: checking it status: accepted date: 2026-08-24 deciders: jochen diff --git a/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md b/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md index 7871c7a..04d9d32 100644 --- a/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md +++ b/02-DECISIONS/0018-a-picture-is-read-from-what-runs.md @@ -1,4 +1,5 @@ --- +topic: checking it status: accepted date: 2026-08-24 deciders: jochen diff --git a/02-DECISIONS/0019-how-this-repository-works.md b/02-DECISIONS/0019-how-this-repository-works.md index 385eec4..56cf26c 100644 --- a/02-DECISIONS/0019-how-this-repository-works.md +++ b/02-DECISIONS/0019-how-this-repository-works.md @@ -1,4 +1,5 @@ --- +topic: how we work status: accepted date: 2026-08-28 deciders: jochen @@ -77,11 +78,25 @@ settled. Everything else belongs in the design document, where the reasoning is **There is no ledger** — no separate document summarising, ranking or tracking decisions. A chronological view is generated from frontmatter, which is what a ledger was actually for. -**The numbering is the flow here too.** Records are ordered the way somebody would learn the -system — what the mesh is, then its tiers from the bottom up, then what runs on them and how it -gets there, then how it is built, how it is checked, and how we work. **Not chronologically**: the -date is in the frontmatter and a consolidated record holds decisions taken across a week, so -ordering by age would order by an accident that no longer exists. +**A number identifies a record and never changes.** It is not a position, and it cannot be +both — a position moves when the set changes, and an identity that moves is not one. + +That is not a preference. Records are referenced from **outside** this repository: code +comments, commit messages, the knowledge base. Renumbering once cost 96 references across two +code repositories, and nothing in either would have failed to compile — the comments would +simply have pointed at the wrong reasoning, which is worse than a broken link because nothing +reports it. + +**So the reading order lives in a generated index**, from each record's `topic:` — what the mesh +is, then its tiers from the bottom up, then what runs on them and how it gets there, then how it +is built, how it is checked, and how we work. + +**And the index is written, not only generated on demand.** A reader looking at the folder on a +forge sees the folder, not a command. The objection to a written index is that it drifts, and +that is answered by **checking** it rather than by refusing to write one — which is §5's own +rule: a rule states how it is checked. A record with no topic, or a topic nobody defined, fails +the same check, because the quiet failure is a record that vanishes from the order rather than +appearing in the wrong place. **The design layer is what you read.** These records explain *why* a thing is as it is. They are not a description of the system, and needing to read them to understand it would mean the design diff --git a/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md b/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md index 3e199c6..c43cbdd 100644 --- a/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md +++ b/02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md @@ -1,4 +1,5 @@ --- +topic: how we work status: accepted date: 2026-07-10 deciders: jochen diff --git a/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md b/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md index 82c4ec5..9317c7a 100644 --- a/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md +++ b/02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md @@ -1,4 +1,5 @@ --- +topic: how we work status: accepted date: 2026-08-23 deciders: jochen diff --git a/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md b/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md index 25506f6..7b7a608 100644 --- a/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md +++ b/02-DECISIONS/0022-the-constitution-absorbs-what-is-enforced.md @@ -1,4 +1,5 @@ --- +topic: how we work status: accepted date: 2026-08-26 deciders: jochen diff --git a/02-DECISIONS/0023-approval-is-the-checkpoint.md b/02-DECISIONS/0023-approval-is-the-checkpoint.md index 80ae446..2ec3fad 100644 --- a/02-DECISIONS/0023-approval-is-the-checkpoint.md +++ b/02-DECISIONS/0023-approval-is-the-checkpoint.md @@ -1,4 +1,5 @@ --- +topic: how we work status: accepted date: 2026-08-26 deciders: jochen diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index fee22e0..b3a5767 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -62,6 +62,62 @@ rather than guessing. ## Index -The index is **generated, not maintained** — run the `hq-status` skill, which reads the -frontmatter of every record. A hand-written index drifts from the folder it describes, and -this one had already done so after a single addition. +**A number identifies a record and never changes.** Records are referenced from outside this +repository — code comments, commit messages — so a number that moves invalidates them silently. +Renumbering once cost 96 references across two code repositories, and that is why the numbers +are now fixed. + +So the folder is in creation order, and **the reading order lives here.** It is generated from +each record's `topic:` and written, because a reader looking at the folder on a forge sees the +folder rather than a command. The objection to a written index is that it drifts — which is +answered by checking it rather than by refusing to write one: + +``` +python3 00-META/checks/index.py --write regenerate +python3 00-META/checks/index.py fail if stale +``` + + + +### What the mesh is + +- **0001** — [The mesh brokers capabilities; nodes host; agents think](0001-mesh-brokers-nodes-host-agents-think.md) +- **0002** — [Nodes communicate over a message broker, not over HTTP](0002-nodes-communicate-over-a-broker.md) +- **0003** — [An agent is a persistent employee, not an instance of a pool](0003-agents-are-persistent-employees.md) + +### Its tiers, from the bottom up + +- **0004** — [A node, and how it joins](0004-a-node-and-how-it-joins.md) +- **0005** — [The node host](0005-the-node-host.md) +- **0006** — [The substrate and the control plane](0006-the-substrate-and-the-control-plane.md) +- **0007** — [Connectivity](0007-connectivity.md) +- **0008** — [A context owns its store, exclusively](0008-a-context-owns-its-store.md) + +### What runs on them, and how it gets there + +- **0009** — [Modules and the graph](0009-modules-and-the-graph.md) +- **0010** — [Delivery](0010-delivery.md) + +### How it is built + +- **0011** — [Managed files are generated onto nodes and never edited there](0011-managed-files-are-generated-never-edited.md) +- **0012** — [The mesh creates no symlinks — a derived file is a copy](0012-the-mesh-creates-no-symlinks.md) +- **0013** — [Schema and state changes are numbered migrations, in the same language as the code](0013-schema-changes-are-numbered-migrations.md) +- **0014** — [No workspace — each module is a standalone package consuming published dependencies](0014-no-npm-workspace.md) +- **0015** — [Applications live in their own repository; the monorepo is for the mesh](0015-applications-live-in-their-own-repository.md) +- **0016** — [The lab](0016-the-lab.md) + +### How it is checked + +- **0017** — [A test defends a decision](0017-a-test-defends-a-decision.md) +- **0018** — [A picture of a system is read from the system, never from what asked for it](0018-a-picture-is-read-from-what-runs.md) + +### How we work + +- **0019** — [How this repository works](0019-how-this-repository-works.md) +- **0020** — [The mesh is governed by a constitution, injected where work is decided](0020-the-mesh-is-governed-by-a-constitution.md) +- **0021** — [HQ is the source of the mesh constitution](0021-hq-is-the-source-of-the-constitution.md) +- **0022** — [The constitution absorbs what is already enforced](0022-the-constitution-absorbs-what-is-enforced.md) +- **0023** — [The approval is the checkpoint, not the second pair of hands](0023-approval-is-the-checkpoint.md) + +