Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378#!/usr/bin/env python3"""Check the shape of plan/ — the epic files and the register that lists them.
An epic is a markdown file with YAML frontmatter, and that frontmatter is readby more than a person: scripts/gen-plan-readme.py builds the register's tablesout of it, and the id is the Conventional Commits scope a change to that epicis committed under. A malformed file therefore breaks the register rather thanthe file it is in, which is the wrong place to find out.
The structure — a directory of epics, generated tables, order.txt, a Donesection that is the exit test — is lifted from lance.blue's headquarters repo,along with these two scripts. The one difference is the frontmatter: `crates`rather than `repos`, because everything here is in one repository and what isworth naming is which parts of the workspace an epic touches.
This checks shape only — the keys that must be there, the ids that must match,the links that must resolve, and the `## Done` invariant the register describes.Whether an epic is well scoped or its prose is true is a human's call and isdeliberately not checked here.
No third-party modules on purpose: this runs on every commit that touchesplan/, so it has to start fast and work in a fresh clone. The frontmatterparser below is a subset of YAML, not YAML.
Run by prek over plan/, or by hand: scripts/check-plan.py"""
import osimport reimport sys
# Every epic carries exactly these, no more and no less. An epic missing one# cannot be put in the register, and an epic with a seventh is writing down# something nothing reads.REQUIRED_KEYS = ["id", "title", "status", "crates", "dependsOn", "exitCriterion"]
# Documented in plan/README.md, under "Status".VALID_STATUS = ["shipped", "open", "blocked", "continuous", "declined"]
# Files in plan/ that are not epics. Everything else there is one, so a new# hand-written page needs a line here or it fails as a malformed epic.NOT_EPICS = {"README.md", "milestones.md"}
LINK_RE = re.compile(r"(?<!\\)\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")CHECKBOX_RE = re.compile(r"^\s*[-*] \[([ xX])\]")HEADING_RE = re.compile(r"^(#{1,6}) +(.*?)\s*$")
def strip_fences(lines): """Return lines with fenced code blocks blanked out.
A heading or a checkbox inside a fence is an example, not structure. """ out = [] in_fence = False for line in lines: if line.lstrip().startswith("```"): in_fence = not in_fence out.append("") continue out.append("" if in_fence else line) return out
def parse_frontmatter(path, lines, errors): """Parse the leading --- block. Returns a dict, or None if unparseable.
Understands only what plan/ uses: `key: scalar`, `key: [a, b]` and a `>` folded block. Anything else is reported rather than guessed at. """ if not lines or lines[0].rstrip() != "---": errors.append( f"{path}: no frontmatter.\n" f" An epic starts with a --- block holding: " f"{', '.join(REQUIRED_KEYS)}.\n" f" Copy the block from any other file in plan/." ) return None
end = None for i in range(1, len(lines)): if lines[i].rstrip() == "---": end = i break if end is None: errors.append( f"{path}: frontmatter is never closed.\n" f" Add a --- line after the last key." ) return None
data = {} i = 1 while i < end: raw = lines[i] i += 1 if not raw.strip(): continue m = re.match(r"^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$", raw) if not m: errors.append( f"{path}: frontmatter line {i} is not `key: value`:\n" f" {raw.strip()}\n" f" This file's frontmatter is a small subset of YAML. Keep it " f"to the six keys other epics use." ) return None key, value = m.group(1), m.group(2).strip() if key in data: errors.append( f"{path}: frontmatter sets `{key}` twice. Delete one of them." ) return None if value == ">" or value == "|": # Folded or literal block: the indented lines that follow. block = [] while i < end and (not lines[i].strip() or lines[i][:1] in " \t"): block.append(lines[i].strip()) i += 1 data[key] = " ".join(p for p in block if p) elif value.startswith("[") and value.endswith("]"): inner = value[1:-1].strip() data[key] = [p.strip() for p in inner.split(",") if p.strip()] else: data[key] = value.strip("'\"") return data
def check_frontmatter(path, stem, data, known_ids, errors): if "order" in data: errors.append( f"{path}: frontmatter has an `order` key.\n" f" There is deliberately no `order` field: a total order over " f"twenty epics has to\n" f" be rewritten whenever one moves, so two branches adding an " f"epic conflict over a\n" f" number neither cares about. Delete the key. What an epic " f"waits on goes in\n" f" `dependsOn`; the reading order is plan/order.txt, which is " f"one shared list\n" f" rather than a rank in every file, and which an epic does not " f"have to appear in." )
missing = [k for k in REQUIRED_KEYS if k not in data] extra = [k for k in data if k not in REQUIRED_KEYS and k != "order"] if missing: errors.append( f"{path}: frontmatter is missing {', '.join(missing)}.\n" f" Every epic carries exactly: {', '.join(REQUIRED_KEYS)}.\n" f" Copy the block from any other file in plan/ and fill it in." ) if extra: errors.append( f"{path}: frontmatter has keys nothing reads: " f"{', '.join(sorted(extra))}.\n" f" Every epic carries exactly: {', '.join(REQUIRED_KEYS)}.\n" f" Put it in the prose instead, or delete it." )
if "id" in data and data["id"] != stem: errors.append( f"{path}: id is `{data['id']}` but the filename says `{stem}`.\n" f" The id is the file's name and the commit scope both, so a " f"change to this epic\n" f" is committed as `feat({stem}): …`. Either set `id: {stem}`, or " f"rename the file\n" f" to {data['id']}.md and update the link in plan/README.md." )
if "status" in data and data["status"] not in VALID_STATUS: errors.append( f"{path}: status is `{data['status']}`. Use one of:\n" f" {', '.join(VALID_STATUS)}\n" f" plan/README.md, under \"Status\", says what each one means." )
depends = data.get("dependsOn", []) if not isinstance(depends, list): errors.append( f"{path}: dependsOn is not a list. Write it as `dependsOn: []` or " f"`dependsOn: [some-epic]`." ) else: for dep in depends: if dep not in known_ids: errors.append( f"{path}: dependsOn names `{dep}`, which is not an epic.\n" f" It has to be the id of a file in plan/ or " f"plan/complete/. Fix the spelling,\n" f" write plan/{dep}.md, or drop it from the list." )
def check_done_section(path, lines, archived, errors): """Exactly one `## Done`, last, with the checkboxes on the right side.""" done_at = None trailing = [] for n, line in enumerate(lines): m = HEADING_RE.match(line) if not m: continue level, text = len(m.group(1)), m.group(2) if level == 2 and text == "Done": if done_at is not None: errors.append( f"{path}: two `## Done` headings (lines {done_at + 1} and " f"{n + 1}).\n" f" An epic has one, at the end. Merge them." ) return done_at = n elif done_at is not None and level <= 2: trailing.append((n + 1, line.strip()))
if done_at is None: errors.append( f"{path}: no `## Done` section.\n" f" Every epic ends in one, holding the finished work as `- [x]` " f"items — that is\n" f" what makes \"is this epic finished\" answerable by looking. " f"Add it at the end of\n" f" the file, empty if nothing is done yet." ) return
if trailing: line_no, text = trailing[0] errors.append( f"{path}: `## Done` is not the last section — `{text}` follows it " f"on line {line_no}.\n" f" Move that section above `## Done`, so the finished work stays " f"at the bottom." )
for n, line in enumerate(lines): m = CHECKBOX_RE.match(line) if not m: continue checked = m.group(1).lower() == "x" if checked and n < done_at: errors.append( f"{path}:{n + 1}: a `- [x]` item above `## Done`:\n" f" {line.strip()}\n" f" Finished work moves down under `## Done`. Open work stays " f"above it as `- [ ]`." ) elif not checked and n > done_at and not archived: # An archived file gets the sharper message from check_archived # instead: nothing is open in there, wherever it sits. errors.append( f"{path}:{n + 1}: a `- [ ]` item under `## Done`:\n" f" {line.strip()}\n" f" Done holds finished work only. Move it above the heading, " f"or tick it." )
def check_archived(path, lines, errors): """An epic in plan/complete/ has nothing left open.""" for n, line in enumerate(lines): m = CHECKBOX_RE.match(line) if m and m.group(1) == " ": errors.append( f"{path}:{n + 1}: an open `- [ ]` item in an archived epic:\n" f" {line.strip()}\n" f" plan/complete/ is for epics with nothing open. Either " f"finish it and tick it,\n" f" or move the file back up to plan/ until it closes." )
def check_links(path, abs_path, lines, repo_root, errors): """Every relative link resolves, from the linking file's own directory.""" base = os.path.dirname(abs_path) for n, line in enumerate(lines): for target in LINK_RE.findall(line): if re.match(r"^[a-z][a-z0-9+.-]*:", target) or target.startswith("//"): continue target = target.split("#", 1)[0] if not target: continue resolved = os.path.normpath(os.path.join(base, target)) if not os.path.exists(resolved): rel = os.path.relpath(resolved, repo_root) errors.append( f"{path}:{n + 1}: link to `{target}` does not resolve " f"(looked for {rel}).\n" f" Links are relative to the file they are in, so a file " f"in plan/complete/\n" f" reaches the rest of plan/ with `../`. Fix the path, or " f"drop the link." )
def main(): repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) plan_dir = os.path.join(repo_root, "plan") register = os.path.join(plan_dir, "README.md")
if not os.path.isdir(plan_dir): print(f"no plan/ directory at {plan_dir}", file=sys.stderr) return 1
epics = [] for directory in (plan_dir, os.path.join(plan_dir, "complete")): if not os.path.isdir(directory): continue for name in sorted(os.listdir(directory)): # README.md is the register and milestones.md is the narrative, # not epics: they carry no frontmatter and no Done section. The # register is checked for its links and for listing everything # else; milestones.md is prose nothing reads. if not name.endswith(".md") or name in NOT_EPICS: continue epics.append(os.path.join(directory, name))
known_ids = {os.path.basename(p)[:-3] for p in epics} errors = []
if not os.path.isfile(register): errors.append( "plan/README.md is missing. It is the register: every epic is " "linked from it." ) register_links = set() else: with open(register, encoding="utf-8") as fh: lines = strip_fences(fh.read().splitlines()) check_links("plan/README.md", register, lines, repo_root, errors) register_links = set() for line in lines: for target in LINK_RE.findall(line): if re.match(r"^[a-z][a-z0-9+.-]*:", target): continue target = target.split("#", 1)[0] if target: register_links.add( os.path.normpath(os.path.join(plan_dir, target)) )
for path in epics: with open(path, encoding="utf-8") as fh: lines = strip_fences(fh.read().splitlines()) stem = os.path.basename(path)[:-3] rel = os.path.relpath(path, repo_root)
archived = os.path.dirname(path).endswith(os.sep + "complete")
data = parse_frontmatter(rel, lines, errors) if data is not None: check_frontmatter(rel, stem, data, known_ids, errors) check_done_section(rel, lines, archived, errors) check_links(rel, path, lines, repo_root, errors) if archived: check_archived(rel, lines, errors)
if os.path.normpath(path) not in register_links: where = os.path.relpath(path, plan_dir) errors.append( f"{rel}: not linked from plan/README.md.\n" f" The register is how an epic is found, and how " f"check-commit-scope.sh knows the\n" f" scope exists. Add a row to the table for its status: " f"`[{stem}]({where})`." )
if errors: print(f"plan/ check failed ({len(errors)} problem(s)):\n", file=sys.stderr) for err in errors: print(err + "\n", file=sys.stderr) return 1 return 0
if __name__ == "__main__": sys.exit(main())