155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
|
|
#!/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()
|