ความคืบหน้างาน ไฟล์ต้นฉบับ

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()