Per design-wi-2-architecture-cleanup.md §11.b/c: keyfob-station pipeline studied (add-wi19-uc11.ps1, export-model-json.ps1). Platform gap: EA COM pipeline is Windows-only; this session ran on Linux (no EABase.eapx found, no pwsh) - .eapx skeleton NOT committed, documented in model/README.md with the two path options. Landed: - docs/openup/model/BUILD-CHECKLIST.md: full EA-session checklist (packages, 3 actors, 12 UCs with scenarios, 13 associations, 12 realization packages / 36 elements) derived from use-case-model.md + uc-realization-mapping.md - docs/openup/model/README.md: platform note, status, drift rule - docs/openup/model/exports/uc-realizations.json: generated from mapping §2 via scripts/openup/markdown-to-json.py (tested locally) - scripts/openup/build-model.ps1, export-model.ps1: adapted from keyfob patterns, Windows-gated (human gate per design §11.d) - scripts/openup/markdown-to-json.py: local markdown -> JSON transform
149 lines
5.3 KiB
Python
149 lines
5.3 KiB
Python
#!/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())
|