Something went wrong. Try again.
bayes for days
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183#!/usr/bin/env python3"""Generate the tables in plan/README.md from the epics' own frontmatter.
The same shape as headquarters' register, written fresh rather than copied: thevalue is the structure - one file per epic, the id is the commit scope, thetables are output - and a forked copy of another repo's tooling would driftwithout anyone noticing.
Every table sits between an HTML comment fence and is rewritten from the files.Everything outside a fence is hand-written and passed through untouched. Whichtable an epic lands in follows from where it is and what its status says:plan/complete/ is Complete, and outside it the status picks between the rest.
Row order comes from plan/order.txt, which is advisory - an epic it does notname still appears, alphabetically, at the end of its table. So adding an epicis one file and one command, and two branches adding one do not conflict overrows neither of them cared about.
No third-party modules: this runs on every commit that touches plan/.
scripts/gen-plan-readme.py rewrite the register scripts/gen-plan-readme.py --check ask whether it is current"""
import argparseimport difflibimport reimport sysfrom pathlib import Path
REPO = Path(__file__).resolve().parent.parentPLAN = REPO / "plan"COMPLETE = PLAN / "complete"REGISTER = PLAN / "README.md"ORDER = PLAN / "order.txt"
# Fence name -> (heading it lives under, which epics belong in it).TABLES = { "complete": lambda epic: epic["_complete"], "open": lambda epic: not epic["_complete"] and epic["status"] in ("open", "blocked"), "continuous": lambda epic: not epic["_complete"] and epic["status"] == "continuous", "declined": lambda epic: not epic["_complete"] and epic["status"] == "declined",}
SCALARS = ("id", "title", "status")LISTS = ("dependsOn",)
def parse_frontmatter(text: str, path: Path) -> dict: """Enough YAML for the keys an epic carries, and nothing else.
A real parser would be a dependency on a hook that runs constantly. The subset is: `key: value`, `key: [a, b]`, and `key: >` folded blocks. """ if not text.startswith("---\n"): raise SystemExit(f"{path}: no frontmatter") end = text.index("\n---\n", 3) body = text[4:end] epic: dict = {"dependsOn": []} key = None folded: list[str] = [] for line in body.splitlines(): if key and (line.startswith(" ") or not line.strip()): folded.append(line.strip()) continue if key: epic[key] = " ".join(part for part in folded if part) key, folded = None, [] if not line.strip() or line.lstrip().startswith("#"): continue name, _, value = line.partition(":") name, value = name.strip(), value.strip() if value == ">": key = name continue if value.startswith("[") and value.endswith("]"): inner = value[1:-1].strip() epic[name] = [v.strip() for v in inner.split(",") if v.strip()] else: epic[name] = value if key: epic[key] = " ".join(part for part in folded if part) for required in SCALARS: if required not in epic: raise SystemExit(f"{path}: frontmatter is missing '{required}'") return epic
def load() -> list[dict]: epics = [] for directory, complete in ((PLAN, False), (COMPLETE, True)): if not directory.is_dir(): continue for path in sorted(directory.glob("*.md")): if path.name == "README.md": continue epic = parse_frontmatter(path.read_text(), path) if epic["id"] != path.stem: raise SystemExit(f"{path}: id '{epic['id']}' does not match the filename") epic["_complete"] = complete epic["_path"] = f"complete/{path.name}" if complete else path.name epics.append(epic) return epics
def ranking() -> list[str]: if not ORDER.is_file(): return [] return [ line.strip() for line in ORDER.read_text().splitlines() if line.strip() and not line.startswith("#") ]
def rows(epics: list[dict], order: list[str], complete: bool) -> str: def sort_key(epic: dict) -> tuple: try: return (0, order.index(epic["id"]), "") except ValueError: return (1, 0, epic["id"])
epics = sorted(epics, key=sort_key) if complete: lines = ["| id | title |", "|---|---|"] for epic in epics: lines.append(f"| [{epic['id']}]({epic['_path']}) | {epic['title']} |") return "\n".join(lines) lines = ["| id | title | status | depends on |", "|---|---|---|---|"] for epic in epics: depends = ", ".join(epic["dependsOn"]) or "-" lines.append( f"| [{epic['id']}]({epic['_path']}) | {epic['title']} | {epic['status']} | {depends} |" ) return "\n".join(lines)
def render(current: str, epics: list[dict], order: list[str]) -> str: out = current for name, belongs in TABLES.items(): fence = re.compile(rf"(<!-- generated: {name} -->\n).*?(<!-- /generated -->)", re.DOTALL) if not fence.search(out): raise SystemExit(f"plan/README.md has no '{name}' fence") table = rows([e for e in epics if belongs(e)], order, name == "complete") replacement = table + "\n" out = fence.sub(lambda m, r=replacement: m.group(1) + r + m.group(2), out, count=1) return out
def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="fail if stale") args = parser.parse_args()
epics = load() current = REGISTER.read_text() updated = render(current, epics, ranking())
if args.check: if current != updated: print("plan/README.md is stale. Run scripts/gen-plan-readme.py.", file=sys.stderr) sys.stderr.writelines( difflib.unified_diff( current.splitlines(keepends=True), updated.splitlines(keepends=True), "plan/README.md", "generated", ) ) return 1 return 0
if current != updated: REGISTER.write_text(updated) print(f"rewrote plan/README.md from {len(epics)} epics") else: print("plan/README.md is current") return 0
if __name__ == "__main__": sys.exit(main())