#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).
174 lines
6.2 KiB
Python
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())
|