#!/usr/bin/env python3 """Generate docs/decisions/README.md — the compact ACTIVE decision catalog / task router. Do not edit docs/decisions/README.md by hand; regenerate with this script. The decisions-validate CI job checks it is in sync. """ from __future__ import annotations import sys from pathlib import Path import scripts.decisions_lib as dl OUTPUT = dl.TOPIC_DIR / "README.md" BANNER = "" def _rel(rec) -> str: """Link from docs/decisions/README.md to the record's heading.""" src = Path(rec.source).name anchor = _anchor(rec.heading) if src == "decisions.md": return f"../decisions.md#{anchor}" return f"{src}#{anchor}" def _anchor(heading: str) -> str: # Gitea slugger: lowercase, keep alphanumerics + underscore, spaces/hyphens → '-' each (one # hyphen PER such char — Gitea does NOT collapse runs of '-'), drop other punctuation. No # collapsing, no trimming. Keeping '_' matters — headings like "…(`iptv.base_url`)…" anchor to # …iptvbase_url…. A " — " (space em-dash space) heading therefore anchors with "--", e.g. # "2026-07-19 — Foo" → "2026-07-19--foo". VERIFIED against this repo's own working Index # anchors — do not "fix" this back to a collapsed form without re-checking against Gitea. out = [] for ch in heading.lower(): if ch.isalnum() or ch == "_": out.append(ch) elif ch in " -": out.append("-") return "".join(out) def render_catalog(records) -> str: active = sorted( [r for r in records if r.status == "active" and r.key], key=lambda r: r.key, ) lines = [ BANNER, "", "# Active decisions — catalog / task router", "", "The compact current view of settled decisions. Each row is an **active** record; follow", "the link for rationale. Superseded/retired history lives in `archive/`. Regenerated by", "`scripts/build_decisions_catalog.py`.", "", "| Key | Current rule | Since | Record |", "| --- | --- | --- | --- |", ] for r in active: rule = (r.rule or "").replace("|", "\\|") lines.append(f"| `{r.key}` | {rule} | {r.since or ''} | [link]({_rel(r)}) |") lines.append("") # Review-due section (ersatztv#603). Renders the DATE only — never "today", never a computed # stale/fresh verdict. This file is checked with --check in CI, so anything time-dependent here # would drift it red on a calendar boundary with no commit touching the corpus. Evaluating # today >= stale-after is the validator's job, on its non-blocking notice path. due = sorted([r for r in active if r.stale_after], key=lambda r: (r.stale_after or "", r.key or "")) if due: lines += [ "## Review due", "", "Active records that assert facts about the outside world and carry a `stale-after` date.", "Once that date passes, re-confirm the fact and either extend the date or supersede the", "record. Sorted soonest-first.", "", "| Stale after | Key | Record |", "| --- | --- | --- |", ] for r in due: lines.append(f"| {r.stale_after} | `{r.key}` | [link]({_rel(r)}) |") lines.append("") return "\n".join(lines) def main(argv=None) -> int: check = "--check" in (argv or sys.argv[1:]) want = render_catalog(dl.all_active_records()) have = OUTPUT.read_text(encoding="utf-8") if OUTPUT.exists() else "" if check: if want.strip() != have.strip(): print( "build_decisions_catalog: docs/decisions/README.md is stale — regenerate.", file=sys.stderr, ) return 1 print("build_decisions_catalog: catalog up to date") return 0 OUTPUT.write_text(want.rstrip("\n") + "\n", encoding="utf-8") print(f"build_decisions_catalog: wrote {OUTPUT}") return 0 if __name__ == "__main__": raise SystemExit(main())