Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 85 additions & 19 deletions src/extensions/score_metamodel/docs/generate_metamodel_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -42,43 +61,83 @@ 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 = (
", ".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)
lines.append(" - " + mandatory)
lines.append(" - " + links)
lines.append(" - " + inc)
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] = []
for name in sorted(types):
mandatory_opts = sorted(types[name].get("mandatory_options", {}).keys())
if mandatory_opts:
lines.append(f"class {name} {{")
lines.extend(f" +{opt}" for opt in mandatory_opts)
lines.append("}")
else:
lines.append(f"class {name}")
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()):
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)
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)
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


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:
lines.append(f"style {name} fill:{color},stroke:#666,color:#000")
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)
Expand Down Expand Up @@ -109,7 +168,14 @@ def main() -> int:
return 1

table = _build_table(types)
mermaid_lines = ["classDiagram"] + _build_mermaid(types)
mermaid_lines = [
"---",
"config:",
" layout: elk",
Comment thread
a-zw marked this conversation as resolved.
"---",
"classDiagram",
" direction BT",
] + _build_mermaid(types)
args.mmd_output.write_text("\n".join(mermaid_lines) + "\n", encoding="utf-8")
output = "\n".join(
[
Expand Down
24 changes: 24 additions & 0 deletions src/extensions/score_metamodel/metamodel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ needs_types:
# TSF
tsf:
title: TSF
color: "#E1D5E7"
mandatory_options:
id: ^tsf__[0-9a-zA-Z_-]*$
status: ^(draft|valid)$
Expand All @@ -72,6 +73,7 @@ needs_types:

tenet:
title: Tenet
color: "#E1D5E7"
mandatory_options:
id: ^tenet__[0-9a-zA-Z_-]*$
status: ^(draft|valid)$
Expand All @@ -80,6 +82,7 @@ needs_types:
parts: 3

assertion:
color: "#E1D5E7"
title: Assertion
mandatory_options:
id: ^assertion__[0-9a-zA-Z_-]*$
Expand All @@ -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)$
Expand All @@ -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)$
Expand All @@ -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)$
Expand All @@ -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]+$
Expand All @@ -141,6 +148,7 @@ needs_types:

gd_temp:
title: Process Template
color: "#E1D5E7"
mandatory_options:
status: ^(valid|draft)$
optional_links:
Expand All @@ -149,6 +157,7 @@ needs_types:

gd_chklst:
title: Process Checklist
color: "#E1D5E7"
mandatory_options:
status: ^(valid|draft)$
optional_links:
Expand All @@ -157,6 +166,7 @@ needs_types:

gd_guidl:
title: Process Guideline
color: "#E1D5E7"
mandatory_options:
status: ^(valid|draft)$
optional_links:
Expand All @@ -165,6 +175,7 @@ needs_types:

gd_method:
title: Process Method
color: "#E1D5E7"
prefix: gd_meth__
mandatory_options:
status: ^(valid|draft)$
Expand All @@ -175,6 +186,7 @@ needs_types:
# S-CORE Workproduct
workproduct:
title: Workproduct
color: "#E1D5E7"
prefix: wp__
mandatory_options:
status: ^(valid|draft)$
Expand All @@ -186,6 +198,7 @@ needs_types:
# Role
role:
title: Role
color: "#E1D5E7"
prefix: rl__
optional_links:
contains: role
Expand Down Expand Up @@ -689,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
Expand All @@ -709,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: ^.+$
Expand Down Expand Up @@ -740,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: ^.+$
Expand Down Expand Up @@ -770,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: ^.*$
Expand Down Expand Up @@ -802,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: ^.*$
Expand Down Expand Up @@ -831,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])$
Expand All @@ -844,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])$
Expand All @@ -857,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])$
Expand All @@ -870,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]))$
Expand All @@ -895,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)$
Expand All @@ -914,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)$
Expand Down
1 change: 1 addition & 0 deletions src/extensions/score_sphinx_bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading