Tracer bullet of the migration design (Gate 4, slice 1): pinned v0.1.1 baseline under docs/sources/upstream/ with provenance note, the Karpathy block moved verbatim to docs/sources/regelwerk/ (standing rule mapped onto the sources read-only mechanism), AGENTS.md assembled from the byte-true upstream sections plus the project section 6 (group rules condensed from the old CLAUDE.md), CLAUDE.md reduced to the upstream pointer, WORKFLOW.md and all four templates copied, schema.yaml extended (issue milestone/priority/status columns, component type, wiki area vision - all flagged in the header), validate.py and gen_status.py forked with marked extensions, pruefe_upstream_drift.py added, STATUS.md generated, CI gains the offline validate job, README directory link defused. Verified: validate 0 errors 0 warnings (the three pre-existing directory-link errors are gone), gen_status --check current, drift check 0 findings, baseline byte-identical to the reference checkout (10/10 files), four negative tests fire (WIP limit 3x in-progress, waiting without wartegrund, component slug mismatch, single-byte drift in WORKFLOW.md). gen_status needs Python >= 3.10 locally (write_text newline) - noted for the design AAR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
5.2 KiB
Python
158 lines
5.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("| Issue | Status | Meilenstein | Priorität | Title |")
|
|
out.append("|---|---|---|---|---|")
|
|
for rel, meta, name in open_issues:
|
|
out.append(f"| [{meta.get('id', '?')}]({rel}) "
|
|
f"| {meta.get('status')} | {meta.get('milestone')} "
|
|
f"| {meta.get('priority')} | {name} |")
|
|
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())
|