From b4607dfc03cd66da9b979790df2b3f7ac37cdf07 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 28 Aug 2026 23:39:18 +0200 Subject: [PATCH] Numbers are identity; the reading order is a generated, checked index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decided after measuring what renumbering actually costs: 96 references in code comments across two repositories, none of which would have failed to compile. They would have pointed at the wrong reasoning, which is worse than a broken link because nothing reports it. So a number identifies a record and never changes. It cannot also be a position -- a position moves when the set changes, and an identity that moves is not one. The reading order moves into an index generated from each record's `topic:`. Six topics, in the order somebody learns the system. The index is WRITTEN rather than only generated on demand, which reverses what this repository previously said. The reason it said otherwise is that a hand-written index drifts -- but a reader looking at the folder on a forge sees the folder, not a command, and the drift objection is answered by checking rather than by refusing to write one. That is §5's own rule: a rule states how it is checked. Two checks, both confirmed to bite. index.py fails when the written order no longer matches the records. records.py fails when a record has no topic or one nobody defined -- the quiet failure being a record that vanishes from the order rather than appearing in the wrong place. --- 00-META/checks/README.md | 6 +- 00-META/checks/index.py | 108 ++++++++++++++++++ 00-META/checks/records.py | 20 ++++ ...01-mesh-brokers-nodes-host-agents-think.md | 1 + .../0002-nodes-communicate-over-a-broker.md | 1 + .../0003-agents-are-persistent-employees.md | 1 + 02-DECISIONS/0004-a-node-and-how-it-joins.md | 1 + 02-DECISIONS/0005-the-node-host.md | 1 + ...006-the-substrate-and-the-control-plane.md | 1 + 02-DECISIONS/0007-connectivity.md | 1 + 02-DECISIONS/0008-a-context-owns-its-store.md | 1 + 02-DECISIONS/0009-modules-and-the-graph.md | 1 + 02-DECISIONS/0010-delivery.md | 1 + ...anaged-files-are-generated-never-edited.md | 1 + .../0012-the-mesh-creates-no-symlinks.md | 1 + ...-schema-changes-are-numbered-migrations.md | 1 + 02-DECISIONS/0014-no-npm-workspace.md | 1 + ...plications-live-in-their-own-repository.md | 1 + 02-DECISIONS/0016-the-lab.md | 1 + .../0017-a-test-defends-a-decision.md | 1 + .../0018-a-picture-is-read-from-what-runs.md | 1 + .../0019-how-this-repository-works.md | 25 +++- ...-the-mesh-is-governed-by-a-constitution.md | 1 + ...21-hq-is-the-source-of-the-constitution.md | 1 + ...e-constitution-absorbs-what-is-enforced.md | 1 + .../0023-approval-is-the-checkpoint.md | 1 + 02-DECISIONS/README.md | 62 +++++++++- 27 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 00-META/checks/index.py 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) + +