openup: EA model preparation (COD) - checklist, scripts, realizations intermediate

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
This commit is contained in:
Jan-Peter von Hunnius 2026-10-03 17:38:06 +02:00
parent 7a9a5a3e7b
commit 41f1ec2887
6 changed files with 1311 additions and 0 deletions

View file

@ -0,0 +1,149 @@
#!/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())