diff --git a/src/BUILD b/src/BUILD index aa9885ef8..b7b216854 100644 --- a/src/BUILD +++ b/src/BUILD @@ -53,6 +53,7 @@ filegroup( "//src/extensions/score_mounts:all_sources", "//src/extensions/score_source_code_linker:all_sources", "//src/extensions/score_sphinx_bundle:all_sources", + "//src/extensions/score_sphinx_needs_templates:all_sources", "//src/extensions/score_sync_toml:all_sources", "//src/extensions/score_metrics:all_sources", "//src/helper_lib:all_sources", diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 33b088d52..bebb693b2 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -998,6 +998,11 @@ needs_types: - verification_report parts: 3 + auto_mod_ver_report: + title: Module Verification Report (automated) + parts: 2 + mandatory_links: + belongs_to: mod # https://eclipse-score.github.io/process_description/main/permalink.html?id=gd_temp__change_decision_record dec_rec: diff --git a/src/extensions/score_sphinx_bundle/BUILD b/src/extensions/score_sphinx_bundle/BUILD index 113803b97..3230c6124 100644 --- a/src/extensions/score_sphinx_bundle/BUILD +++ b/src/extensions/score_sphinx_bundle/BUILD @@ -22,14 +22,11 @@ filegroup( py_library( name = "score_sphinx_bundle", srcs = [":all_sources"], - # Keep the shared Sphinx-Needs templates beside the extension in the - # Bazel runfiles tree. The Python extension discovers their directory from - # its own __file__ instead of receiving a path from docs.bzl. - data = ["@score_docs_as_code//src/needs_templates:files"], visibility = ["//visibility:public"], deps = all_requirements + [ "@score_docs_as_code//src/extensions:score_plantuml", "@score_docs_as_code//src/extensions:broken_link_fix", + "@score_docs_as_code//src/extensions/score_sphinx_needs_templates", "@score_docs_as_code//src/extensions/score_draw_uml_funcs", "@score_docs_as_code//src/extensions/score_cross_module_compatibility", "@score_docs_as_code//src/extensions/score_layout", diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index f3399e507..523649be0 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -10,8 +10,6 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -from pathlib import Path - import matplotlib from sphinx.application import Sphinx @@ -24,6 +22,7 @@ "sphinxcontrib.plantuml", "score_plantuml", "sphinx_needs", + "score_sphinx_needs_templates", "score_cross_module_compatibility", "score_metamodel", "sphinx_design", @@ -45,38 +44,12 @@ ] -def _needs_template_folder() -> Path: - """Return the shared Sphinx-Needs template directory. - - The extension and the templates are both part of the main ``src`` tree. - Deriving the path from ``__file__`` works for the workspace, Bazel - runfiles, and the sandbox because the extension's data files preserve that - source-tree layout. - """ - # Keep the runfiles/sandbox prefix intact; only walk from the extension's - # package directory to the sibling ``needs_templates`` directory. - # Basically: src/extensions/score_sphinx_bundle/../../needs_templates. - template_folder = Path(__file__).parents[2] / "needs_templates" - if not template_folder.is_dir(): - raise FileNotFoundError( - f"Sphinx-Needs template folder does not exist: {template_folder}" - ) - return template_folder - - def setup(app: Sphinx) -> dict[str, object]: matplotlib.rcParamsDefault["savefig.bbox"] = "tight" config_setdefault(app.config, "html_copy_source", False) config_setdefault(app.config, "html_show_sourcelink", False) - # The templates are a data dependency of this extension. Locate the - # shared directory from the extension itself instead of passing a Bazel - # label or a list of generated paths through every docs target. - config_setdefault( - app.config, "needs_template_folder", str(_needs_template_folder()) - ) - # Global settings # Note: the "sub-extensions" also set their own config values diff --git a/src/extensions/score_sphinx_needs_templates/BUILD b/src/extensions/score_sphinx_needs_templates/BUILD new file mode 100644 index 000000000..a57da451c --- /dev/null +++ b/src/extensions/score_sphinx_needs_templates/BUILD @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@aspect_rules_py//py:defs.bzl", "py_library") +load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements") + +filegroup( + name = "all_sources", + srcs = ["__init__.py"], + visibility = ["//visibility:public"], +) + +py_library( + name = "score_sphinx_needs_templates", + srcs = [":all_sources"], + data = ["@score_docs_as_code//src/needs_templates:files"], + visibility = ["//visibility:public"], + deps = all_requirements + [ + "@score_docs_as_code//src/helper_lib", + ], +) diff --git a/src/extensions/score_sphinx_needs_templates/README.md b/src/extensions/score_sphinx_needs_templates/README.md new file mode 100644 index 000000000..6abf076ed --- /dev/null +++ b/src/extensions/score_sphinx_needs_templates/README.md @@ -0,0 +1,48 @@ + + +# `score_sphinx_needs_templates` + +This extension contains the runtime support for the repository's Sphinx-Needs +`.need` templates. It is loaded by `score_sphinx_bundle` immediately after +`sphinx_needs`. + +## Features + +The extension provides: + +* the shared `src/needs_templates` directory as the Sphinx-Needs template + directory; +* the `linked_needs(need_id, link_name)` helper for traversing Need links; +* support for templates whose output depends on other Needs in the model; +* local page navigation for headings generated by marked templates. + +## Using `linked_needs` + +The helper returns the linked `NeedItem` objects in the order declared by the +source Need. This allows a template to derive its sections from the Need graph +instead of embedding Need IDs. + +For example: + +```jinja +{# score: render-after-needs-collection #} +{% set components = linked_needs(module_id, "includes") %} +{% for component in components %} +{{ component["title"] }} +{% endfor %} +``` + +Templates that follow links across the Need model should include the +`score: render-after-needs-collection` marker in a Jinja comment. Need fields and filters +continue to expose the corresponding backlink fields with the `_back` suffix. diff --git a/src/extensions/score_sphinx_needs_templates/__init__.py b/src/extensions/score_sphinx_needs_templates/__init__.py new file mode 100644 index 000000000..a9a1f4dec --- /dev/null +++ b/src/extensions/score_sphinx_needs_templates/__init__.py @@ -0,0 +1,248 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +from pathlib import Path + +from docutils import nodes +from sphinx import addnodes +from sphinx.application import Sphinx +from sphinx.environment import BuildEnvironment +from sphinx_needs.data import SphinxNeedsData +from sphinx_needs.need_item import NeedItem +from sphinx_needs.nodes import Need + +from src.helper_lib import config_setdefault + +_template_environment: BuildEnvironment | None = None +# Templates containing this marker need a second read after parallel Need +# collection has been merged. +_RENDER_AFTER_NEEDS_COLLECTION_MARKER = "score: render-after-needs-collection" + + +def _needs_template_folder() -> Path: + """Locate the shared ``.need`` template directory for Sphinx-Needs.""" + template_folder = Path(__file__).parents[2] / "needs_templates" + if not template_folder.is_dir(): + raise FileNotFoundError( + f"Sphinx-Needs template folder does not exist: {template_folder}" + ) + return template_folder + + +class _LinkedNeeds: + """Provide link traversal to Need templates as a pickleable callable. + + Calling the object with a Need ID and a link field returns the target + ``NeedItem`` objects in the order declared by the source Need. This lets a + template derive sections from the Need graph instead of embedding IDs. + + The object is deliberately a top-level class instance because Sphinx puts + the render context into its parallel-reader configuration. A plain + function would make that configuration unpickleable. The build environment + is kept process-local and captured once Sphinx has created ``app.env``. + """ + + def __call__(self, need_id: str, link_name: str) -> list[NeedItem]: + if _template_environment is None: + return [] + + needs = SphinxNeedsData(_template_environment).get_needs_mutable() + source = needs.get(need_id) + if source is None: + return [] + + linked: list[NeedItem] = [] + for link in source.get_links(link_name, as_str=False): + target = needs.get(link.id) + if target is not None: + linked.append(target) + return linked + + +_linked_needs_callable = _LinkedNeeds() + + +def _complex_template_names(app: Sphinx) -> set[str]: + """Return template names opting into the post-merge rendering pass.""" + template_folder = Path(str(app.config.needs_template_folder)) + return { + template.stem + for template in template_folder.glob("*.need") + if ( + _RENDER_AFTER_NEEDS_COLLECTION_MARKER + in template.read_text(encoding="utf-8") + ) + } + + +def _rerender_pages_with_complex_templates( + app: Sphinx, env: BuildEnvironment +) -> list[str]: + """Re-read marked pages after the parallel Need environments are merged. + + Need templates are expanded while source documents are read. A parallel + worker cannot see Needs collected by other workers at that point. The + marked pages are therefore purged and read once more from the main + environment before Sphinx-Needs post-processing begins. + """ + if app.builder.name != "html": + return [] + + complex_templates = _complex_template_names(app) + if not complex_templates: + return [] + + needs_data = SphinxNeedsData(env) + if needs_data.needs_is_post_processed: + return [] + + complex_template_docs: set[str] = set() + for need in needs_data.get_needs_mutable().values(): + if need["template"] not in complex_templates: + continue + docname = need["docname"] + if isinstance(docname, str) and docname: + complex_template_docs.add(docname) + + pages_to_rerender = sorted(complex_template_docs) + for docname in pages_to_rerender: + app.events.emit("env-purge-doc", env, docname) + env.clear_doc(docname) + app.builder.read_doc(docname) + + return pages_to_rerender + + +def _template_rubrics( + app: Sphinx, doctree: nodes.document, complex_templates: set[str] +) -> list[nodes.Element]: + """Return headings rendered inside marked Need templates. + + Sphinx-Needs parses Need content with ``match_titles=False``. Consequently, + a normal section title inside a template cannot participate in Sphinx's + document ToC. The templates use rubrics for these headings, so those nodes + are the reliable, already-rendered representation to expose to the ToC. + """ + needs = SphinxNeedsData(app.env).get_needs_mutable() + rubrics: list[nodes.Element] = [] + + for need_node in doctree.findall(Need): + need_id = need_node.get("refid") + if not isinstance(need_id, str): + continue + + need = needs.get(need_id) + if need is None or need["template"] not in complex_templates: + continue + + # Only direct rubrics are report headings. Rubrics nested in dropdowns + # are control labels and should not become navigation entries. + rubrics.extend( + child for child in need_node.children if isinstance(child, nodes.rubric) + ) + + return rubrics + + +def _add_template_rubrics_to_toc(app: Sphinx, doctree: nodes.document) -> None: + """Expose headings inside marked Need templates in the local page ToC. + + The regular Sphinx ToC collector deliberately ignores sections nested in + arbitrary container nodes such as a Sphinx-Needs Need. This handler runs + after that collector, assigns IDs directly to the rendered rubrics, and + adds matching local references to its already-built ToC. It therefore + changes navigation only; the generated report content stays inside the + Need node and does not have to be moved or rendered twice. + """ + if app.builder.name != "html": + return + + complex_templates = _complex_template_names(app) + if not complex_templates: + return + + rubrics = _template_rubrics(app, doctree, complex_templates) + if not rubrics: + return + + docname = app.env.current_document.docname + toc = app.env.tocs.get(docname) + if toc is None: + return + + used_ids = { + node_id + for element in doctree.findall(nodes.Element) + for node_id in element.get("ids", []) + } + + for rubric in rubrics: + rubric_ids = rubric.get("ids", []) + if rubric_ids: + rubric_id = rubric_ids[0] + else: + rubric_id = nodes.make_id(f"{docname}-{rubric.astext()}") + if not rubric_id: + rubric_id = f"{docname}-template-heading" + + candidate = rubric_id + suffix = 2 + while candidate in used_ids: + candidate = f"{rubric_id}-{suffix}" + suffix += 1 + rubric_id = candidate + rubric["ids"] = [rubric_id] + used_ids.add(rubric_id) + + reference = nodes.reference( + "", + "", + *(child.deepcopy() for child in rubric.children), + internal=True, + refuri=docname, + anchorname=f"#{rubric_id}", + ) + toc += nodes.list_item("", addnodes.compact_paragraph("", "", reference)) + + app.env.toc_num_entries[docname] += len(rubrics) + + +def _capture_template_environment(app: Sphinx) -> None: + """Give the link helper the environment in which it should resolve Needs. + + The helper is registered during ``setup()``, but Sphinx creates ``app.env`` + only after extension setup has completed. ``builder-inited`` is the first + lifecycle event at which the final build environment is available. + """ + global _template_environment + _template_environment = app.env + + +def setup(app: Sphinx) -> dict[str, object]: + """Install Sphinx-Needs template helpers and the marked-page second pass.""" + app.setup_extension("sphinx_needs") + + config_setdefault( + app.config, "needs_template_folder", str(_needs_template_folder()) + ) + app.config.needs_render_context.setdefault("linked_needs", _linked_needs_callable) + app.connect("builder-inited", _capture_template_environment) + # The priority is intentionally after Sphinx's environment collectors + # (default 500), so the handler can augment the collector-owned ToC. + app.connect("doctree-read", _add_template_rubrics_to_toc, priority=600) + app.connect("env-updated", _rerender_pages_with_complex_templates) + + return { + "version": "1.0.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/src/needs_templates/mod_ver_report_tiny.need b/src/needs_templates/mod_ver_report_tiny.need new file mode 100644 index 000000000..39d774a59 --- /dev/null +++ b/src/needs_templates/mod_ver_report_tiny.need @@ -0,0 +1,361 @@ +{# + score: render-after-needs-collection + + The report follows the content and order of the standard module verification + report used by the baselibs PR report: + + * feature summary and feature-level verification statistics; + * component overview; + * component-level verification, traceability, coverage, and work products. + + A need template receives the current need's fields only. The + ``linked_needs`` render helper follows the module's outgoing links so that + the component sections stay driven by the Need model. +#} +{% set module_id = belongs_to|first|default("") %} +{% set module_short = module_id|replace("mod__", "") %} +{% set module_slug = module_short|replace("_", "")|lower %} +{% set feature_id = module_id|replace("mod__", "feat__") %} + +.. _{{ id }}: + +.. raw:: html + + + +This document provides a verification report of the Baselibs feature and its +associated components. + +.. rubric:: Feature + +.. needtable:: + :filter: id == "{{ feature_id }}" + :columns: title as "Name";id as "Id";safety;security;status + :style: table + +.. rubric:: Feature Requirements Statistics + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: Feature Requirements Status + :labels: valid, invalid + :colors: #37a12d, #ca2828 + :legend: + + type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid" + type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "invalid" + + .. grid-item:: + + .. needpie:: Feature Requirements Test Coverage + :labels: fully covered, partially covered, not covered + :colors: #37a12d, #f0a500, #ca2828 + :legend: + + type == "feat_req" and "{{ feature_id }}" in satisfied_by and len(fully_verifies_back) > 0 + type == "feat_req" and "{{ feature_id }}" in satisfied_by and len(partially_verifies_back) > 0 and len(fully_verifies_back) == 0 + type == "feat_req" and "{{ feature_id }}" in satisfied_by and len(fully_verifies_back) == 0 and len(partially_verifies_back) == 0 + +.. dropdown:: Show requirements table + :animate: fade-in + + .. needtable:: + :filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by + :style: table + :columns: id;title;safety;status;fully_verifies_back;partially_verifies_back + :colwidths: 13,22,8,10,23,24 + :sort: id + +.. rubric:: Feature Architecture Statistics + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: Feature Architecture Elements Status + :labels: valid, invalid + :colors: #37a12d, #ca2828 + :legend: + + type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "valid" + type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "invalid" + + .. grid-item:: + + .. needpie:: Feature Architecture Elements Inspection Status + :labels: inspected, not inspected + :colors: #37a12d, #ca2828 + :legend: + + type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and "inspected" in tags + type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and "inspected" not in tags + +.. dropdown:: Show architectural elements table + :animate: fade-in + + .. needtable:: + :filter: type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to + :style: table + :columns: id;title;safety;status;tags + :colwidths: 25,30,10,15,20 + :sort: id + +.. rubric:: Feature Inspection Statistics + +Presence of the feature-level inspection work products. + +.. dropdown:: Show work products table + :animate: fade-in + + .. list-table:: + :header-rows: 1 + :widths: 30 25 25 20 + :class: wp-doc-table + + * - Work Product + - Kind + - Realized by + - Status + * - :need:`wp__requirements_inspect` + - Requirements Inspection + - .. needtable:: + :filter: type == "document" and "{{ module_slug }}" in id.replace("_", "").lower() and "wp__requirements_inspect" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ module_slug }}" in id.replace("_", "").lower() and "wp__requirements_inspect" in realizes + :columns: status + :style: table + * - :need:`wp__sw_arch_verification` + - Architecture Inspection + - .. needtable:: + :filter: type == "document" and "{{ module_slug }}" in id.replace("_", "").lower() and "wp__sw_arch_verification" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ module_slug }}" in id.replace("_", "").lower() and "wp__sw_arch_verification" in realizes + :columns: status + :style: table + +{% set report_components = linked_needs(module_id, "includes") %} + +.. rubric:: Components + +.. rubric:: Component Overview + +.. needtable:: + :filter: type == "comp" and "{{ module_id }}" in includes_back + :columns: id as "Component";safety;security;status + :style: table + :sort: id + +{% macro render_component(component_id, component_title, component_slug) %} +.. raw:: html + + + +.. rubric:: {{ component_title }} + +.. raw:: html + +
+ +.. rubric:: Component Requirements Statistics + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: {{ component_title }} Requirements Status + :labels: valid, invalid + :colors: #37a12d, #ca2828 + :legend: + + type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "valid" + type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "invalid" + + .. grid-item:: + + .. needpie:: {{ component_title }} Requirements Test Coverage + :labels: fully covered, partially covered, not covered + :colors: #37a12d, #f0a500, #ca2828 + :legend: + + type == "comp_req" and "{{ component_id }}" in satisfied_by and len(fully_verifies_back) > 0 + type == "comp_req" and "{{ component_id }}" in satisfied_by and len(partially_verifies_back) > 0 and len(fully_verifies_back) == 0 + type == "comp_req" and "{{ component_id }}" in satisfied_by and len(fully_verifies_back) == 0 and len(partially_verifies_back) == 0 + +.. rubric:: Component Architecture Statistics + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: {{ component_title }} Architecture Elements Status + :labels: valid, invalid + :colors: #37a12d, #ca2828 + :legend: + + type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "valid" + type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "invalid" + + .. grid-item:: + + .. needpie:: {{ component_title }} Architecture Elements Inspection Status + :labels: inspected, not inspected + :colors: #37a12d, #ca2828 + :legend: + + type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and "inspected" in tags + type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and "inspected" not in tags + +.. rubric:: Requirements Traceability + +The following table lists all requirements of this component together with +their verification status and the tests that (fully or partially) verify them: + +.. dropdown:: Show requirements table + :animate: fade-in + + .. needtable:: + :filter: type == "comp_req" and "{{ component_id }}" in satisfied_by + :style: table + :columns: id;title;safety;status;fully_verifies_back;partially_verifies_back + :colwidths: 13,22,8,10,23,24 + :sort: id + +.. rubric:: Test Coverage + +Per-source-file line and branch coverage aggregated from the LCOV report +produced by ``bazel coverage``. + +.. dropdown:: Show test coverage table + :animate: fade-in + + .. note:: + + No coverage data available for this component. Run ``bazel coverage`` + with the corresponding targets and rebuild the docs to populate this + table. + +.. rubric:: Architectural Elements + +The following table lists the architectural elements of this component +together with their inspection status. Elements that have been formally +inspected carry the ``inspected`` tag; elements without that tag have not yet +been inspected. + +.. dropdown:: Show architectural elements table + :animate: fade-in + + .. needtable:: + :filter: type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to + :style: table + :columns: id;title;safety;status;tags + :colwidths: 25,30,10,15,20 + :sort: id + +.. rubric:: Verification & Safety Analysis Documents + +Presence of the standard verification and safety analysis work products for +this component. A dash (``—``) means the corresponding document is missing. + +.. dropdown:: Show work products table + :animate: fade-in + + .. list-table:: + :header-rows: 1 + :widths: 30 25 25 20 + :class: wp-doc-table + + * - Work Product + - Kind + - Realized by + - Status + * - :need:`wp__requirements_inspect` + - Requirements Inspection + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__requirements_inspect" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__requirements_inspect" in realizes + :columns: status + :style: table + * - :need:`wp__sw_arch_verification` + - Architecture Inspection + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_arch_verification" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_arch_verification" in realizes + :columns: status + :style: table + * - :need:`wp__sw_implementation_inspection` + - Implementation Inspection + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_implementation_inspection" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_implementation_inspection" in realizes + :columns: status + :style: table + * - :need:`wp__sw_component_dfa` + - DFA + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_component_dfa" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_component_dfa" in realizes + :columns: status + :style: table + * - :need:`wp__sw_component_fmea` + - FMEA + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_component_fmea" in realizes + :columns: id + :style: table + - .. needtable:: + :filter: type == "document" and "{{ component_slug|replace("_", "")|lower }}" in id.replace("_", "").lower() and "wp__sw_component_fmea" in realizes + :columns: status + :style: table +{% endmacro %} + +{% for component in report_components %} +{{ render_component( + component["id"], + component["title"], + component["id"]|replace("comp__" ~ module_short ~ "_", "") +) }} +{% endfor %}