#!/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()