Files
data-meta-model/governance/generate_rulebook_md.py
T

155 lines
4.9 KiB
Python
Raw Normal View History

2026-08-05 09:14:34 +02:00
#!/usr/bin/env python3
"""
Generate the Markdown rulebook from rules.yaml (EV-008).
USAGE
python3 generate_rulebook_md.py [output.md]
rules.yaml is the single source of truth. This script never edits it.
The output must never be edited by hand (EV-007).
"""
import os
import sys
import yaml
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "rules.yaml")
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Rulebook.md")
def selfcheck(doc):
"""EV-011 and EV-012 applied to the rulebook itself."""
problems = []
for r in doc["rules"]:
ctl = r.get("control") or {}
if not ctl.get("tier"):
problems.append("%s: no control tier (EV-012)" % r["id"])
if r.get("severity") == "BLOCKING" and not ctl.get("executor"):
problems.append("%s: BLOCKING without an executor (EV-011)" % r["id"])
if r.get("category") not in doc["categories"]:
problems.append("%s: unknown category" % r["id"])
return problems
def flow(text):
"""Collapse a YAML folded scalar into a single paragraph."""
return " ".join((text or "").split())
def joined(v):
return ", ".join(v) if isinstance(v, list) else (v or "-")
def main():
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
problems = selfcheck(doc)
if problems:
print("SELF-CHECK FAILED")
for p in problems:
print(" " + p)
sys.exit(1)
m, out = doc["meta"], []
w = out.append
w("# %s" % m["title"])
w("")
w("**Version %s** — %s — generated %s" % (m["version"], m["status"], m["date"]))
w("")
w("> Generated from `rules.yaml`. Do not edit this document: edit the source "
"and regenerate (EV-007, EV-008).")
w("")
w("## Scope")
w("")
w(flow(m["scope"]))
w("")
w(flow(m["audience"]))
w("")
w("## Categories")
w("")
w("| Category | Subject | Rules |")
w("|---|---|---|")
for k, v in doc["categories"].items():
n = sum(1 for r in doc["rules"] if r["category"] == k)
w("| **%s** | %s | %d |" % (k, v["title"], n))
w("")
w("## Severity")
w("")
for k, v in doc["severity_model"].items():
w("- **%s** — %s" % (k, flow(v)))
w("")
for cat, info in doc["categories"].items():
w("---")
w("")
w("# %s%s" % (cat, info["title"]))
w("")
w(flow(info["intent"]))
w("")
for r in [x for x in doc["rules"] if x["category"] == cat]:
ctl = r.get("control") or {}
w("## %s%s" % (r["id"], r["title"]))
w("")
w("**Statement.** %s" % flow(r["statement"]))
w("")
w("| | |")
w("|---|---|")
w("| Severity | **%s** |" % r["severity"])
w("| Applies to | %s |" % joined(r.get("scope")))
w("| Enforced at | %s |" % joined(ctl.get("tier")))
w("| Executor | `%s` |" % (ctl.get("executor") or "-"))
w("| Procedure | %s |" % (ctl.get("procedure") or "_pending_"))
if r.get("filiation"):
w("| Related | %s |" % r["filiation"])
w("")
if r.get("rationale"):
w("**Why.** %s" % flow(r["rationale"]))
w("")
if r.get("examples"):
w("| From | To | Note |")
w("|---|---|---|")
for e in r["examples"]:
w("| %s | %s | %s |" % (e["from"], e["to"], e.get("note") or ""))
w("")
ab = doc["abstractness"]
w("---")
w("")
w("# Annex A — Abstractness")
w("")
w("Applies TN-006 and TN-007 to the model. %d classes, of which **%d abstract** "
"and **%d concrete**. Maintained with the model, not after it."
% (len(ab), sum(1 for a in ab if a["is_abstract"]),
sum(1 for a in ab if not a["is_abstract"])))
w("")
w("| Class | isAbstract | Note |")
w("|---|---|---|")
for a in ab:
w("| `%s` | %s | %s |"
% (a["term"], "**true**" if a["is_abstract"] else "false", a.get("note") or ""))
w("")
w("---")
w("")
w("# Annex B — Display")
w("")
w("Applies TN-011 and TN-022. The short label is what screens display and what "
"relation names reuse; the acronym is a search key, never an identity.")
w("")
w("| IRI | rdfs:label | shortLabel | acronym | Note |")
w("|---|---|---|---|---|")
for d in doc["display"]:
w("| `%s` | %s | %s | %s | %s |"
% (d["iri"], d["label"], d.get("short_label") or "-",
d.get("acronym") or "-", d.get("note") or ""))
w("")
open(OUT, "w", encoding="utf-8").write("\n".join(out))
print("self-check passed: %d rules, every tier assigned, no blocking rule "
"without an executor" % len(doc["rules"]))
print("written: %s (%d lines)" % (OUT, len(out)))
if __name__ == "__main__":
main()