docs/agent/tools/gen_use_cases.py
#!/usr/bin/env python3
"""Generate docs/agent/use-cases/*.md from the team's use-case explorer export.
Source: use_cases/UC1-Explorer.dc.html (a design-tool export). The page embeds
all use-case data as `const D = {...};` inside its <script type="text/x-dc">
block. This script extracts that object and writes one Markdown file per
module plus an index and a role index.
Re-run after the team updates the export:
python3 docs/agent/tools/gen_use_cases.py
Uses the standard library only.
"""
import json
import re
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SRC = ROOT / "use_cases" / "UC1-Explorer.dc.html"
OUT = ROOT / "docs" / "agent" / "use-cases"
SYSTEM_ACTOR = "ระบบ"
def load_data():
html = SRC.read_text(encoding="utf-8")
marker = "const D = "
start = html.index(marker) + len(marker)
data, _ = json.JSONDecoder().raw_decode(html[start:])
return data
def uc_sort_key(uid):
return int(re.sub(r"\D", "", uid))
def md_escape(text):
return text.replace("|", "\\|")
def uc_link(uid, uc_module):
return f"[{uid}]({uc_module[uid]}.md#{uid.lower()})"
def write_module(code, mod, data, uc_module):
uc = data["uc"]
lines = [
f"# {code} — {mod['n']}",
"",
f"Group: **{mod['g']}** · Use cases: **{len(mod['u'])}**",
"",
"> Generated by `docs/agent/tools/gen_use_cases.py` from "
"`use_cases/UC1-Explorer.dc.html`. Do not edit by hand; re-run the script.",
"",
]
if not mod["u"]:
lines += [
"This module has no use cases of its own. It is a background service "
"that other use cases call (for example sending notifications, writing "
"audit records or syncing with the LMS).",
"",
]
by_feature = defaultdict(list)
for uid in mod["u"]:
by_feature[uc[uid]["p"]].append(uid)
if mod["u"]:
lines += ["| ID | Use case | Lifecycle tag | Actors | Steps |", "|---|---|---|---|---|"]
for uid in mod["u"]:
u = uc[uid]
lines.append(
f"| [{uid}](#{uid.lower()}) | {md_escape(u['n'])} | {u['p']} | {len(u['a'])} | {len(u['f'])} |"
)
lines.append("")
for uid in mod["u"]:
u = uc[uid]
sys_steps = sum(1 for a, _ in u["f"] if a == SYSTEM_ACTOR)
lines += [
f'<a id="{uid.lower()}"></a>',
f"## {uid} {u['n']}",
"",
f"- **Lifecycle tag:** {u['p']}",
f"- **Starts when:** {u['s']}",
f"- **Outcome:** {u['o']}",
f"- **Actors:** {', '.join(u['a'])}",
f"- **Screens:** {', '.join(u['sc']) if u['sc'] else '—'}",
"",
f"Steps ({len(u['f'])}, of which {sys_steps} automated by the system):",
"",
]
for i, (actor, text) in enumerate(u["f"], 1):
who = f"_{actor}_" if actor == SYSTEM_ACTOR else f"**{actor}**"
lines.append(f"{i}. {who}: {text}")
lines.append("")
if review := u.get("review"):
lines += [f"> **ข้อสังเกตการทบทวน {review['date']} — รอยืนยัน:** {review['note']}", ""]
if wf := u.get("wf"):
lines += ["### ทางแยก workflow (ข้อเสนอ รอผู้รับผิดชอบยืนยัน)", "",
"| ก่อนขั้น | Decision | เงื่อนไข | ไปขั้น / ผลลัพธ์ |", "|---|---|---|---|"]
for gate in wf.get("decisions", []):
for branch in gate["branches"]:
target = f"ขั้น {branch['target']}" if "target" in branch else branch["outcome"]
lines.append(f"| {gate['before']} | {md_escape(gate['title'])} | {md_escape(branch['label'])} | {md_escape(target)} |")
lines.append("")
for step, target in wf.get("next", {}).items():
lines += [f"- หลังขั้น {step} → ขั้น {target} (แทนลำดับถัดไป)", ""]
for step, outcome in wf.get("stop", {}).items():
lines += [f"- หลังขั้น {step} → สิ้นสุดแยก: {outcome}", ""]
if u["r"]:
lines.append("Relations:")
lines.append("")
for rel, target in u["r"]:
name = uc[target]["n"] if target in uc else "?"
link = uc_link(target, uc_module) if target in uc_module else target
lines.append(f"- {rel} {link} {name}")
lines.append("")
else:
lines += ["Relations: none. The use case completes on its own.", ""]
(OUT / f"{code}.md").write_text("\n".join(lines), encoding="utf-8")
def write_index(data, uc_module):
uc = data["uc"]
mods = data["mods"]
n_steps = sum(len(u["f"]) for u in uc.values())
roles = defaultdict(list)
for uid, u in uc.items():
for actor in u["a"]:
roles[actor].append(uid)
tags = defaultdict(list)
for uid, u in uc.items():
tags[u["p"]].append(uid)
lines = [
"# Use-case catalog (index)",
"",
"> Generated by `docs/agent/tools/gen_use_cases.py` from "
"`use_cases/UC1-Explorer.dc.html`. Do not edit by hand; re-run the script.",
"> For what these use cases mean for the project, read "
"[../12-use-cases.md](../12-use-cases.md) first.",
"",
f"{len(mods)} modules · {len(uc)} use cases · {n_steps} steps · {len(roles)} roles",
"",
"## Modules",
"",
]
for group, codes in data["groups"]:
lines += [f"### {group}", "", "| Code | Module | Use cases |", "|---|---|---|"]
for code in codes:
m = mods[code]
ids = ", ".join(uc_link(u, uc_module) for u in m["u"]) or "— (background service)"
lines.append(f"| [{code}]({code}.md) | {m['n']} | {ids} |")
lines.append("")
lines += ["## All use cases by ID", "", "| ID | Use case | Module | Lifecycle tag |", "|---|---|---|---|"]
for uid in sorted(uc, key=uc_sort_key):
u = uc[uid]
lines.append(f"| {uc_link(uid, uc_module)} | {md_escape(u['n'])} | {u['m'][0]} | {u['p']} |")
lines.append("")
lines += ["## Use cases by lifecycle tag", ""]
for tag in sorted(tags, key=lambda t: -len(tags[t])):
ids = ", ".join(uc_link(u, uc_module) for u in sorted(tags[tag], key=uc_sort_key))
lines.append(f"- **{tag}** ({len(tags[tag])}): {ids}")
lines.append("")
lines += ["## Roles", "", "Role → use cases in which the role takes part.", "", "| Role | Count | Use cases |", "|---|---|---|"]
for role in sorted(roles, key=lambda r: (-len(roles[r]), r)):
ids = ", ".join(uc_link(u, uc_module) for u in sorted(roles[role], key=uc_sort_key))
lines.append(f"| {role} | {len(roles[role])} | {ids} |")
lines.append("")
(OUT / "README.md").write_text("\n".join(lines), encoding="utf-8")
def write_role_module_matrix(data):
"""Role x module matrix: number of use cases in which the role takes part."""
uc = data["uc"]
codes = [code for _, group_codes in data["groups"] for code in group_codes]
counts = defaultdict(lambda: defaultdict(int))
for u in uc.values():
for actor in u["a"]:
counts[actor][u["m"][0]] += 1
roles = sorted(counts, key=lambda r: (-sum(counts[r].values()), r))
lines = [
"# Role × module participation (derived)",
"",
"> Generated by `docs/agent/tools/gen_use_cases.py`. Each cell is the number of "
"use cases in that module in which the role takes part (any step). It shows "
"involvement, not permission level; the curated permission view is in "
"[../13-role-matrix.md](../13-role-matrix.md) and "
"[../14-security-matrix.md](../14-security-matrix.md).",
"",
"| Role | Total | " + " | ".join(codes) + " |",
"|---|---|" + "---|" * len(codes),
]
for role in roles:
cells = [str(counts[role][c]) if counts[role][c] else "" for c in codes]
lines.append(f"| {role} | {sum(counts[role].values())} | " + " | ".join(cells) + " |")
lines.append("")
(OUT / "role-module-matrix.md").write_text("\n".join(lines), encoding="utf-8")
def main():
data = load_data()
OUT.mkdir(parents=True, exist_ok=True)
for old in OUT.glob("*.md"):
old.unlink()
uc_module = {uid: u["m"][0] for uid, u in data["uc"].items()}
for code, mod in data["mods"].items():
write_module(code, mod, data, uc_module)
write_index(data, uc_module)
write_role_module_matrix(data)
print(f"Wrote {len(data['mods']) + 2} files to {OUT.relative_to(ROOT)}")
if __name__ == "__main__":
main()