#!/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` or `fixed`, `located-in:` names the owner; once `fixed` or `resolved`, `fixed-by:` says what fixed it (prose counts -- "nothing, the capability existed" is an answer). 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", "fixed", "resolved"} RESEARCH_STATUSES = {"active", "graduated", "abandoned"} 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 ------------------------------------------------------------------------ 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", "fixed") and not listy(front, "located-in"): bad(path, "status %s but located-in is empty" % status) if status in ("fixed", "resolved") and not listy(front, "fixed-by"): bad(path, "status %s but fixed-by says nothing" % status) # ---- 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())