From 03887b3221ecd6a3a6cbcae1f9f7b878cde2e86f Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 09:31:58 +0200 Subject: [PATCH 01/10] docs: improve metamodel vizualisation * use metamodel colors in diagram * show incoming mandatory links in table --- .../docs/generate_metamodel_rst.py | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index 423cc8947..fa5561a22 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -33,7 +33,26 @@ def _parse_yaml(path: Path) -> dict: return yaml.load(fh) # type: ignore[no-any-return] +def _split_targets(targets: str) -> list[str]: + """Split a comma-separated target spec, ignoring the ``ANY`` wildcard.""" + if targets == "ANY": + return [] + return [t.strip() for t in targets.split(",") if t.strip()] + + +def _incoming_mandatory_links(types: dict) -> dict[str, list[str]]: + """Map each type to the source types that point at it via mandatory links.""" + incoming: dict[str, list[str]] = {name: [] for name in types} + for source, ty in types.items(): + for targets in ty.get("mandatory_links", {}).values(): + for target in _split_targets(targets): + if target in incoming and source not in incoming[target]: + incoming[target].append(source) + return {name: sorted(sources) for name, sources in incoming.items()} + + def _build_table(types: dict) -> list[str]: + incoming = _incoming_mandatory_links(types) lines: list[str] = [] lines.append(".. list-table:: Need Types") lines.append(" :header-rows: 1") @@ -42,6 +61,7 @@ def _build_table(types: dict) -> list[str]: lines.append(" - Title") lines.append(" - Mandatory Options") lines.append(" - Links") + lines.append(" - Incoming Mandatory Links") for name, ty in sorted(types.items()): title = ty.get("title", name) mandatory = ( @@ -53,29 +73,39 @@ def _build_table(types: dict) -> list[str]: links = f"{optional_links} | mandatory: {mandatory_links}" else: links = optional_links or "\u2014" + inc = ", ".join(incoming.get(name, [])) or "\u2014" lines.append(" * - " + name) lines.append(" - " + title) lines.append(" - " + mandatory) lines.append(" - " + links) + lines.append(" - " + inc) return lines def _build_mermaid(types: dict) -> list[str]: lines: list[str] = [] + # Declare every type so isolated nodes render and can be styled. + for name in sorted(types): + lines.append(f"class {name}") + # Edges for all (mandatory + optional) links. seen: set[tuple[str, str, str]] = set() for name, ty in sorted(types.items()): - for link_name, targets in sorted( - list(ty.get("mandatory_links", {}).items()) - + list(ty.get("optional_links", {}).items()) - ): - target_types = targets.split(", ") if targets != "ANY" else [] - for target in target_types: - target = target.strip() - if target and target in types: - key = (name, target, link_name) - if key not in seen: - seen.add(key) - lines.append(f"{name} --> {target} : {link_name}") + links = list(ty.get("mandatory_links", {}).items()) + list( + ty.get("optional_links", {}).items() + ) + for link_name, targets in sorted(links): + for target in _split_targets(targets): + if target not in types: + continue + key = (name, target, link_name) + if key not in seen: + seen.add(key) + lines.append(f"{name} --> {target} : {link_name}") + # Color nodes per the ``color`` option in metamodel.yaml. + for name, ty in sorted(types.items()): + color = ty.get("color") + if color: + lines.append(f"style {name} fill:{color}") return lines From b89129f304de7d7b8643998b31cfa1bc3968cd19 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 09:38:30 +0200 Subject: [PATCH 02/10] docs: mandatory links in bold --- .../score_metamodel/docs/generate_metamodel_rst.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index fa5561a22..ceb821857 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -67,12 +67,13 @@ def _build_table(types: dict) -> list[str]: mandatory = ( ", ".join(sorted(ty.get("mandatory_options", {}).keys())) or "\u2014" ) - optional_links = ", ".join(sorted(ty.get("optional_links", {}).keys())) - mandatory_links = ", ".join(sorted(ty.get("mandatory_links", {}).keys())) - if mandatory_links: - links = f"{optional_links} | mandatory: {mandatory_links}" - else: - links = optional_links or "\u2014" + optional_links = ty.get("optional_links", {}) + mandatory_links = ty.get("mandatory_links", {}) + link_strs = [ + f"**{n}**" if n in mandatory_links else n + for n in sorted(set(optional_links) | set(mandatory_links)) + ] + links = ", ".join(link_strs) or "\u2014" inc = ", ".join(incoming.get(name, [])) or "\u2014" lines.append(" * - " + name) lines.append(" - " + title) From c39eb91ddbacc9cfcfc4fd12ea53cbb89e36d27e Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 09:43:01 +0200 Subject: [PATCH 03/10] docs: use elk layout --- .../score_metamodel/docs/generate_metamodel_rst.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index ceb821857..4c5cc0ca8 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -140,7 +140,13 @@ def main() -> int: return 1 table = _build_table(types) - mermaid_lines = ["classDiagram"] + _build_mermaid(types) + mermaid_lines = [ + "---", + "config:", + " layout: elk", + "---", + "classDiagram", + ] + _build_mermaid(types) args.mmd_output.write_text("\n".join(mermaid_lines) + "\n", encoding="utf-8") output = "\n".join( [ From 29f31be9de85405fcd7f78cf3895016126eaf368 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 09:43:16 +0200 Subject: [PATCH 04/10] docs: show mandatory options in diagram --- .../score_metamodel/docs/generate_metamodel_rst.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index 4c5cc0ca8..1d2821817 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -86,8 +86,16 @@ def _build_table(types: dict) -> list[str]: def _build_mermaid(types: dict) -> list[str]: lines: list[str] = [] # Declare every type so isolated nodes render and can be styled. + # List the mandatory options as class members (optional ones are omitted). for name in sorted(types): - lines.append(f"class {name}") + mandatory_opts = sorted(types[name].get("mandatory_options", {}).keys()) + if mandatory_opts: + lines.append(f"class {name} {{") + for opt in mandatory_opts: + lines.append(f" +{opt}") + lines.append("}") + else: + lines.append(f"class {name}") # Edges for all (mandatory + optional) links. seen: set[tuple[str, str, str]] = set() for name, ty in sorted(types.items()): From c3d0b15e946607b2daf99046c8e68293914f2633 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 10:13:46 +0200 Subject: [PATCH 05/10] docs: use lavender color for process --- src/extensions/score_metamodel/metamodel.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index f81584a6e..2d0673d05 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -63,6 +63,7 @@ needs_types: # TSF tsf: title: TSF + color: "#E1D5E7" mandatory_options: id: ^tsf__[0-9a-zA-Z_-]*$ status: ^(draft|valid)$ @@ -72,6 +73,7 @@ needs_types: tenet: title: Tenet + color: "#E1D5E7" mandatory_options: id: ^tenet__[0-9a-zA-Z_-]*$ status: ^(draft|valid)$ @@ -80,6 +82,7 @@ needs_types: parts: 3 assertion: + color: "#E1D5E7" title: Assertion mandatory_options: id: ^assertion__[0-9a-zA-Z_-]*$ @@ -92,6 +95,7 @@ needs_types: # req-Id: tool_req__docs_stdreq_types std_req: title: Standard Requirement + color: "#E1D5E7" mandatory_options: id: ^std_req__(iso26262|isosae21434|isopas8926|aspice_40)__[0-9a-zA-Z_-]*$ status: ^(valid)$ @@ -101,6 +105,7 @@ needs_types: std_wp: title: Standard Work Product + color: "#E1D5E7" mandatory_options: id: ^std_wp__(iso26262|isosae21434|isopas8926|aspice_40)__[0-9a-z_]*$ status: ^(valid)$ @@ -110,6 +115,7 @@ needs_types: # req-Id: tool_req__docs_wf_types workflow: title: Workflow + color: "#E1D5E7" prefix: wf__ mandatory_options: status: ^(valid|draft)$ @@ -127,6 +133,7 @@ needs_types: # req-Id: tool_req__docs_req_types gd_req: title: Process Requirements + color: "#E1D5E7" mandatory_options: # req-Id: tool_req__docs_common_attr_description content: ^[\s\S]+$ @@ -141,6 +148,7 @@ needs_types: gd_temp: title: Process Template + color: "#E1D5E7" mandatory_options: status: ^(valid|draft)$ optional_links: @@ -149,6 +157,7 @@ needs_types: gd_chklst: title: Process Checklist + color: "#E1D5E7" mandatory_options: status: ^(valid|draft)$ optional_links: @@ -157,6 +166,7 @@ needs_types: gd_guidl: title: Process Guideline + color: "#E1D5E7" mandatory_options: status: ^(valid|draft)$ optional_links: @@ -165,6 +175,7 @@ needs_types: gd_method: title: Process Method + color: "#E1D5E7" prefix: gd_meth__ mandatory_options: status: ^(valid|draft)$ @@ -175,6 +186,7 @@ needs_types: # S-CORE Workproduct workproduct: title: Workproduct + color: "#E1D5E7" prefix: wp__ mandatory_options: status: ^(valid|draft)$ @@ -186,6 +198,7 @@ needs_types: # Role role: title: Role + color: "#E1D5E7" prefix: rl__ optional_links: contains: role From 1d8bae2552e9288227e174a091c1e6e0bc544cb7 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 10:21:41 +0200 Subject: [PATCH 06/10] fix: readable in dark mode --- src/extensions/score_metamodel/docs/generate_metamodel_rst.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index 1d2821817..0c34fae07 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -114,7 +114,7 @@ def _build_mermaid(types: dict) -> list[str]: for name, ty in sorted(types.items()): color = ty.get("color") if color: - lines.append(f"style {name} fill:{color}") + lines.append(f"style {name} fill:{color},stroke:#666,color:#000") return lines From 624fa8265a4f44790bf31a3d4df0cbdc319d3bc2 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 10:38:22 +0200 Subject: [PATCH 07/10] docs: spread colors to more types --- src/extensions/score_metamodel/metamodel.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 2d0673d05..cb23cb5f4 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -702,6 +702,7 @@ needs_types: # No requirement!! plat_saf_dfa: title: Platform Dependent Failure Analysis + color: "#DAE8FC" mandatory_options: failure_id: ^.+$ # req-Id: tool_req__docs_saf_attr_fmea_failure_effect @@ -722,6 +723,7 @@ needs_types: # req-Id: tool_req__docs_saf_types feat_saf_dfa: title: Feature DFA (Dependent Failure Analysis) + color: "#FFF2CC" mandatory_options: # req-Id: tool_req__docs_saf_attr_dfa_failure_id failure_id: ^.+$ @@ -753,6 +755,7 @@ needs_types: # req-Id: tool_req__docs_saf_types comp_saf_dfa: title: Component DFA (Dependent Failure Analysis) + color: "#D5E8D4" mandatory_options: # req-Id: tool_req__docs_saf_attr_dfa_failure_id failure_id: ^.+$ @@ -783,6 +786,7 @@ needs_types: # req-Id: tool_req__docs_saf_types feat_saf_fmea: title: Feature FMEA (Failure Mode and Effects Analysis) + color: "#FFF2CC" mandatory_options: # req-Id: tool_req__docs_saf_attr_fmea_fault_id fault_id: ^.*$ @@ -815,6 +819,7 @@ needs_types: # req-Id: tool_req__docs_saf_types comp_saf_fmea: title: Component FMEA (Failure Mode and Effects Analysis) + color: "#D5E8D4" mandatory_options: # req-Id: tool_req__docs_saf_attr_fmea_fault_id fault_id: ^.*$ @@ -844,6 +849,7 @@ needs_types: # req-Id: tool_req__docs_sec_types feat_sec_threat: title: Feature Security Analysis Threat (STRIDE) + color: "#FFF2CC" mandatory_options: # req-Id: tool_req__docs_sec_attr_stride_threat_id threat_id: ^(AU_01_0[1-3]|AZ_01_0[1-3]|CT_01_0[1-2]|DS_01_0[1-3]|EX_01_0[1-6]|LA_01_0[1-3]|MT_01_0[1-7])$ @@ -857,6 +863,7 @@ needs_types: # req-Id: tool_req__docs_sec_types comp_sec_threat: title: Component Security Analysis Threat (STRIDE) + color: "#D5E8D4" mandatory_options: # req-Id: tool_req__docs_sec_attr_stride_threat_id threat_id: ^(AU_01_0[1-3]|AZ_01_0[1-3]|CT_01_0[1-2]|DS_01_0[1-3]|EX_01_0[1-6]|LA_01_0[1-3]|MT_01_0[1-7])$ @@ -870,6 +877,7 @@ needs_types: # req-Id: tool_req__docs_sec_types plat_sec_threat: title: Platform Security Analysis Threat (STRIDE) + color: "#DAE8FC" mandatory_options: # req-Id: tool_req__docs_sec_attr_stride_threat_id threat_id: ^(AU_01_0[1-3]|AZ_01_0[1-3]|CT_01_0[1-2]|DS_01_0[1-3]|EX_01_0[1-6]|LA_01_0[1-3]|MT_01_0[1-7])$ @@ -883,6 +891,7 @@ needs_types: # req-Id: tool_req__docs_sec_types feat_sec_ana: title: Feature Security Analysis (Threat Scenario) + color: "#FFF2CC" mandatory_options: # req-Id: tool_req__docs_sec_attrs_mandatory threat_scenario_id: ^(AS_01_(01|02|04|05|06|07|09|10)|CO_01_0[1-7]|SC_01_0[2-5]|SI_01_0[2-5]|UI_01_(0[1-9]|1[0-2]))$ @@ -908,6 +917,7 @@ needs_types: # req-Id: tool_req__docs_sec_types comp_sec_ana: title: Component Security Analysis (Threat Scenario) + color: "#D5E8D4" mandatory_options: threat_scenario_id: ^(AS_01_(01|02|04|05|06|07|09|10)|CO_01_0[1-7]|SC_01_0[2-5]|SI_01_0[2-5]|UI_01_(0[1-9]|1[0-2]))$ status: ^(valid|invalid)$ @@ -927,6 +937,7 @@ needs_types: # req-Id: tool_req__docs_sec_types plat_sec_ana: title: Platform Security Analysis (Threat Scenario) + color: "#DAE8FC" mandatory_options: threat_scenario_id: ^(AS_01_(01|02|04|05|06|07|09|10)|CO_01_0[1-7]|SC_01_0[2-5]|SI_01_0[2-5]|UI_01_(0[1-9]|1[0-2]))$ status: ^(valid|invalid)$ From 9a177622051024ab42d0603d485100d4024ac094 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 10:59:53 +0200 Subject: [PATCH 08/10] refactor: fix complexity warning --- .../docs/generate_metamodel_rst.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index 0c34fae07..54eda585f 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -83,20 +83,23 @@ def _build_table(types: dict) -> list[str]: return lines -def _build_mermaid(types: dict) -> list[str]: +def _class_declarations(types: dict) -> list[str]: + """Declare every type, listing mandatory options as class members.""" lines: list[str] = [] - # Declare every type so isolated nodes render and can be styled. - # List the mandatory options as class members (optional ones are omitted). for name in sorted(types): mandatory_opts = sorted(types[name].get("mandatory_options", {}).keys()) if mandatory_opts: lines.append(f"class {name} {{") - for opt in mandatory_opts: - lines.append(f" +{opt}") + lines.extend(f" +{opt}" for opt in mandatory_opts) lines.append("}") else: lines.append(f"class {name}") - # Edges for all (mandatory + optional) links. + return lines + + +def _link_edges(types: dict) -> list[str]: + """Edges for all (mandatory + optional) links between known types.""" + lines: list[str] = [] seen: set[tuple[str, str, str]] = set() for name, ty in sorted(types.items()): links = list(ty.get("mandatory_links", {}).items()) + list( @@ -110,7 +113,16 @@ def _build_mermaid(types: dict) -> list[str]: if key not in seen: seen.add(key) lines.append(f"{name} --> {target} : {link_name}") - # Color nodes per the ``color`` option in metamodel.yaml. + return lines + + +def _node_styles(types: dict) -> list[str]: + """Color nodes per the ``color`` option in metamodel.yaml. + + Dark text and a visible border keep pastel-filled nodes readable + in both light and dark mermaid themes. + """ + lines: list[str] = [] for name, ty in sorted(types.items()): color = ty.get("color") if color: @@ -118,6 +130,10 @@ def _build_mermaid(types: dict) -> list[str]: return lines +def _build_mermaid(types: dict) -> list[str]: + return _class_declarations(types) + _link_edges(types) + _node_styles(types) + + def main() -> int: parser = argparse.ArgumentParser(description="Generate RST from metamodel.yaml") parser.add_argument("--rst-output", type=Path, required=True) From 53844741bbef78f41aa30e7abf50d7050ede83e1 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 13:19:19 +0200 Subject: [PATCH 09/10] fix: elk layout bottom-up --- src/extensions/score_metamodel/docs/generate_metamodel_rst.py | 1 + src/extensions/score_sphinx_bundle/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index 54eda585f..cf9b531ab 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -170,6 +170,7 @@ def main() -> int: " layout: elk", "---", "classDiagram", + " direction BT", ] + _build_mermaid(types) args.mmd_output.write_text("\n".join(mermaid_lines) + "\n", encoding="utf-8") output = "\n".join( diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index d45e60385..11adca09e 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -55,6 +55,7 @@ def setup(app: Sphinx) -> dict[str, object]: # Same as current VS Code extension config_setdefault(app.config, "mermaid_version", "11.6.0") config_setdefault(app.config, "mermaid_d3_zoom", True) + config_setdefault(app.config, "mermaid_include_elk", True) # The following entries are not required when building the documentation via # 'bazel build //:docs', as that command runs in a sandboxed environment. From 9109add2240de928bf6c853c75cb8273d0e77bc1 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Tue, 18 Aug 2026 13:26:18 +0200 Subject: [PATCH 10/10] docs: improve layout via directions --- .../score_metamodel/docs/generate_metamodel_rst.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py index cf9b531ab..0e2ef5ee3 100644 --- a/src/extensions/score_metamodel/docs/generate_metamodel_rst.py +++ b/src/extensions/score_metamodel/docs/generate_metamodel_rst.py @@ -112,7 +112,11 @@ def _link_edges(types: dict) -> list[str]: key = (name, target, link_name) if key not in seen: seen.add(key) - lines.append(f"{name} --> {target} : {link_name}") + if link_name.endswith("_by"): + # for layouting, reverse link direction for "passive" verbs + lines.append(f"{target} <-- {name} : {link_name}") + else: + lines.append(f"{name} --> {target} : {link_name}") return lines