The operator approved Phase 5. Decides what the design left open: the build seat runs the merge check, the facts live in the artifact store, the merge gate composes the mesh as it is and with the change and judges only what the change adds, a replay lives where its incident is, and the controller's tests run a bus of their own at the mesh's release. A core issue now resolves only with a replay or a stated reason, checked by cycle.py.
252 lines
12 KiB
Python
252 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""The development cycle, checked.
|
|
|
|
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
|
|
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
|
|
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
|
|
frontmatter, because status lives in frontmatter and nowhere else.
|
|
|
|
What is enforced:
|
|
|
|
design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a
|
|
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
|
|
design without a decision -- and once `in-progress` or `implemented` it names
|
|
its owning code (`code:`) -- no development without a design that says where.
|
|
issues a known `status:`; once `located`, `located-in:` names the owner;
|
|
once `resolved`, `fixed-by:` says what fixed it (prose counts --
|
|
"nothing, the capability existed" is an answer). And no two records share a
|
|
number -- the number is how a record is cited.
|
|
replays a core issue -- one opened from 2026-10-07 whose `located-in:` names a core repository --
|
|
resolves only with `replay:` (the replay's id in mesh-lab's replays register) or
|
|
`replay-none:` (why no replay is possible): ADR 0237, to-be 45 §9. A replay is what
|
|
proves a fix fails before and passes after; without one, a fixed class comes back
|
|
through a different door (research 031: four such chains in one week).
|
|
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
|
target it names exists.
|
|
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
|
frontmatter, a research overview, an issue report, a 00-META document, or another
|
|
record's extends/supersedes chain. A decision nothing points at is one nobody will
|
|
find by following pointers -- which is how records go stale in people's heads.
|
|
|
|
Deliberately NOT enforced: `resolved` issues may leave `located-in` empty (a symptom that
|
|
turned out not to be a defect has no owner), and as-is docs need no decisions (they
|
|
describe what exists, not what was decided).
|
|
|
|
python3 00-META/checks/cycle.py
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
|
|
ISSUE_STATUSES = {"open", "diagnosing", "located", "resolved", "wontfix"}
|
|
RESEARCH_STATUSES = {"active", "graduated", "abandoned"}
|
|
|
|
# The core, as to-be 45 names it, by where its code lives: the controller, the node-engine, the node
|
|
# tools and the console, the SDK's loops, and the catalogue's bus, forge (the announcer of merges) and
|
|
# build agent. A `located-in:` entry naming any of these makes an issue a core issue.
|
|
CORE = re.compile(r"\b(mesh-controller|mesh-host|mesh-tools|mesh-sdk|node-tools|"
|
|
r"mesh-catalog\s+modules/(nats|gitea|build-agent))\b")
|
|
|
|
# The day the rule began (ADR 0237): issues opened before it are not held to it.
|
|
REPLAYS_FROM = "2026-10-07"
|
|
REPLAY_ID = re.compile(r"^R\d+\b")
|
|
|
|
|
|
def rel(path):
|
|
return os.path.relpath(path, ROOT)
|
|
|
|
|
|
def frontmatter(path):
|
|
"""The YAML block between the first two --- lines, as {key: raw-value-string}.
|
|
|
|
Minimal on purpose, like records.py: enough for the fields these checks read. A list
|
|
value (block or inline) is joined into its items; a scalar stays a string.
|
|
"""
|
|
text = open(path, encoding="utf-8").read()
|
|
m = re.match(r"^---\n(.*?)\n---", text, re.S)
|
|
if not m:
|
|
return None
|
|
front, out, key = m.group(1), {}, None
|
|
for line in front.split("\n"):
|
|
item = re.match(r"^\s+-\s*(.+?)\s*$", line)
|
|
if item and key:
|
|
out[key].append(item.group(1))
|
|
continue
|
|
kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line)
|
|
if not kv:
|
|
continue
|
|
key, value = kv.group(1), kv.group(2).strip()
|
|
if value.startswith("[") and value.endswith("]"):
|
|
out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()]
|
|
elif value == "":
|
|
out[key] = [] # a block list may follow; stays [] if nothing does
|
|
else:
|
|
out[key] = value
|
|
return out
|
|
|
|
|
|
def listy(front, key):
|
|
v = front.get(key)
|
|
if v is None:
|
|
return []
|
|
return v if isinstance(v, list) else ([v] if str(v).strip() else [])
|
|
|
|
|
|
def main():
|
|
failures = []
|
|
|
|
def bad(path, why):
|
|
failures.append(" %s: %s" % (rel(path), why))
|
|
|
|
# ---- design ------------------------------------------------------------------------
|
|
for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")):
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))):
|
|
if os.path.basename(path) == "README.md":
|
|
continue
|
|
front = frontmatter(path)
|
|
if front is None:
|
|
bad(path, "no frontmatter")
|
|
continue
|
|
if front.get("layer") != name:
|
|
bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name))
|
|
status = front.get("status")
|
|
if status not in DESIGN_STATUSES:
|
|
bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES)))
|
|
if name == "to-be":
|
|
if not listy(front, "decisions"):
|
|
bad(path, "names no decisions -- no design without a decision")
|
|
if status in ("in-progress", "implemented") and not listy(front, "code"):
|
|
bad(path, "status %s but code: names no owner -- no development "
|
|
"without a design that says where" % status)
|
|
|
|
# ---- issues ------------------------------------------------------------------------
|
|
# Two records may not share a number. Numbers are taken as "next free after main", and work
|
|
# sits on unmerged branches for days -- so two people reading the same main allocate the same
|
|
# number, and nothing said so. It happened twice in one evening between two machines, and the
|
|
# second collision landed on main with all three checks passing (issue 155). An issue number is
|
|
# how every other record cites this one; two records answering to it means a pointer that
|
|
# resolves to whichever the reader happened to open.
|
|
seen = {}
|
|
for folder in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", ""))):
|
|
name = os.path.basename(os.path.normpath(folder))
|
|
number = name.split("-", 1)[0]
|
|
if not number.isdigit():
|
|
continue
|
|
if number in seen:
|
|
bad(os.path.join("04-ISSUES", name),
|
|
"is numbered %s, and so is %s -- an issue number is how it is cited, and two "
|
|
"records answering to one means a citation that resolves to whichever the reader "
|
|
"opened. Take the next free number across main AND every open pull request"
|
|
% (number, seen[number]))
|
|
else:
|
|
seen[number] = name
|
|
|
|
# And decision records, which 155's fix left out: on 2026-10-02 two ADRs numbered 0169 landed
|
|
# on main from two sessions within the hour, and every check passed.
|
|
seen_records = {}
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
|
|
name = os.path.basename(path)
|
|
number = name.split("-", 1)[0]
|
|
if not number.isdigit():
|
|
continue
|
|
if number in seen_records:
|
|
bad(os.path.join("02-DECISIONS", name),
|
|
"is numbered %s, and so is %s -- a record's number is how it is cited. Take the next "
|
|
"free number across main AND every open pull request; the branch that lands last "
|
|
"renumbers" % (number, seen_records[number]))
|
|
else:
|
|
seen_records[number] = name
|
|
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
|
front = frontmatter(path)
|
|
if front is None:
|
|
bad(path, "no frontmatter")
|
|
continue
|
|
status = front.get("status")
|
|
if status not in ISSUE_STATUSES:
|
|
bad(path, "status %r is not one of %s" % (status, sorted(ISSUE_STATUSES)))
|
|
if status in ("located", "resolved") and not listy(front, "located-in"):
|
|
bad(path, "status %s but located-in is empty" % status)
|
|
if status == "resolved" and not listy(front, "fixed-by"):
|
|
bad(path, "status %s but fixed-by says nothing" % status)
|
|
# A core issue resolves with its replay, or with why none is possible (ADR 0237).
|
|
replay = " ".join(listy(front, "replay"))
|
|
if replay and not REPLAY_ID.match(replay):
|
|
bad(path, "replay: %r names no replay -- an id from mesh-lab's replays register, R<issue>" % replay)
|
|
core = any(CORE.search(entry) for entry in listy(front, "located-in"))
|
|
opened = str(front.get("opened") or "")
|
|
if (status == "resolved" and core and opened >= REPLAYS_FROM and not replay
|
|
and not " ".join(listy(front, "replay-none")).strip()):
|
|
bad(path, "a core issue resolved with no replay: name it in `replay:` (mesh-lab replays "
|
|
"register.go) or say in `replay-none:` why none is possible (ADR 0237)")
|
|
|
|
# ---- research ----------------------------------------------------------------------
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
|
|
front = frontmatter(path)
|
|
if front is None:
|
|
bad(path, "no frontmatter")
|
|
continue
|
|
status = front.get("status")
|
|
if status not in RESEARCH_STATUSES:
|
|
bad(path, "status %r is not one of %s" % (status, sorted(RESEARCH_STATUSES)))
|
|
if status == "graduated":
|
|
became = listy(front, "became")
|
|
if not became:
|
|
bad(path, "graduated but became: names nothing")
|
|
for target in became:
|
|
if not os.path.exists(os.path.join(ROOT, target)):
|
|
bad(path, "became names %s, which does not exist" % target)
|
|
|
|
# ---- decisions -------------------------------------------------------------------
|
|
records = {}
|
|
for path in sorted(glob.glob(os.path.join(ROOT, "02-DECISIONS", "[0-9]*.md"))):
|
|
front = frontmatter(path)
|
|
records[os.path.basename(path)] = (path, (front or {}).get("status"))
|
|
|
|
cited = set()
|
|
sources = (glob.glob(os.path.join(ROOT, "03-DESIGN", "*", "*.md"))
|
|
+ glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))
|
|
+ glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))
|
|
+ glob.glob(os.path.join(ROOT, "00-META", "**", "*.md"), recursive=True))
|
|
for path in sources:
|
|
text = open(path, encoding="utf-8").read()
|
|
if os.sep + "03-DESIGN" + os.sep in path:
|
|
# A design doc's governing citations live in frontmatter; a prose mention is
|
|
# commentary, not a home.
|
|
m = re.match(r"^---\n(.*?)\n---", text, re.S)
|
|
text = m.group(1) if m else ""
|
|
for m in re.finditer(r"([0-9]{4}-[^\s\)\],#]+\.md)", text):
|
|
cited.add(os.path.basename(m.group(1)))
|
|
for name in records:
|
|
front = frontmatter(records[name][0]) or {}
|
|
for key in ("extends", "supersedes", "superseded-by"):
|
|
v = front.get(key)
|
|
if isinstance(v, str) and v:
|
|
cited.add(os.path.basename(v))
|
|
for name, (path, status) in records.items():
|
|
if status == "accepted" and name not in cited:
|
|
bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
|
|
"design doc's decisions:, a research became:, an issue, or 00-META")
|
|
|
|
checked = (
|
|
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
|
|
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
|
|
+ len(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md")))
|
|
+ len(records)
|
|
)
|
|
if failures:
|
|
print("cycle: %d document(s) break the development cycle:" % len(failures))
|
|
print("\n".join(failures))
|
|
return 1
|
|
print("cycle: %d documents checked, the chain holds" % checked)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|