Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24
@@ -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
|
||||
|
||||
|
||||
@@ -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 = "<!-- index:start -->"
|
||||
END = "<!-- index: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())
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-08-22
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-02-25
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-07-12
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-26
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-04-03
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-08-23
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-05-14
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-06-04
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-07-10
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-08-28
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: checking it
|
||||
status: accepted
|
||||
date: 2026-08-24
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: checking it
|
||||
status: accepted
|
||||
date: 2026-08-24
|
||||
deciders: jochen
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-07-10
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-08-23
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-08-26
|
||||
deciders: jochen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-08-26
|
||||
deciders: jochen
|
||||
|
||||
+59
-3
@@ -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
|
||||
```
|
||||
|
||||
<!-- index:start -->
|
||||
|
||||
### 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)
|
||||
|
||||
<!-- index:end -->
|
||||
|
||||
Reference in New Issue
Block a user