Evaluated the Open Knowledge Format (GoogleCloudPlatform/knowledge-catalog okf v0.2, scaccogatto/okf-skills) as a replacement for our decision-record system and rejected it: its conformance rules are deliberately permissive exactly where ours are strict (broken links, unknown types and missing fields must all be tolerated; `deprecated` points at no successor), and its stable identity is the file path, which the breadcrumb rule tells agents not to trust. Adopted two of its optional families instead, additively: - `stale-after: YYYY-MM-DD` on the metadata line — marks a record asserting an outside-world fact as due for re-confirmation. Absolute date, no TTL. - `**Sources:**` in the metadata block — the evidence a record rests on, as distinct from `Signals:` (recall keywords). Neither is required; absence is never an error. A malformed `stale-after` is blocking (it would silently never fire), but a past-due record is only a non-blocking `::notice::` — going stale is the passage of time, not a defect in whatever commit is under test. The catalog's new "Review due" section renders the date only and never a clock-derived verdict, so it cannot drift `--check` red on a calendar boundary with no commit touching the corpus. No backfill: no existing record adopts either field here. fixes #603
108 lines
4.0 KiB
Python
108 lines
4.0 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 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 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())
|