From c498e7edaf9e0e9ea91bbb9d95a0bfaf7d611cbe Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Tue, 7 Apr 2026 18:48:55 +0200 Subject: [PATCH 1/8] FMEA xml table --- docs/conf.py | 7 + docs/extensions/fmea_xml_table.py | 145 ++++++++++++++++++ .../persistency/safety_analysis/fmea.rst | 58 +------ .../persistency/safety_analysis/fmea.xml | 69 +++++++++ 4 files changed, 224 insertions(+), 55 deletions(-) create mode 100644 docs/extensions/fmea_xml_table.py create mode 100644 docs/features/persistency/safety_analysis/fmea.xml diff --git a/docs/conf.py b/docs/conf.py index 5690729f596..3120c4b0992 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,12 @@ # Configuration file for the Sphinx documentation builder. +import os +import sys + +# Make local extensions importable +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "extensions")) + project = "S-CORE" project_url = "https://eclipse-score.github.io/score" version = "0.1" @@ -23,4 +29,5 @@ # is updated with new sphinx-needs version "sphinxcontrib.plantuml", "score_sphinx_bundle", + "fmea_xml_table", ] diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py new file mode 100644 index 00000000000..25f0233f83a --- /dev/null +++ b/docs/extensions/fmea_xml_table.py @@ -0,0 +1,145 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +""" +Sphinx extension that provides the ``fmea_xml_table`` directive. + +The directive reads a FMEA XML file and: + 1. Registers each entry as a hidden ``feat_saf_fmea`` need in sphinx-needs. + 2. Renders all entries as a table with the columns defined in FMEA_COLUMNS. + +Usage in RST:: + + .. fmea_xml_table:: path/to/fmea.xml + +The path is resolved relative to the document that contains the directive. +""" + +import os +import xml.etree.ElementTree as ET +from collections.abc import Sequence + +from docutils import nodes +from sphinx.util.docutils import SphinxDirective +from sphinx_needs.api import add_external_need + +FMEA_COLUMNS = [ + "id", + "violates", + "fault_id", + "failure_effect", + "mitigated_by", + "sufficient", + "status", + "safety_relevant", + "root_cause", + "content", +] + +# Fields passed to add_need (must match metamodel's feat_saf_fmea definition). +# safety_relevant and root_cause are not in the metamodel, so they are +# displayed in the table only. +_NEED_EXTRA_KWARGS = {"fault_id", "failure_effect", "sufficient"} +_NEED_LINK_KWARGS = {"violates", "mitigated_by"} + + +class FmeaXmlTable(SphinxDirective): + """Import FMEA data from an XML file and render it as a table.""" + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} + + def run(self) -> Sequence[nodes.Node]: + xml_rel_path = self.arguments[0] + xml_abs_path = self.env.relfn2path(xml_rel_path, self.env.docname)[1] + + if not os.path.exists(xml_abs_path): + msg = f"fmea_xml_table: file not found: {xml_abs_path}" + return [nodes.system_message(msg, level=3, type="ERROR", line=self.lineno)] + + tree = ET.parse(xml_abs_path) + root = tree.getroot() + + entries: list[dict[str, str]] = [] + result_nodes: list[nodes.Node] = [] + + for entry_elem in root.findall("entry"): + row = { + field: (entry_elem.findtext(field) or "").strip() + for field in FMEA_COLUMNS + } + entries.append(row) + + violates_str = row["violates"] + mitigated_str = row["mitigated_by"] + + add_external_need( + app=self.env.app, + need_type="feat_saf_fmea", + title=row["id"], + id=row["id"], + external_url="", + content=row["content"], + status=row["status"], + fault_id=row["fault_id"], + failure_effect=row["failure_effect"], + sufficient=row["sufficient"], + violates=violates_str, + mitigated_by=mitigated_str, + ) + + result_nodes.append(_build_table(entries)) + return result_nodes + + +def _build_table(entries: list[dict[str, str]]) -> nodes.table: + table = nodes.table() + tgroup = nodes.tgroup(cols=len(FMEA_COLUMNS)) + table += tgroup + + for _ in FMEA_COLUMNS: + tgroup += nodes.colspec(colwidth=1) + + # Header + thead = nodes.thead() + tgroup += thead + hrow = nodes.row() + thead += hrow + for col in FMEA_COLUMNS: + cell = nodes.entry() + cell += nodes.paragraph(text=col) + hrow += cell + + # Body + tbody = nodes.tbody() + tgroup += tbody + for row in entries: + trow = nodes.row() + tbody += trow + for col in FMEA_COLUMNS: + cell = nodes.entry() + cell += nodes.paragraph(text=row.get(col, "")) + trow += cell + + return table + + +def setup(app): + app.add_directive("fmea_xml_table", FmeaXmlTable) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/features/persistency/safety_analysis/fmea.rst b/docs/features/persistency/safety_analysis/fmea.rst index 98fae6189ed..f7f6547d004 100644 --- a/docs/features/persistency/safety_analysis/fmea.rst +++ b/docs/features/persistency/safety_analysis/fmea.rst @@ -40,59 +40,7 @@ Fault models - EX_01_06: Processing is not complete (infinite loop): Failure initiator not applicable at persistency, so no mitigation is needed. The feature is developed fully deterministic, so no infinite loop is expected caused by persistency. -.. feat_saf_fmea:: Persistency - :violates: feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - :id: feat_saf_fmea__persistency__message_nreived - :fault_id: MF_01_01 - :failure_effect: Message is not received so the feature persistency is not available. - :mitigated_by: aou_req__persistency__error_handling - :sufficient: yes - :status: valid +Failure Mode List +----------------- - User is not able to use the feature. Middleware cant be used. User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. - Failure handling is addressed to the application by the aou_req__persistency__error_handling. - -.. feat_saf_fmea:: Persistency - :violates: feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - :id: feat_saf_fmea__persistency__late_message - :fault_id: MF_01_02 - :failure_effect: message received too late. - :mitigated_by: aou_req__persistency__error_handling - :sufficient: yes - :status: valid - - Subset of MF_01_01 if the delay is to long. - -.. feat_saf_fmea:: Persistency - :violates: feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - :id: feat_saf_fmea__persistency__corrupted_message - :fault_id: MF_01_05 - :failure_effect: message is corrupted so the feature persistency is not available. - :mitigated_by: aou_req__persistency__error_handling - :sufficient: yes - :status: valid - - Covered by MF_01_01 - -.. feat_saf_fmea:: Persistency - :violates: feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - :id: feat_saf_fmea__persistency__not_sent - :fault_id: MF_01_06 - :failure_effect: message is not sent so the feature persistency is not available. - :mitigated_by: aou_req__persistency__error_handling - :sufficient: yes - :status: valid - - Covered by MF_01_01 because the violation cause is the same. - -.. feat_saf_fmea:: Persistency - :violates: feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - :id: feat_saf_fmea__persistency__err_handl - :fault_id: EX_01_04 - :failure_effect: loss of execution will lead to an unavailability of the persistency feature. - :mitigated_by: aou_req__persistency__error_handling - :sufficient: yes - :status: valid - - User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. - Failure handling is addressed to the application by the aou_req__persistency__error_handling. +.. fmea_xml_table:: fmea.xml diff --git a/docs/features/persistency/safety_analysis/fmea.xml b/docs/features/persistency/safety_analysis/fmea.xml new file mode 100644 index 00000000000..3cc62837ae1 --- /dev/null +++ b/docs/features/persistency/safety_analysis/fmea.xml @@ -0,0 +1,69 @@ + + + + feat_saf_fmea__persistency__message_nreived + feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore + MF_01_01 + Message is not received so the feature persistency is not available. + aou_req__persistency__error_handling + yes + valid + NA + NA + User is not able to use the feature. Middleware cant be used. User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. +Failure handling is addressed to the application by the aou_req__persistency__error_handling. + + + + feat_saf_fmea__persistency__late_message + feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore + MF_01_02 + message received too late. + aou_req__persistency__error_handling + yes + valid + NA + NA + Subset of MF_01_01 if the delay is to long. + + + + feat_saf_fmea__persistency__corrupted_message + feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore + MF_01_05 + message is corrupted so the feature persistency is not available. + aou_req__persistency__error_handling + yes + valid + NA + NA + Covered by MF_01_01 + + + + feat_saf_fmea__persistency__not_sent + feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore + MF_01_06 + message is not sent so the feature persistency is not available. + aou_req__persistency__error_handling + yes + valid + NA + NA + Covered by MF_01_01 because the violation cause is the same. + + + + feat_saf_fmea__persistency__err_handl + feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore + EX_01_04 + loss of execution will lead to an unavailability of the persistency feature. + aou_req__persistency__error_handling + yes + valid + NA + NA + User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. +Failure handling is addressed to the application by the aou_req__persistency__error_handling. + + \ No newline at end of file From cf5d919fd385710806e35403333828a91798947a Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Wed, 8 Apr 2026 17:25:08 +0200 Subject: [PATCH 2/8] extend PER FMEA xml --- docs/conf.py | 8 +++ docs/extensions/fmea_xml_table.py | 94 +++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3120c4b0992..050a5ed4ed0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,14 @@ project_url = "https://eclipse-score.github.io/score" version = "0.1" +# Base URL of the fault models guideline page in the process_description build. +# fault_id values in FMEA XML files (e.g. "MF_01_01") are linked as +# {fmea_fault_model_base_url}#fmea_fault_model__mf_01_01 +fmea_fault_model_base_url = ( + "https://eclipse-score.github.io/process_description/" + "process_areas/safety_analysis/guidance/fault_models_guideline.html" +) + extensions = [ # TODO: remove plantuml here once # https://github.com/useblocks/sphinx-needs/pull/1508 is merged and docs-as-code diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py index 25f0233f83a..0b0c7dd4378 100644 --- a/docs/extensions/fmea_xml_table.py +++ b/docs/extensions/fmea_xml_table.py @@ -25,12 +25,15 @@ """ import os +import re import xml.etree.ElementTree as ET from collections.abc import Sequence from docutils import nodes +from sphinx.util.nodes import make_refnode from sphinx.util.docutils import SphinxDirective from sphinx_needs.api import add_external_need +from sphinx_needs.data import SphinxNeedsData FMEA_COLUMNS = [ "id", @@ -46,11 +49,18 @@ ] # Fields passed to add_need (must match metamodel's feat_saf_fmea definition). -# safety_relevant and root_cause are not in the metamodel, so they are -# displayed in the table only. -_NEED_EXTRA_KWARGS = {"fault_id", "failure_effect", "sufficient"} +# safety_relevant and root_cause are now in the metamodel. +_NEED_EXTRA_KWARGS = {"fault_id", "failure_effect", "sufficient", "safety_relevant", "root_cause"} _NEED_LINK_KWARGS = {"violates", "mitigated_by"} +# fault_id values (e.g. "MF_01_01") map to needs in the process_description build +# using the prefix "fmea_fault_model__" + lower-case value as the anchor. +_FAULT_ID_NEED_PREFIX = "fmea_fault_model__" + + +class FmeaNeedRef(nodes.Inline, nodes.Element): + """Placeholder node for need references rendered by fmea_xml_table.""" + class FmeaXmlTable(SphinxDirective): """Import FMEA data from an XML file and render it as a table.""" @@ -96,15 +106,73 @@ def run(self) -> Sequence[nodes.Node]: fault_id=row["fault_id"], failure_effect=row["failure_effect"], sufficient=row["sufficient"], + safety_relevant=row["safety_relevant"], + root_cause=row["root_cause"], violates=violates_str, mitigated_by=mitigated_str, ) - result_nodes.append(_build_table(entries)) + result_nodes.append(_build_table(entries, self)) return result_nodes -def _build_table(entries: list[dict[str, str]]) -> nodes.table: +def _append_fault_id_link(paragraph: nodes.paragraph, value: str, base_url: str) -> None: + """Render a fault_id value as a link to the fault models guideline page.""" + full_need_id = f"{_FAULT_ID_NEED_PREFIX}{value.lower()}" + ref = nodes.reference(value, value, refuri=f"{base_url}#{full_need_id}") + paragraph += ref + + +def _append_need_links(paragraph: nodes.paragraph, value: str, directive: SphinxDirective) -> None: + """Render a need-link field value as clickable references.""" + need_ids = [token.strip() for token in re.split(r"[,|]", value) if token.strip()] + if not need_ids: + paragraph += nodes.Text(value) + return + + for index, need_id in enumerate(need_ids): + if index: + paragraph += nodes.Text(", ") + + need_ref = FmeaNeedRef("", reftarget=need_id) + need_ref += nodes.Text(need_id) + paragraph += need_ref + + +def _resolve_fmea_need_refs(app, doctree: nodes.document, fromdocname: str) -> None: + """Replace placeholder FMEA need refs with final links once all needs are known.""" + needs_view = SphinxNeedsData(app.env).get_needs_view() + + for node in doctree.findall(FmeaNeedRef): + need_id = str(node.get("reftarget", "")) + target_need = needs_view.get(need_id) + + if target_need and target_need.get("docname") and not target_need.get("is_external"): + node.replace_self( + make_refnode( + app.builder, + fromdocname, + target_need["docname"], + need_id, + nodes.Text(need_id), + need_id, + ) + ) + continue + + external_url = target_need.get("external_url") if target_need else None + if external_url: + ref = nodes.reference(need_id, need_id, refuri=external_url) + external_css = target_need.get("external_css") if target_need else None + if external_css: + ref["classes"].append(external_css) + node.replace_self(ref) + continue + + node.replace_self(nodes.Text(need_id)) + + +def _build_table(entries: list[dict[str, str]], directive: SphinxDirective) -> nodes.table: table = nodes.table() tgroup = nodes.tgroup(cols=len(FMEA_COLUMNS)) table += tgroup @@ -130,14 +198,28 @@ def _build_table(entries: list[dict[str, str]]) -> nodes.table: tbody += trow for col in FMEA_COLUMNS: cell = nodes.entry() - cell += nodes.paragraph(text=row.get(col, "")) + paragraph = nodes.paragraph() + value = row.get(col, "") + if col in _NEED_LINK_KWARGS and value: + _append_need_links(paragraph, value, directive) + elif col == "fault_id" and value: + base_url = directive.env.app.config.fmea_fault_model_base_url + if base_url: + _append_fault_id_link(paragraph, value, base_url) + else: + paragraph += nodes.Text(value) + else: + paragraph += nodes.Text(value) + cell += paragraph trow += cell return table def setup(app): + app.add_config_value("fmea_fault_model_base_url", "", "env") app.add_directive("fmea_xml_table", FmeaXmlTable) + app.connect("doctree-resolved", _resolve_fmea_need_refs) return { "version": "0.1", "parallel_read_safe": True, From 18962c0b391b90f80c594593a46ad966c98985f4 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Wed, 15 Apr 2026 11:50:30 +0200 Subject: [PATCH 3/8] fix formatting issues --- docs/extensions/fmea_xml_table.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py index 0b0c7dd4378..e297445b544 100644 --- a/docs/extensions/fmea_xml_table.py +++ b/docs/extensions/fmea_xml_table.py @@ -30,8 +30,8 @@ from collections.abc import Sequence from docutils import nodes -from sphinx.util.nodes import make_refnode from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import make_refnode from sphinx_needs.api import add_external_need from sphinx_needs.data import SphinxNeedsData @@ -50,7 +50,13 @@ # Fields passed to add_need (must match metamodel's feat_saf_fmea definition). # safety_relevant and root_cause are now in the metamodel. -_NEED_EXTRA_KWARGS = {"fault_id", "failure_effect", "sufficient", "safety_relevant", "root_cause"} +_NEED_EXTRA_KWARGS = { + "fault_id", + "failure_effect", + "sufficient", + "safety_relevant", + "root_cause", +} _NEED_LINK_KWARGS = {"violates", "mitigated_by"} # fault_id values (e.g. "MF_01_01") map to needs in the process_description build @@ -116,14 +122,18 @@ def run(self) -> Sequence[nodes.Node]: return result_nodes -def _append_fault_id_link(paragraph: nodes.paragraph, value: str, base_url: str) -> None: +def _append_fault_id_link( + paragraph: nodes.paragraph, value: str, base_url: str +) -> None: """Render a fault_id value as a link to the fault models guideline page.""" full_need_id = f"{_FAULT_ID_NEED_PREFIX}{value.lower()}" ref = nodes.reference(value, value, refuri=f"{base_url}#{full_need_id}") paragraph += ref -def _append_need_links(paragraph: nodes.paragraph, value: str, directive: SphinxDirective) -> None: +def _append_need_links( + paragraph: nodes.paragraph, value: str, directive: SphinxDirective +) -> None: """Render a need-link field value as clickable references.""" need_ids = [token.strip() for token in re.split(r"[,|]", value) if token.strip()] if not need_ids: @@ -147,7 +157,11 @@ def _resolve_fmea_need_refs(app, doctree: nodes.document, fromdocname: str) -> N need_id = str(node.get("reftarget", "")) target_need = needs_view.get(need_id) - if target_need and target_need.get("docname") and not target_need.get("is_external"): + if ( + target_need + and target_need.get("docname") + and not target_need.get("is_external") + ): node.replace_self( make_refnode( app.builder, @@ -172,7 +186,9 @@ def _resolve_fmea_need_refs(app, doctree: nodes.document, fromdocname: str) -> N node.replace_self(nodes.Text(need_id)) -def _build_table(entries: list[dict[str, str]], directive: SphinxDirective) -> nodes.table: +def _build_table( + entries: list[dict[str, str]], directive: SphinxDirective +) -> nodes.table: table = nodes.table() tgroup = nodes.tgroup(cols=len(FMEA_COLUMNS)) table += tgroup From c662c5979d8dca8696b8c751b80cedeef9aa0663 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Wed, 15 Apr 2026 14:35:43 +0200 Subject: [PATCH 4/8] Add temporary optional need --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 050a5ed4ed0..3e146222e2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,3 +39,7 @@ "score_sphinx_bundle", "fmea_xml_table", ] + +# Extra options added for the feat_saf_fmea need type used by fmea_xml_table. +# These extend the base metamodel defined in score_sphinx_bundle. +needs_extra_options = ["safety_relevant", "root_cause"] From 7f8a70bb29014d7c5397e66f4f95706a6c960852 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Fri, 17 Apr 2026 17:31:26 +0200 Subject: [PATCH 5/8] restore fmea needs element --- docs/extensions/fmea_xml_table.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py index e297445b544..64626a302b6 100644 --- a/docs/extensions/fmea_xml_table.py +++ b/docs/extensions/fmea_xml_table.py @@ -32,7 +32,7 @@ from docutils import nodes from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode -from sphinx_needs.api import add_external_need +from sphinx_needs.api import add_need from sphinx_needs.data import SphinxNeedsData FMEA_COLUMNS = [ @@ -101,22 +101,28 @@ def run(self) -> Sequence[nodes.Node]: violates_str = row["violates"] mitigated_str = row["mitigated_by"] - add_external_need( + add_need( app=self.env.app, + state=self.state, + docname=self.env.docname, + lineno=self.lineno, need_type="feat_saf_fmea", title=row["id"], id=row["id"], - external_url="", content=row["content"], status=row["status"], + hide=True, fault_id=row["fault_id"], failure_effect=row["failure_effect"], sufficient=row["sufficient"], - safety_relevant=row["safety_relevant"], - root_cause=row["root_cause"], violates=violates_str, mitigated_by=mitigated_str, ) + # Provide an anchor so cross-document links (e.g. from requirements + # pages) can resolve to this ID without rendering a visible need box. + result_nodes.append( + nodes.target("", "", ids=[row["id"]], refid=row["id"]) + ) result_nodes.append(_build_table(entries, self)) return result_nodes From f39731a0fbeddd4da177400528a03cb4ea2c0ee0 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Fri, 17 Apr 2026 18:16:27 +0200 Subject: [PATCH 6/8] Fix formatting error --- docs/extensions/fmea_xml_table.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py index 64626a302b6..8295a6c7201 100644 --- a/docs/extensions/fmea_xml_table.py +++ b/docs/extensions/fmea_xml_table.py @@ -120,9 +120,7 @@ def run(self) -> Sequence[nodes.Node]: ) # Provide an anchor so cross-document links (e.g. from requirements # pages) can resolve to this ID without rendering a visible need box. - result_nodes.append( - nodes.target("", "", ids=[row["id"]], refid=row["id"]) - ) + result_nodes.append(nodes.target("", "", ids=[row["id"]], refid=row["id"])) result_nodes.append(_build_table(entries, self)) return result_nodes From 583d5ef8e69c17979f0cb0e94ed4de91f7ecb377 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Wed, 29 Apr 2026 10:17:49 +0200 Subject: [PATCH 7/8] switch from xml to json --- docs/conf.py | 13 - docs/extensions/fmea_xml_table.py | 247 ------------------ .../persistency/safety_analysis/fmea.json | 124 +++++++++ .../persistency/safety_analysis/fmea.rst | 7 +- .../persistency/safety_analysis/fmea.xml | 69 ----- 5 files changed, 130 insertions(+), 330 deletions(-) delete mode 100644 docs/extensions/fmea_xml_table.py create mode 100644 docs/features/persistency/safety_analysis/fmea.json delete mode 100644 docs/features/persistency/safety_analysis/fmea.xml diff --git a/docs/conf.py b/docs/conf.py index 3e146222e2e..c41134c2971 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,23 +23,10 @@ project_url = "https://eclipse-score.github.io/score" version = "0.1" -# Base URL of the fault models guideline page in the process_description build. -# fault_id values in FMEA XML files (e.g. "MF_01_01") are linked as -# {fmea_fault_model_base_url}#fmea_fault_model__mf_01_01 -fmea_fault_model_base_url = ( - "https://eclipse-score.github.io/process_description/" - "process_areas/safety_analysis/guidance/fault_models_guideline.html" -) - extensions = [ # TODO: remove plantuml here once # https://github.com/useblocks/sphinx-needs/pull/1508 is merged and docs-as-code # is updated with new sphinx-needs version "sphinxcontrib.plantuml", "score_sphinx_bundle", - "fmea_xml_table", ] - -# Extra options added for the feat_saf_fmea need type used by fmea_xml_table. -# These extend the base metamodel defined in score_sphinx_bundle. -needs_extra_options = ["safety_relevant", "root_cause"] diff --git a/docs/extensions/fmea_xml_table.py b/docs/extensions/fmea_xml_table.py deleted file mode 100644 index 8295a6c7201..00000000000 --- a/docs/extensions/fmea_xml_table.py +++ /dev/null @@ -1,247 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2024 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -""" -Sphinx extension that provides the ``fmea_xml_table`` directive. - -The directive reads a FMEA XML file and: - 1. Registers each entry as a hidden ``feat_saf_fmea`` need in sphinx-needs. - 2. Renders all entries as a table with the columns defined in FMEA_COLUMNS. - -Usage in RST:: - - .. fmea_xml_table:: path/to/fmea.xml - -The path is resolved relative to the document that contains the directive. -""" - -import os -import re -import xml.etree.ElementTree as ET -from collections.abc import Sequence - -from docutils import nodes -from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import make_refnode -from sphinx_needs.api import add_need -from sphinx_needs.data import SphinxNeedsData - -FMEA_COLUMNS = [ - "id", - "violates", - "fault_id", - "failure_effect", - "mitigated_by", - "sufficient", - "status", - "safety_relevant", - "root_cause", - "content", -] - -# Fields passed to add_need (must match metamodel's feat_saf_fmea definition). -# safety_relevant and root_cause are now in the metamodel. -_NEED_EXTRA_KWARGS = { - "fault_id", - "failure_effect", - "sufficient", - "safety_relevant", - "root_cause", -} -_NEED_LINK_KWARGS = {"violates", "mitigated_by"} - -# fault_id values (e.g. "MF_01_01") map to needs in the process_description build -# using the prefix "fmea_fault_model__" + lower-case value as the anchor. -_FAULT_ID_NEED_PREFIX = "fmea_fault_model__" - - -class FmeaNeedRef(nodes.Inline, nodes.Element): - """Placeholder node for need references rendered by fmea_xml_table.""" - - -class FmeaXmlTable(SphinxDirective): - """Import FMEA data from an XML file and render it as a table.""" - - has_content = False - required_arguments = 1 - optional_arguments = 0 - final_argument_whitespace = False - option_spec = {} - - def run(self) -> Sequence[nodes.Node]: - xml_rel_path = self.arguments[0] - xml_abs_path = self.env.relfn2path(xml_rel_path, self.env.docname)[1] - - if not os.path.exists(xml_abs_path): - msg = f"fmea_xml_table: file not found: {xml_abs_path}" - return [nodes.system_message(msg, level=3, type="ERROR", line=self.lineno)] - - tree = ET.parse(xml_abs_path) - root = tree.getroot() - - entries: list[dict[str, str]] = [] - result_nodes: list[nodes.Node] = [] - - for entry_elem in root.findall("entry"): - row = { - field: (entry_elem.findtext(field) or "").strip() - for field in FMEA_COLUMNS - } - entries.append(row) - - violates_str = row["violates"] - mitigated_str = row["mitigated_by"] - - add_need( - app=self.env.app, - state=self.state, - docname=self.env.docname, - lineno=self.lineno, - need_type="feat_saf_fmea", - title=row["id"], - id=row["id"], - content=row["content"], - status=row["status"], - hide=True, - fault_id=row["fault_id"], - failure_effect=row["failure_effect"], - sufficient=row["sufficient"], - violates=violates_str, - mitigated_by=mitigated_str, - ) - # Provide an anchor so cross-document links (e.g. from requirements - # pages) can resolve to this ID without rendering a visible need box. - result_nodes.append(nodes.target("", "", ids=[row["id"]], refid=row["id"])) - - result_nodes.append(_build_table(entries, self)) - return result_nodes - - -def _append_fault_id_link( - paragraph: nodes.paragraph, value: str, base_url: str -) -> None: - """Render a fault_id value as a link to the fault models guideline page.""" - full_need_id = f"{_FAULT_ID_NEED_PREFIX}{value.lower()}" - ref = nodes.reference(value, value, refuri=f"{base_url}#{full_need_id}") - paragraph += ref - - -def _append_need_links( - paragraph: nodes.paragraph, value: str, directive: SphinxDirective -) -> None: - """Render a need-link field value as clickable references.""" - need_ids = [token.strip() for token in re.split(r"[,|]", value) if token.strip()] - if not need_ids: - paragraph += nodes.Text(value) - return - - for index, need_id in enumerate(need_ids): - if index: - paragraph += nodes.Text(", ") - - need_ref = FmeaNeedRef("", reftarget=need_id) - need_ref += nodes.Text(need_id) - paragraph += need_ref - - -def _resolve_fmea_need_refs(app, doctree: nodes.document, fromdocname: str) -> None: - """Replace placeholder FMEA need refs with final links once all needs are known.""" - needs_view = SphinxNeedsData(app.env).get_needs_view() - - for node in doctree.findall(FmeaNeedRef): - need_id = str(node.get("reftarget", "")) - target_need = needs_view.get(need_id) - - if ( - target_need - and target_need.get("docname") - and not target_need.get("is_external") - ): - node.replace_self( - make_refnode( - app.builder, - fromdocname, - target_need["docname"], - need_id, - nodes.Text(need_id), - need_id, - ) - ) - continue - - external_url = target_need.get("external_url") if target_need else None - if external_url: - ref = nodes.reference(need_id, need_id, refuri=external_url) - external_css = target_need.get("external_css") if target_need else None - if external_css: - ref["classes"].append(external_css) - node.replace_self(ref) - continue - - node.replace_self(nodes.Text(need_id)) - - -def _build_table( - entries: list[dict[str, str]], directive: SphinxDirective -) -> nodes.table: - table = nodes.table() - tgroup = nodes.tgroup(cols=len(FMEA_COLUMNS)) - table += tgroup - - for _ in FMEA_COLUMNS: - tgroup += nodes.colspec(colwidth=1) - - # Header - thead = nodes.thead() - tgroup += thead - hrow = nodes.row() - thead += hrow - for col in FMEA_COLUMNS: - cell = nodes.entry() - cell += nodes.paragraph(text=col) - hrow += cell - - # Body - tbody = nodes.tbody() - tgroup += tbody - for row in entries: - trow = nodes.row() - tbody += trow - for col in FMEA_COLUMNS: - cell = nodes.entry() - paragraph = nodes.paragraph() - value = row.get(col, "") - if col in _NEED_LINK_KWARGS and value: - _append_need_links(paragraph, value, directive) - elif col == "fault_id" and value: - base_url = directive.env.app.config.fmea_fault_model_base_url - if base_url: - _append_fault_id_link(paragraph, value, base_url) - else: - paragraph += nodes.Text(value) - else: - paragraph += nodes.Text(value) - cell += paragraph - trow += cell - - return table - - -def setup(app): - app.add_config_value("fmea_fault_model_base_url", "", "env") - app.add_directive("fmea_xml_table", FmeaXmlTable) - app.connect("doctree-resolved", _resolve_fmea_need_refs) - return { - "version": "0.1", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/features/persistency/safety_analysis/fmea.json b/docs/features/persistency/safety_analysis/fmea.json new file mode 100644 index 00000000000..a577fff1042 --- /dev/null +++ b/docs/features/persistency/safety_analysis/fmea.json @@ -0,0 +1,124 @@ +{ + "current_version": "0.1", + "versions": { + "0.1": { + "needs": { + "feat_saf_fmea__persistency__message_nreived": { + "id": "feat_saf_fmea__persistency__message_nreived", + "type": "feat_saf_fmea", + "title": "feat_saf_fmea__persistency__message_nreived", + "content": "User is not able to use the feature. Middleware cant be used. User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself.\nFailure handling is addressed to the application by the aou_req__persistency__error_handling.", + "status": "valid", + "tags": [], + "fault_id": "MF_01_01", + "failure_effect": "Message is not received so the feature persistency is not available.", + "sufficient": "yes", + "violates": [ + "feat_arc_dyn__persistency__check_key_default", + "feat_arc_dyn__persistency__delete_key", + "feat_arc_dyn__persistency__flush", + "feat_arc_dyn__persistency__read_key", + "feat_arc_dyn__persistency__read_from_storage", + "feat_arc_dyn__persistency__write_key", + "feat_arc_dyn__persistency__snapshot_restore" + ], + "mitigated_by": [ + "aou_req__persistency__error_handling" + ] + }, + "feat_saf_fmea__persistency__late_message": { + "id": "feat_saf_fmea__persistency__late_message", + "type": "feat_saf_fmea", + "title": "feat_saf_fmea__persistency__late_message", + "content": "Subset of MF_01_01 if the delay is to long.", + "status": "valid", + "tags": [], + "fault_id": "MF_01_02", + "failure_effect": "message received too late.", + "sufficient": "yes", + "violates": [ + "feat_arc_dyn__persistency__check_key_default", + "feat_arc_dyn__persistency__delete_key", + "feat_arc_dyn__persistency__flush", + "feat_arc_dyn__persistency__read_key", + "feat_arc_dyn__persistency__read_from_storage", + "feat_arc_dyn__persistency__write_key", + "feat_arc_dyn__persistency__snapshot_restore" + ], + "mitigated_by": [ + "aou_req__persistency__error_handling" + ] + }, + "feat_saf_fmea__persistency__corrupted_message": { + "id": "feat_saf_fmea__persistency__corrupted_message", + "type": "feat_saf_fmea", + "title": "feat_saf_fmea__persistency__corrupted_message", + "content": "Covered by MF_01_01", + "status": "valid", + "tags": [], + "fault_id": "MF_01_05", + "failure_effect": "message is corrupted so the feature persistency is not available.", + "sufficient": "yes", + "violates": [ + "feat_arc_dyn__persistency__check_key_default", + "feat_arc_dyn__persistency__delete_key", + "feat_arc_dyn__persistency__flush", + "feat_arc_dyn__persistency__read_key", + "feat_arc_dyn__persistency__read_from_storage", + "feat_arc_dyn__persistency__write_key", + "feat_arc_dyn__persistency__snapshot_restore" + ], + "mitigated_by": [ + "aou_req__persistency__error_handling" + ] + }, + "feat_saf_fmea__persistency__not_sent": { + "id": "feat_saf_fmea__persistency__not_sent", + "type": "feat_saf_fmea", + "title": "feat_saf_fmea__persistency__not_sent", + "content": "Covered by MF_01_01 because the violation cause is the same.", + "status": "valid", + "tags": [], + "fault_id": "MF_01_06", + "failure_effect": "message is not sent so the feature persistency is not available.", + "sufficient": "yes", + "violates": [ + "feat_arc_dyn__persistency__check_key_default", + "feat_arc_dyn__persistency__delete_key", + "feat_arc_dyn__persistency__flush", + "feat_arc_dyn__persistency__read_key", + "feat_arc_dyn__persistency__read_from_storage", + "feat_arc_dyn__persistency__write_key", + "feat_arc_dyn__persistency__snapshot_restore" + ], + "mitigated_by": [ + "aou_req__persistency__error_handling" + ] + }, + "feat_saf_fmea__persistency__err_handl": { + "id": "feat_saf_fmea__persistency__err_handl", + "type": "feat_saf_fmea", + "title": "feat_saf_fmea__persistency__err_handl", + "content": "User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself.\nFailure handling is addressed to the application by the aou_req__persistency__error_handling.", + "status": "valid", + "tags": [], + "fault_id": "EX_01_04", + "failure_effect": "loss of execution will lead to an unavailability of the persistency feature.", + "sufficient": "yes", + "violates": [ + "feat_arc_dyn__persistency__check_key_default", + "feat_arc_dyn__persistency__delete_key", + "feat_arc_dyn__persistency__flush", + "feat_arc_dyn__persistency__read_key", + "feat_arc_dyn__persistency__read_from_storage", + "feat_arc_dyn__persistency__write_key", + "feat_arc_dyn__persistency__snapshot_restore" + ], + "mitigated_by": [ + "aou_req__persistency__error_handling" + ] + } + } + } + } +} diff --git a/docs/features/persistency/safety_analysis/fmea.rst b/docs/features/persistency/safety_analysis/fmea.rst index f7f6547d004..4a1b434b8e9 100644 --- a/docs/features/persistency/safety_analysis/fmea.rst +++ b/docs/features/persistency/safety_analysis/fmea.rst @@ -43,4 +43,9 @@ Fault models Failure Mode List ----------------- -.. fmea_xml_table:: fmea.xml +.. needimport:: fmea.json + :hide: + +.. needtable:: + :types: feat_saf_fmea + :columns: id;violates;fault_id;failure_effect;mitigated_by;sufficient;status;content diff --git a/docs/features/persistency/safety_analysis/fmea.xml b/docs/features/persistency/safety_analysis/fmea.xml deleted file mode 100644 index 3cc62837ae1..00000000000 --- a/docs/features/persistency/safety_analysis/fmea.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - feat_saf_fmea__persistency__message_nreived - feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - MF_01_01 - Message is not received so the feature persistency is not available. - aou_req__persistency__error_handling - yes - valid - NA - NA - User is not able to use the feature. Middleware cant be used. User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. -Failure handling is addressed to the application by the aou_req__persistency__error_handling. - - - - feat_saf_fmea__persistency__late_message - feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - MF_01_02 - message received too late. - aou_req__persistency__error_handling - yes - valid - NA - NA - Subset of MF_01_01 if the delay is to long. - - - - feat_saf_fmea__persistency__corrupted_message - feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - MF_01_05 - message is corrupted so the feature persistency is not available. - aou_req__persistency__error_handling - yes - valid - NA - NA - Covered by MF_01_01 - - - - feat_saf_fmea__persistency__not_sent - feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - MF_01_06 - message is not sent so the feature persistency is not available. - aou_req__persistency__error_handling - yes - valid - NA - NA - Covered by MF_01_01 because the violation cause is the same. - - - - feat_saf_fmea__persistency__err_handl - feat_arc_dyn__persistency__check_key_default, feat_arc_dyn__persistency__delete_key, feat_arc_dyn__persistency__flush, feat_arc_dyn__persistency__read_key, feat_arc_dyn__persistency__read_from_storage, feat_arc_dyn__persistency__write_key, feat_arc_dyn__persistency__snapshot_restore - EX_01_04 - loss of execution will lead to an unavailability of the persistency feature. - aou_req__persistency__error_handling - yes - valid - NA - NA - User is not able to use the feature. Middleware cant be used. Loss of execution can only be caused by the application, not by the persistency feature itself. -Failure handling is addressed to the application by the aou_req__persistency__error_handling. - - \ No newline at end of file From d1a626c4b20e23413bb3a6a0441ae1fb7cc0c458 Mon Sep 17 00:00:00 2001 From: "Negm Adham (ETAS-ECM/ESY3)" Date: Wed, 29 Apr 2026 11:33:54 +0200 Subject: [PATCH 8/8] remove changes made for deleted extension --- docs/conf.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index c41134c2971..5690729f596 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,12 +13,6 @@ # Configuration file for the Sphinx documentation builder. -import os -import sys - -# Make local extensions importable -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "extensions")) - project = "S-CORE" project_url = "https://eclipse-score.github.io/score" version = "0.1"