feat: slice 1 - the neckbeard framework chain runs end to end
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5e46372ea8
commit
e36ed337a7
@@ -0,0 +1,157 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user