Files
management/scripts/gen_status.py
T
Thore Cimbal ed87ffe236 STATUS nach Meilenstein gruppieren, #0032 abgelehnt
#0032 (gameserver ohne Push-Mirror) auf rejected: Der Game-Server ist nicht Teil
des Arbeitsumfangs. Der Befund bleibt sachlich richtig, er ist nur nicht unserer.

STATUS.md listete alle offenen Issues flach; die Frage "was gehoert zu M1" war
darin nur durch Suchen zu beantworten. Jetzt nach Meilenstein gruppiert und
innerhalb dessen nach Prioritaet - ersetzend, nicht ergaenzend: derselbe Bestand
zweimal untereinander waere genau die Doppelpflege, die diese Datei vermeidet.

roadmap.md bekommt die Bedeutung der fuenf Meilensteine in einem Satz je Zeile.
Die stand bisher nur in den GitLab-Milestone-Beschreibungen und war im Repo
nirgends nachlesbar. Zahlen bleiben draussen - die Datei sagt in ihrem eigenen
Kopf, warum (F-001/F-010).
2026-08-20 12:00:00 +00:00

174 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
PROJEKTERWEITERUNG gegenüber neckbeard v0.1.1 (Original unter
docs/sources/upstream/neckbeard-v0.1.1/scripts/): offene Issues sind
alles außer done/rejected (Status-Enum ist projektweit erweitert);
Tabelle zeigt Meilenstein/Priorität; Verteilungszeile je Meilenstein
ersetzt die früheren Hand-Zählungen der roadmap.md (F-001/F-010).
Writes STATUS.md (no timestamps — output depends only on repo content, so
reruns are diff-clean). With --check, regenerates in memory and fails if
the committed STATUS.md is stale; CI uses this mode.
Usage:
python scripts/gen_status.py [repo-root] # write STATUS.md
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
def parse(path: Path):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "---":
return None, ""
for j in range(1, len(lines)):
if lines[j].strip() == "---":
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
body = "\n".join(lines[j + 1:])
return meta, body
return None, ""
def title(body: str, fallback: str) -> str:
match = H1_RE.search(body)
return match.group(1).strip() if match else fallback
def collect(root: Path, subdir: str, wanted_type: str):
items = []
base = root / subdir
if not base.is_dir():
return items
for path in sorted(base.rglob("*.md")):
if path.name == "template.md":
continue
meta, body = parse(path)
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
continue
rel = path.relative_to(root).as_posix()
items.append((rel, meta, title(body, path.stem)))
return items
def render(root: Path) -> str:
issues = collect(root, "docs/issues", "issue")
designs = collect(root, "docs/design", "design")
adrs = collect(root, "docs/adr", "adr")
aars = collect(root, "docs/aar", "aar")
out: list[str] = []
out.append("# STATUS")
out.append("")
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
out.append("")
open_issues = [i for i in issues
if i[1].get("status") not in ("done", "rejected")]
closed = len(issues) - len(open_issues)
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
out.append("")
if open_issues:
dist: dict[str, int] = {}
for _rel, meta, _name in open_issues:
m = str(meta.get("milestone", "?"))
dist[m] = dist.get(m, 0) + 1
out.append("Verteilung: " + " · ".join(
f"{m} {n}" for m, n in sorted(dist.items())))
out.append("")
out.append("Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md).")
out.append("")
# Nach Meilenstein gruppiert, innerhalb dessen nach Priorität: die Sicht, in
# der man einen Meilenstein abarbeitet. Eine flache Liste ueber alle 57 offenen
# Issues beantwortet die Frage "was gehoert zu M1" nur durch Suchen.
# Bewusst ersetzend statt ergaenzend - derselbe Bestand zweimal untereinander
# waere genau die Doppelpflege, die diese Datei vermeiden soll.
rang = {"high": 0, "medium": 1, "low": 2}
for m in sorted({str(i[1].get("milestone", "?")) for i in open_issues}):
gruppe = [i for i in open_issues
if str(i[1].get("milestone", "?")) == m]
gruppe.sort(key=lambda i: (rang.get(str(i[1].get("priority")), 9),
str(i[1].get("id", ""))))
out.append(f"### {m} ({len(gruppe)})")
out.append("")
out.append("| Issue | Priorität | Status | Title |")
out.append("|---|---|---|---|")
for rel, meta, name in gruppe:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('priority')} | {meta.get('status')} "
f"| {name} |")
out.append("")
else:
out.append("_none open_")
out.append("")
active = [d for d in designs if d[1].get("status") != "done"]
out.append(f"## Active design docs ({len(active)})")
out.append("")
if active:
out.append("| Design | Gate | Title |")
out.append("|---|---|---|")
for rel, meta, name in active:
out.append(f"| [{Path(rel).stem}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none active_")
out.append("")
out.append(f"## ADRs ({len(adrs)})")
out.append("")
if adrs:
out.append("| ADR | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in adrs:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none_")
out.append("")
open_aars = [a for a in aars if a[1].get("status") == "open"]
out.append(f"## Open AARs ({len(open_aars)})")
out.append("")
if open_aars:
for rel, _meta, name in open_aars:
out.append(f"- [{name}]({rel})")
else:
out.append("_none — nothing awaiting harvest_")
out.append("")
return "\n".join(out)
def main() -> int:
args = [a for a in sys.argv[1:] if a != "--check"]
check = "--check" in sys.argv[1:]
root = Path(args[0]) if args else Path.cwd()
content = render(root)
status = root / "STATUS.md"
if check:
current = status.read_text(encoding="utf-8") if status.is_file() else ""
if current != content:
print("gen_status --check: STATUS.md is stale — "
"run scripts/gen_status.py and commit the result")
return 1
print("gen_status --check: STATUS.md is current")
return 0
status.write_text(content, encoding="utf-8", newline="\n")
print(f"wrote {status}")
return 0
if __name__ == "__main__":
sys.exit(main())