Numbers are identity; the reading order is a generated, checked index

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.
This commit is contained in:
2026-08-28 23:39:18 +02:00
parent 333356cff3
commit b4607dfc03
27 changed files with 234 additions and 9 deletions
+5 -1
View File
@@ -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
+108
View File
@@ -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())
+20
View File
@@ -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()