158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate the Markdown processbook, with Mermaid views, from processes.yaml
|
|
(EV-008). The BPMN files remain the authoritative form; the views here are
|
|
derived from the same source and cannot drift from it.
|
|
|
|
USAGE
|
|
python3 generate_processbook_md.py [output.md]
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SRC = os.path.join(HERE, "processes.yaml")
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "PR_TBox_Processbook.md")
|
|
|
|
SHAPE = {
|
|
"start": ("([", "])"),
|
|
"end": ("([", "])"),
|
|
"userTask": ("[", "]"),
|
|
"scriptTask": ("[", "]"),
|
|
"callActivity": ("[[", "]]"),
|
|
"gateway": ("{", "}"),
|
|
}
|
|
KIND = {
|
|
"userTask": "human",
|
|
"scriptTask": "script",
|
|
"callActivity": "calls",
|
|
"gateway": "decision",
|
|
"start": "start",
|
|
"end": "end",
|
|
}
|
|
|
|
|
|
def flow(t):
|
|
return " ".join((t or "").split())
|
|
|
|
|
|
def mermaid(proc):
|
|
out = ["```mermaid", "flowchart TD"]
|
|
for n in proc["flow"]:
|
|
o, c = SHAPE[n["type"]]
|
|
label = n["name"].replace('"', "'")
|
|
if n["type"] == "callActivity":
|
|
label = "%s: %s" % (n["calls"], label)
|
|
out.append(' %s%s"%s"%s' % (n["id"], o, label, c))
|
|
for f in proc["flows"]:
|
|
if f.get("condition"):
|
|
out.append(' %s -->|%s| %s' % (f["from"], f["condition"], f["to"]))
|
|
else:
|
|
out.append(" %s --> %s" % (f["from"], f["to"]))
|
|
for n in proc["flow"]:
|
|
if n["type"] == "userTask":
|
|
out.append(" class %s human;" % n["id"])
|
|
elif n["type"] == "gateway":
|
|
out.append(" class %s decision;" % n["id"])
|
|
out.append(" classDef human fill:#FFF4CE,stroke:#B08900;")
|
|
out.append(" classDef decision fill:#E8F0F7,stroke:#3E6E96;")
|
|
out.append("```")
|
|
return out
|
|
|
|
|
|
def main():
|
|
doc = yaml.safe_load(open(SRC, encoding="utf-8"))
|
|
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 `processes.yaml`. The BPMN files in `bpmn/` are the "
|
|
"authoritative form; the views below are derived from the same source. "
|
|
"Do not edit this document (EV-007, EV-008).")
|
|
w("")
|
|
w(flow(m["scope"]))
|
|
w("")
|
|
w("## Procedures")
|
|
w("")
|
|
w("| Id | Family | Procedure | Scope |")
|
|
w("|---|---|---|---|")
|
|
for p in doc["processes"]:
|
|
w("| **%s** | %s | %s | %s |" % (p["id"], p["family"], p["name"], p.get("scope", "")))
|
|
w("")
|
|
w("## Reading a diagram")
|
|
w("")
|
|
w("- A **rounded** node is a start or end state.")
|
|
w("- A **diamond** is a decision; every outgoing path is labelled.")
|
|
w("- A **shaded** box is a human task: it cannot be automated, and the work "
|
|
"stops there until someone acts.")
|
|
w("- A **double-bordered** box calls another procedure by its identifier.")
|
|
w("")
|
|
|
|
for p in doc["processes"]:
|
|
w("---")
|
|
w("")
|
|
w("# %s — %s" % (p["id"], p["name"]))
|
|
w("")
|
|
w("| | |")
|
|
w("|---|---|")
|
|
w("| Family | %s |" % p["family"])
|
|
w("| Scope | %s |" % p.get("scope", ""))
|
|
w("| Trigger | %s |" % p.get("trigger", ""))
|
|
w("| Inputs | %s |" % ", ".join(p.get("inputs") or []))
|
|
w("| Outputs | %s |" % ", ".join(p.get("outputs") or []))
|
|
rules = sorted({r for n in p["flow"] for r in (n.get("rules") or [])})
|
|
w("| Rules enforced | %s |" % (", ".join(rules) or "-"))
|
|
calls = sorted({n["calls"] for n in p["flow"] if n["type"] == "callActivity"})
|
|
w("| Calls | %s |" % (", ".join(calls) or "-"))
|
|
w("")
|
|
if p.get("parameter"):
|
|
w("**Parameter.** %s" % flow(p["parameter"]))
|
|
w("")
|
|
if p.get("note"):
|
|
w("**Note.** %s" % flow(p["note"]))
|
|
w("")
|
|
out.extend(mermaid(p))
|
|
w("")
|
|
w("| Step | Type | Rules |")
|
|
w("|---|---|---|")
|
|
for n in p["flow"]:
|
|
if n["type"] in ("start", "end"):
|
|
continue
|
|
name = n["name"]
|
|
if n["type"] == "callActivity":
|
|
name = "%s (%s)" % (name, n["calls"])
|
|
w("| %s | %s | %s |"
|
|
% (name, KIND[n["type"]], ", ".join(n.get("rules") or []) or "-"))
|
|
w("")
|
|
|
|
w("---")
|
|
w("")
|
|
w("# Rule coverage")
|
|
w("")
|
|
w("Which procedures enforce each rule. Derived, not maintained: a rule cited "
|
|
"in a diagram appears here automatically.")
|
|
w("")
|
|
cover = {}
|
|
for p in doc["processes"]:
|
|
for n in p["flow"]:
|
|
for r in n.get("rules") or []:
|
|
cover.setdefault(r, set()).add(p["id"])
|
|
w("| Rule | Procedures |")
|
|
w("|---|---|")
|
|
for r in sorted(cover):
|
|
w("| %s | %s |" % (r, ", ".join(sorted(cover[r]))))
|
|
w("")
|
|
|
|
open(OUT, "w", encoding="utf-8").write("\n".join(out))
|
|
print("written: %s (%d lines, %d procedures, %d rules covered)"
|
|
% (OUT, len(out), len(doc["processes"]), len(cover)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|