Files
ersatztv/scripts/build_decisions_catalog.py
T
timothyandClaude Opus 4.8 458ab2111f fix(521): catalog _anchor collapses punctuation runs; single trailing newline
_anchor() mapped each space/hyphen to its own '-' without collapsing runs,
so the standard heading separator " — " (space, em-dash, space) produced a
double hyphen in every generated anchor. Since nearly every real decision
record heading uses that separator, this made the catalog emit a dead link
for essentially every row. Fix: after building the char list, collapse
consecutive '-' into one and strip leading/trailing '-' via re.sub, matching
how Goldmark/GitHub/Gitea sluggers behave.

Also fixed main() writing an extra trailing newline (want already ends in
"\n", then "+ \n" appended a second one) so docs/decisions/README.md now
ends with exactly one trailing newline; --check still compares via .strip().

Added test_anchor_collapses_em_dash_and_keeps_underscore to pin the anchor
behavior against the reported iptv.base_url case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 02:58:58 +02:00

88 lines
2.9 KiB
Python

#!/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 re
import sys
from pathlib import Path
import scripts.decisions_lib as dl
OUTPUT = dl.TOPIC_DIR / "README.md"
BANNER = "<!-- GENERATED by scripts/build_decisions_catalog.py — do not edit by hand. -->"
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/GitHub slugger: lowercase, keep alphanumerics + underscore, spaces/hyphens → '-',
# drop other punctuation, then collapse runs of '-' into one and trim leading/trailing '-'.
# Keeping '_' matters — headings like "…(`iptv.base_url`)…" anchor to …iptvbase_url….
# VERIFY against Gitea's actual output on 2-3 real headings before trusting links (the catalog
# is the startup surface; a dead link there is expensive).
out = []
for ch in heading.lower():
if ch.isalnum() or ch == "_":
out.append(ch)
elif ch in " -":
out.append("-")
return re.sub(r"-+", "-", "".join(out)).strip("-")
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("")
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())