#!/usr/bin/env python3 """Generate the machine-readable OpenUP model intermediates from the markdown sources. Transforms the approved OpenUP artifacts (never hand-edited exports): docs/openup/analysis-design/uc-realization-mapping.md (§2 table) docs/openup/requirements/use-case-model.md (canonical UC names) into: docs/openup/model/exports/uc-realizations.json The use-case-model.json intermediate is NOT produced here: the markdown does not carry per-step actor tags, so a faithful transformation requires the EA session (export-model.ps1) or staged decisions. See docs/openup/model/README.md. Runs on the standard library only (Linux, no EA/COM required). """ import json import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] MAPPING_MD = REPO_ROOT / "docs/openup/analysis-design/uc-realization-mapping.md" UC_MODEL_MD = REPO_ROOT / "docs/openup/requirements/use-case-model.md" OUTPUT_JSON = REPO_ROOT / "docs/openup/model/exports/uc-realizations.json" UC_ID_PATTERN = re.compile(r"^(UC-\d{2})\s*(.*)$") def canonical_use_case_names() -> dict[str, str]: """UC id -> full use case name, from the use-case-model.md overview table.""" names: dict[str, str] = {} for line in UC_MODEL_MD.read_text(encoding="utf-8").splitlines(): cells = [cell.strip() for cell in line.strip().strip("|").split("|")] if len(cells) < 2: continue match = UC_ID_PATTERN.match(cells[0]) if match: names[match.group(1)] = cells[1] if len(names) != 12: raise SystemExit( f"expected 12 use cases in {UC_MODEL_MD}, found {len(names)}" ) return names def split_table_row(line: str) -> list[str]: return [cell.strip() for cell in line.strip().strip("|").split("|")] def is_table_row(line: str) -> bool: return line.strip().startswith("|") and line.strip().endswith("|") def parse_realization_table(mapping_text: str) -> tuple[list[dict], dict[str, str]]: """Parse the §2 mapping table into per-UC realization entries. Returns (realizations, paragraphs) where paragraphs carries the verbatim cross-cutting and dependency-rule paragraphs that follow the table. """ lines = mapping_text.splitlines() section_started = False realizations: list[dict] = [] current: dict | None = None for index, line in enumerate(lines): if line.startswith("## 2. UC realization mapping"): section_started = True continue if not section_started: continue if line.startswith("## 3."): break if not is_table_row(line): continue cells = split_table_row(line) if cells[0] and set(cells[0]) <= {"-", " ", ":"}: continue if cells[0] in ("UC", "Analysis class"): continue uc_cell = cells[0] element = { # Emphasis markers are formatting, not content; element names are # identifiers (e.g. **AdventureService** -> AdventureService). # All other cells stay verbatim. "name": cells[1].replace("**", ""), "stereotype": cells[2], "designElement": cells[3], "notes": cells[4] if len(cells) > 4 else "", } id_match = UC_ID_PATTERN.match(uc_cell) if id_match: current = {"id": f"{id_match.group(1)} Realization", "useCaseId": id_match.group(1), "elements": [element]} realizations.append(current) elif uc_cell == "" and current is not None: current["elements"].append(element) else: raise SystemExit(f"unparseable mapping row {index + 1}: {line!r}") paragraphs = {} for paragraph_key, paragraph_prefix in ( ("crossCutting", "Cross-cutting design elements"), ("dependencyRule", "Dependency rule check"), ): paragraphs[paragraph_key] = next( (line for line in lines if line.startswith(paragraph_prefix)), "" ) return realizations, paragraphs def build_payload() -> dict: canonical_names = canonical_use_case_names() realizations, paragraphs = parse_realization_table( MAPPING_MD.read_text(encoding="utf-8") ) if len(realizations) != 12: raise SystemExit( f"expected 12 realizations in {MAPPING_MD}, found {len(realizations)}" ) for realization in realizations: realization["useCase"] = canonical_names[realization.pop("useCaseId")] return { "exportedFrom": ( "docs/openup/analysis-design/uc-realization-mapping.md §2, generated by " "scripts/openup/markdown-to-json.py — generated file, never hand-edit" ), "realizations": realizations, "crossCutting": paragraphs["crossCutting"], "dependencyRule": paragraphs["dependencyRule"], } def main() -> int: payload = build_payload() OUTPUT_JSON.parent.mkdir(parents=True, exist_ok=True) OUTPUT_JSON.write_text( json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" ) element_count = sum(len(r["elements"]) for r in payload["realizations"]) print( f"wrote {OUTPUT_JSON.relative_to(REPO_ROOT)}: " f"{len(payload['realizations'])} realizations, {element_count} elements" ) return 0 if __name__ == "__main__": sys.exit(main())