#!/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" % 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())