Files
hq/00-META/checks/index.py
T

116 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""The reading order of the decision records: its topics are written, its list is generated.
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.
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.
"""
import glob
import io
import os
import re
import sys
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.
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.
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"),
]
def reading_order(text=None):
"""The topics, in order, as (topic, label) pairs, from the README."""
if text is None:
text = io.open(README, encoding='utf-8').read()
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():
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)
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)))
for p in problems:
print("index: " + p)
if problems:
return 1
print("index: %d topics, %d records, no stored list" % (len(topics), len(rs)))
return 0
if __name__ == "__main__":
sys.exit(main())