diff --git a/src/extensions/score_sphinx_needs_templates/README.md b/src/extensions/score_sphinx_needs_templates/README.md
index fa76b5a7a..31d0e8111 100644
--- a/src/extensions/score_sphinx_needs_templates/README.md
+++ b/src/extensions/score_sphinx_needs_templates/README.md
@@ -25,6 +25,8 @@ The extension provides:
directory;
* the `linked_needs(need_id, link_name)` helper for traversing Need links;
* the `needs_of_type(need_type)` helper for selecting Needs by type;
+* the `id_filter_list`, `workproduct_rows` and `safety_pie_row` helpers, which
+ render RST fragments shared by several report templates;
* support for graph-driven `post_template`s that are rendered after parallel
Need collection has been merged;
* ordinary Sphinx page navigation for sections generated by those
diff --git a/src/extensions/score_sphinx_needs_templates/__init__.py b/src/extensions/score_sphinx_needs_templates/__init__.py
index 806adb609..d7f3f44d8 100644
--- a/src/extensions/score_sphinx_needs_templates/__init__.py
+++ b/src/extensions/score_sphinx_needs_templates/__init__.py
@@ -290,6 +290,178 @@ def __call__(self, ids: list[str], referenced: list[str]) -> dict[str, list[str]
_split_by_reference_callable = _SplitByReference()
+class _IdFilterList:
+ """Render Need IDs as the quoted ``[...]`` literal of an ``id in`` filter."""
+
+ def __call__(self, ids: list[str]) -> str:
+ return "[" + ", ".join(f'"{need_id}"' for need_id in ids) + "]"
+
+
+_id_filter_list_callable = _IdFilterList()
+
+
+class _WorkproductRows:
+ """Render work-product rows for a six-space indented ``list-table``.
+
+ Each row links the work product, its kind, the realising document and its
+ status. Both document cells are needtables over the same filter, differing
+ only in ``:columns:``, so a missing document renders as an empty cell.
+ """
+
+ def __call__(self, slug_norm: str, workproducts: list[list[str]]) -> str:
+ rows: list[str] = []
+ for wp_id, kind in workproducts:
+ doc_filter = (
+ f'type == "document" and "{slug_norm}" in id.replace("_", "").lower()'
+ f' and "{wp_id}" in realizes'
+ )
+ rows.append(
+ "\n".join(
+ [
+ f" * - :need:`{wp_id}`",
+ f" - {kind}",
+ " - .. needtable::",
+ f" :filter: {doc_filter}",
+ " :columns: id",
+ " :style: table",
+ " - .. needtable::",
+ f" :filter: {doc_filter}",
+ " :columns: status",
+ " :style: table",
+ ]
+ )
+ )
+ return "".join(f"\n{row}" for row in rows) + "\n"
+
+
+_workproduct_rows_callable = _WorkproductRows()
+
+
+class _SafetyPieRow:
+ """Render one row of pies restricted to a single safety class.
+
+ Shared by the module and platform verification reports, so that QM and
+ ASIL figures are never mixed in one chart. QM Needs are not formally
+ inspected, so there ``valid`` already is the green "done" slice rather
+ than the yellow "not inspected yet" one; a QM Need that is nevertheless
+ ``valid_inspected`` counts as ``valid``, as in the metamodel graph checks.
+
+ ``with_reference`` adds a third, reference-coverage pie; ``referenced`` and
+ ``missing`` must already be partitioned over this safety class alone. The
+ raster stays at three columns either way, so that a two-pie row renders
+ its charts at exactly the same size as a three-pie one.
+
+ ``count`` decides between pies and placeholders only; it is deliberately
+ not shown, because it is resolved through the Need graph while the pies
+ count through Sphinx-Needs filters, and the two can disagree on links
+ carrying a ``[version==N]`` selector.
+
+ This is a render helper instead of a Jinja macro because the MiniJinja
+ environment has no template loader, so macros cannot be shared between
+ templates.
+ """
+
+ def __call__(
+ self,
+ title: str,
+ subject: str,
+ base: str,
+ safety: str,
+ safety_label: str,
+ count: int,
+ with_reference: bool = False,
+ referenced: list[str] | None = None,
+ missing: list[str] | None = None,
+ ) -> str:
+ scope = f'{base} and safety == "{safety}"'
+ # (slice label, status condition) per pie slice; green "done" comes first.
+ statuses = (
+ [
+ ("valid", '(status == "valid" or status == "valid_inspected")'),
+ ("invalid", 'status == "invalid"'),
+ ]
+ if safety == "QM"
+ else [
+ (state, f'status == "{state}"')
+ for state in ("valid_inspected", "valid", "invalid")
+ ]
+ )
+ status_colors = (
+ "#37a12d, #ca2828" if safety == "QM" else "#37a12d, #f5c518, #ca2828"
+ )
+ # docutils normalises ``:class:`` values, so avoid underscores in them.
+ safety_css = safety.lower().replace("_", "-")
+ chart_prefix = f"{title} {subject}".strip()
+
+ lines = [
+ "",
+ f".. rubric:: {safety_label} — {subject}",
+ f" :class: safety-class-heading safety-{safety_css}",
+ "",
+ ".. grid:: 1 2 3 3",
+ " :gutter: 2",
+ " :class-row: score-pie-row",
+ ]
+
+ def grid_item(name: str, pie: list[str]) -> None:
+ lines.extend(["", " .. grid-item::", ""])
+ if count == 0:
+ lines.extend(
+ [
+ " .. raw:: html",
+ "",
+ '
') == 3
+ assert "no QM requirements" in row
+
+
+def test_safety_pie_row_reference_pie_only_on_request() -> None:
+ safety_pie_row = vars(templates)["_SafetyPieRow"]()
+
+ without = safety_pie_row("", "Stakeholder Requirements", "B", "QM", "QM", 2)
+ with_ref = safety_pie_row(
+ "", "Stakeholder Requirements", "B", "QM", "QM", 2, True, ["a", "b"], ["c"]
+ )
+
+ assert "Reference Coverage" not in without
+ assert without.count(".. needpie::") == 2
+ assert with_ref.count(".. needpie::") == 3
+ assert 'id in ["a", "b"]' in with_ref
+ assert 'id in ["c"]' in with_ref
+ # An empty title must not leave a leading blank in the chart title.
+ assert ".. needpie:: Stakeholder Requirements Status (QM)" in with_ref
+
+
+def test_id_filter_list_renders_quoted_literal() -> None:
+ id_filter_list = vars(templates)["_IdFilterList"]()
+
+ assert id_filter_list(["a", "b"]) == '["a", "b"]'
+ assert id_filter_list([]) == "[]"
+
+
+def test_workproduct_rows_filter_by_slug_and_work_product() -> None:
+ workproduct_rows = vars(templates)["_WorkproductRows"]()
+
+ rows = workproduct_rows("mycomp", [["wp__dfa", "DFA"], ["wp__fmea", "FMEA"]])
+
+ assert rows.count(" * - :need:`") == 2
+ assert " - DFA\n" in rows
+ assert rows.count('"mycomp" in id.replace("_", "").lower()') == 4
+ assert rows.count('and "wp__fmea" in realizes') == 2
+
+
def test_tool_qualification_matrix_groups_shared_requirements(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
diff --git a/src/needs_templates/module_verification_report.need b/src/needs_templates/module_verification_report.need
index 80377f442..6df98553a 100644
--- a/src/needs_templates/module_verification_report.need
+++ b/src/needs_templates/module_verification_report.need
@@ -53,30 +53,8 @@
["wp__sw_component_fmea", "FMEA"],
] %}
-{#- One work-product row: the need link, its kind, the realising document and
- its status. Both cells are needtables over the same filter, differing only
- in :columns:, so an empty match renders as an empty cell. -#}
-{% macro workproduct_rows(slug_norm, workproducts) %}
-{%- for wp in workproducts %}
- * - :need:`{{ wp[0] }}`
- - {{ wp[1] }}
- - .. needtable::
- :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes
- :columns: id
- :style: table
- - .. needtable::
- :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes
- :columns: status
- :style: table
-{%- endfor %}
-{% endmacro %}
-
-{#- Bracketed, quoted Sphinx-Needs ``id in [...]`` filter literal for a list
- of Need IDs, used to scope needpie/needtable filters below to the
- requirements that are in scope for ``applies_to_module_version``. -#}
-{% macro id_filter_list(ids) -%}
-[{% for req_id in ids %}"{{ req_id }}"{% if not loop.last %}, {% endif %}{% endfor %}]
-{%- endmacro %}
+{#- ``workproduct_rows``, ``id_filter_list`` and ``safety_pie_row`` are render
+ helpers shared with the platform report. -#}
.. raw:: html
@@ -102,8 +80,74 @@
.wp-doc-table td .dataTables_wrapper .dataTables_filter,
.wp-doc-table td .dataTables_wrapper .dataTables_info,
.wp-doc-table td .dataTables_wrapper .dataTables_paginate { display: none; }
+
+ /* Scales every chart to its column; together with the fixed three-column
+ raster this gives every chart the same size as in the platform report. */
+ .score-pie-row img[id^="needpie-"] {
+ width: 100%;
+ height: auto;
+ }
+ /* ``.sd-row`` is a flex container without a justification of its own, so
+ this centres the two pies in the three-column raster. */
+ .score-pie-row { justify-content: center; }
+
+ p.rubric.safety-class-heading {
+ display: inline-block; margin: 1.4em 0 0.5em 0;
+ padding: 0.15em 0.9em; border-radius: 1em;
+ font-size: 0.95em; font-weight: 600; color: #ffffff;
+ }
+ p.rubric.safety-class-heading.safety-asil-b { background: #1f4e79; }
+ p.rubric.safety-class-heading.safety-qm { background: #6c757d; }
+
+ /* Matches the aspect ratio of the rendered pie charts. */
+ .empty-pie {
+ display: flex; flex-direction: column; align-items: center;
+ justify-content: center; gap: 0.4em;
+ aspect-ratio: 16 / 9; padding: 0.5em; text-align: center;
+ border: 2px dashed #b0b0b0; border-radius: 6px; color: #6c757d;
+ }
+ .empty-pie-title { font-weight: 600; }
+ .empty-pie-text { font-style: italic; }
+
+ .chart-legend { display: flex; flex-wrap: wrap; gap: 1.2em 2.5em; }
+ .chart-legend-group {
+ display: flex; flex-direction: column; gap: 0.3em;
+ flex: 1 1 20em; font-size: 0.9em;
+ }
+ .chart-legend-head { font-weight: 600; }
+ .chart-legend-dot {
+ display: inline-block; width: 0.8em; height: 0.8em;
+ margin-right: 0.5em; border-radius: 50%;
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ }
+.. dropdown:: Legend — status values and chart colours
+ :animate: fade-in
+ :open:
+
+ .. raw:: html
+
+
+
+ Status — ASIL B
+ valid_inspected — agreed and formally inspected
+ valid — agreed, formal inspection still outstanding
+ invalid — not agreed; metamodel checks are skipped
+
+
+ Status — QM
+ valid — agreed; QM needs are not formally inspected, so this is their final state (valid_inspected counts here too)
+ invalid — not agreed; metamodel checks are skipped
+
+
+ Test coverage — both classes
+ fully covered — at least one test fully verifies the need
+ partially covered — only partially verifying tests
+ not covered — no verifying test at all
+
+
+
{# ===================================================================== #}
{# Components #}
{# ===================================================================== #}
@@ -118,7 +162,7 @@ Component Overview
:filter: id in [{% for component in components_in_mod %}"{{ component["id"] }}"{% if not loop.last %}, {% endif %}{% endfor %}]
:columns: id as "Component";safety;security;status
:style: table
- :sort: id
+ :sort: safety
{% for component in components_in_mod %}
{% set component_id = component["id"] %}
@@ -140,69 +184,35 @@ Component Overview
{% set component_reqs_all = linked_needs(component_id, "satisfied_by_back")
|selectattr("type", "eq", "comp_req")
|list %}
-{% set ns = namespace(req_ids=[]) %}
+{% set ns = namespace(reqs=[]) %}
{% for req in component_reqs_all %}
{% if req_in_report_version(req, module_version) %}
-{% set ns.req_ids = ns.req_ids + [req["id"]] %}
+{% set ns.reqs = ns.reqs + [req] %}
{% endif %}
{% endfor %}
-{% set component_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %}
+{% set component_reqs = ns.reqs %}
+{% set component_req_filter = 'id in ' ~ id_filter_list(component_reqs|map(attribute="id")|list) %}
+{#- ``type`` has no "in" test in minijinja, so the two architecture types are
+ collected separately and concatenated. -#}
+{% set component_arcs_all = linked_needs(component_id, "belongs_to_back") %}
+{% set component_arcs = (component_arcs_all|selectattr("type", "eq", "comp_arc_sta")|list)
+ + (component_arcs_all|selectattr("type", "eq", "comp_arc_dyn")|list) %}
Component Requirements Statistics
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. grid:: 1 2 2 2
- :gutter: 3
-
- .. grid-item::
-
- .. needpie:: {{ component_title }} Requirements Status
- :labels: valid, valid_inspected, invalid
- :colors: #f5c518, #37a12d, #ca2828
- :legend:
-
- type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "valid" and {{ component_req_filter }}
- type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "valid_inspected" and {{ component_req_filter }}
- type == "comp_req" and "{{ component_id }}" in satisfied_by and status == "invalid" and {{ component_req_filter }}
-
- .. 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 fully_verifies_back and {{ component_req_filter }}
- type == "comp_req" and "{{ component_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back and {{ component_req_filter }}
- type == "comp_req" and "{{ component_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back and {{ component_req_filter }}
+{% set req_base = 'type == "comp_req" and "' ~ component_id ~ '" in satisfied_by and ' ~ component_req_filter %}
+{{ safety_pie_row(component_title, "Requirements", req_base, "ASIL_B", "ASIL B",
+ component_reqs|selectattr("safety", "eq", "ASIL_B")|list|length) }}
+{{ safety_pie_row(component_title, "Requirements", req_base, "QM", "QM",
+ component_reqs|selectattr("safety", "eq", "QM")|list|length) }}
Component Architecture Statistics
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. grid:: 1 2 2 2
- :gutter: 3
-
- .. grid-item::
-
- .. needpie:: {{ component_title }} Architecture Elements Status
- :labels: valid, valid_inspected, invalid
- :colors: #f5c518, #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 == "valid_inspected"
- type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and status == "invalid"
-
- .. grid-item::
-
- .. needpie:: {{ component_title }} Architecture Elements Test Coverage
- :labels: fully covered, partially covered, not covered
- :colors: #37a12d, #f0a500, #ca2828
- :legend:
-
- type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and fully_verifies_back
- type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and partially_verifies_back and not fully_verifies_back
- type in ["comp_arc_sta", "comp_arc_dyn"] and "{{ component_id }}" in belongs_to and not fully_verifies_back and not partially_verifies_back
+{% set arc_base = 'type in ["comp_arc_sta", "comp_arc_dyn"] and "' ~ component_id ~ '" in belongs_to' %}
+{{ safety_pie_row(component_title, "Architecture Elements", arc_base, "ASIL_B", "ASIL B",
+ component_arcs|selectattr("safety", "eq", "ASIL_B")|list|length) }}
+{{ safety_pie_row(component_title, "Architecture Elements", arc_base, "QM", "QM",
+ component_arcs|selectattr("safety", "eq", "QM")|list|length) }}
Requirements Traceability
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -218,7 +228,7 @@ verification status and the tests that (fully or partially) verify them:
:style: table
:columns: id;title;safety;status;testlink
:colwidths: 13,22,8,10,47
- :sort: id
+ :sort: safety
Test Coverage
^^^^^^^^^^^^^
@@ -251,7 +261,7 @@ status have not yet been inspected.
:style: table
:columns: id;title;safety;status;tags
:colwidths: 25,30,10,15,20
- :sort: id
+ :sort: safety
Verification & Safety Analysis Documents
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need
index 9f6b30aad..62efcf1e8 100644
--- a/src/needs_templates/platform_verification_report.need
+++ b/src/needs_templates/platform_verification_report.need
@@ -37,40 +37,19 @@
["wp__feature_fmea", "FMEA"],
] %}
-{#- One work-product row: the need link, its kind, the realising document and
- its status. Both cells are needtables over the same filter, differing only
- in :columns:, so an empty match renders as an empty cell. -#}
-{% macro workproduct_rows(slug_norm, workproducts) %}
-{%- for wp in workproducts %}
- * - :need:`{{ wp[0] }}`
- - {{ wp[1] }}
- - .. needtable::
- :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes
- :columns: id
- :style: table
- - .. needtable::
- :filter: type == "document" and "{{ slug_norm }}" in id.replace("_", "").lower() and "{{ wp[0] }}" in realizes
- :columns: status
- :style: table
-{%- endfor %}
-{% endmacro %}
-
-{#- Bracketed, quoted Sphinx-Needs ``id in [...]`` filter literal for a list
- of Need IDs, used to scope needpie/needtable filters below to the
- requirements that are in scope for ``report_version``. -#}
-{% macro id_filter_list(ids) -%}
-[{% for req_id in ids %}"{{ req_id }}"{% if not loop.last %}, {% endif %}{% endfor %}]
-{%- endmacro %}
+{#- ``workproduct_rows``, ``id_filter_list`` and ``safety_pie_row`` are render
+ helpers shared with the module report. -#}
{#- Stakeholder requirements are not scoped to a Feature, so they are
collected once for the whole report rather than per-Feature like
``feat_req``. -#}
{% set ns_stkh_reqs = namespace(list=[]) %}
{% for req in needs_of_type("stkh_req")|unique(attribute="id")|list %}
{% if req_in_report_version(req, report_version) %}
-{% set ns_stkh_reqs.list = ns_stkh_reqs.list + [req["id"]] %}
+{% set ns_stkh_reqs.list = ns_stkh_reqs.list + [req] %}
{% endif %}
{% endfor %}
-{% set stkh_req_filter = 'id in ' ~ id_filter_list(ns_stkh_reqs.list) %}
+{% set stkh_req_ids = ns_stkh_reqs.list|map(attribute="id")|list %}
+{% set stkh_req_filter = 'id in ' ~ id_filter_list(stkh_req_ids) %}
.. raw:: html
+.. dropdown:: Legend — status values and chart colours
+ :animate: fade-in
+ :open:
+
+ .. raw:: html
+
+
+
+ Status — ASIL B
+ valid_inspected — agreed and formally inspected
+ valid — agreed, formal inspection still outstanding
+ invalid — not agreed; metamodel checks are skipped
+
+
+ Status — QM
+ valid — agreed; QM needs are not formally inspected, so this is their final state (valid_inspected counts here too)
+ invalid — not agreed; metamodel checks are skipped
+
+
+ Test coverage — both classes
+ fully covered — at least one test fully verifies the need
+ partially covered — only partially verifying tests
+ not covered — no verifying test at all
+
+
+ Reference coverage — both classes
+ referenced by lower — a requirement one level down derives from it
+ not referenced — nothing on the level below derives from it
+
+
+
{# ===================================================================== #}
{# Stakeholder Requirements #}
{# ===================================================================== #}
@@ -113,44 +157,17 @@ Stakeholder Requirements
one in-scope feature requirement is ``derived_from`` it. The index is built
and applied in Python, because every ``in`` test evaluated here would cross
the Rust/Python boundary once per comparison. -#}
-{% set ns_stkh_ref = split_by_reference(
- ns_stkh_reqs.list,
- referenced_ids("feat_req", "derived_from", report_version)) %}
-.. grid:: 1 2 2 2
- :gutter: 3
-
- .. grid-item::
-
- .. needpie:: Stakeholder Requirements Status
- :labels: valid, valid_inspected, invalid
- :colors: #f5c518, #37a12d, #ca2828
- :legend:
-
- type == "stkh_req" and status == "valid" and {{ stkh_req_filter }}
- type == "stkh_req" and status == "valid_inspected" and {{ stkh_req_filter }}
- type == "stkh_req" and status == "invalid" and {{ stkh_req_filter }}
-
- .. grid-item::
-
- .. needpie:: Stakeholder Requirements Test Coverage
- :labels: fully covered, partially covered, not covered
- :colors: #37a12d, #f0a500, #ca2828
- :legend:
-
- type == "stkh_req" and fully_verifies_back and {{ stkh_req_filter }}
- type == "stkh_req" and partially_verifies_back and not fully_verifies_back and {{ stkh_req_filter }}
- type == "stkh_req" and not fully_verifies_back and not partially_verifies_back and {{ stkh_req_filter }}
-
- .. grid-item::
- :class: score-centered-grid-item
-
- .. needpie:: Stakeholder Requirements Reference Coverage
- :labels: referenced by lower, not referenced
- :colors: #37a12d, #ca2828
- :legend:
-
- id in {{ id_filter_list(ns_stkh_ref.referenced) }}
- id in {{ id_filter_list(ns_stkh_ref.missing) }}
+{% set stkh_referenced_ids = referenced_ids("feat_req", "derived_from", report_version) %}
+{% set ns_stkh_ref = split_by_reference(stkh_req_ids, stkh_referenced_ids) %}
+{% set stkh_base = 'type == "stkh_req" and ' ~ stkh_req_filter %}
+{% set stkh_asil = ns_stkh_reqs.list|selectattr("safety", "eq", "ASIL_B")|list %}
+{% set stkh_qm = ns_stkh_reqs.list|selectattr("safety", "eq", "QM")|list %}
+{% set stkh_asil_ref = split_by_reference(stkh_asil|map(attribute="id")|list, stkh_referenced_ids) %}
+{% set stkh_qm_ref = split_by_reference(stkh_qm|map(attribute="id")|list, stkh_referenced_ids) %}
+{{ safety_pie_row("", "Stakeholder Requirements", stkh_base, "ASIL_B", "ASIL B",
+ stkh_asil|length, true, stkh_asil_ref.referenced, stkh_asil_ref.missing) }}
+{{ safety_pie_row("", "Stakeholder Requirements", stkh_base, "QM", "QM",
+ stkh_qm|length, true, stkh_qm_ref.referenced, stkh_qm_ref.missing) }}
.. dropdown:: Show requirements table
:animate: fade-in
@@ -160,9 +177,9 @@ Stakeholder Requirements
:style: table
:columns: id;title;safety;status;testlink
:colwidths: 13,22,8,10,47
- :sort: id
+ :sort: safety
{% if ns_stkh_ref.missing %}
-{#- Detail view of the "not referenced" slice of the reference coverage pie
+{#- Detail view of the "not referenced" slices of the reference coverage pies
above; both are rendered from the same in-scope ID list. -#}
.. dropdown:: Not referenced by feature requirements ({{ ns_stkh_ref.missing|length }})
@@ -174,7 +191,7 @@ Stakeholder Requirements
:style: table
:columns: id;title;safety;status
:colwidths: 15,45,15,25
- :sort: id
+ :sort: safety
{% endif %}
{# ===================================================================== #}
@@ -191,7 +208,7 @@ Feature Overview
:filter: id in [{% for feature in report_features %}"{{ feature["id"] }}"{% if not loop.last %}, {% endif %}{% endfor %}]
:columns: id as "Feature";safety;security;status
:style: table
- :sort: id
+ :sort: safety
{#- Reference coverage index, built once for all Features: a feature
requirement is "referenced" when at least one in-scope component
@@ -217,54 +234,34 @@ Feature Overview
{% set feature_reqs_all = linked_needs(feature_id, "satisfied_by_back")
|selectattr("type", "eq", "feat_req")
|list %}
-{% set ns = namespace(req_ids=[]) %}
+{% set ns = namespace(reqs=[]) %}
{% for req in feature_reqs_all %}
{% if req_in_report_version(req, report_version) %}
-{% set ns.req_ids = ns.req_ids + [req["id"]] %}
+{% set ns.reqs = ns.reqs + [req] %}
{% endif %}
{% endfor %}
-{% set feature_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %}
+{% set feature_reqs = ns.reqs %}
+{% set feature_req_ids = feature_reqs|map(attribute="id")|list %}
+{% set feature_req_filter = 'id in ' ~ id_filter_list(feature_req_ids) %}
{#- Reference coverage: partition this Feature's in-scope requirements against
the index built once above. -#}
-{% set ns_feat_ref = split_by_reference(ns.req_ids, comp_referenced_ids) %}
+{% set ns_feat_ref = split_by_reference(feature_req_ids, comp_referenced_ids) %}
+{#- ``type`` has no "in" test in minijinja, so the two architecture types are
+ collected separately and concatenated. -#}
+{% set feature_arcs_all = linked_needs(feature_id, "belongs_to_back") %}
+{% set feature_arcs = (feature_arcs_all|selectattr("type", "eq", "feat_arc_sta")|list)
+ + (feature_arcs_all|selectattr("type", "eq", "feat_arc_dyn")|list) %}
Requirements Statistics
^^^^^^^^^^^^^^^^^^^^^^^
-
-.. grid:: 1 2 2 2
- :gutter: 3
-
- .. grid-item::
-
- .. needpie:: {{ feature_title }} Requirements Status
- :labels: valid, valid_inspected, invalid
- :colors: #f5c518, #37a12d, #ca2828
- :legend:
-
- type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid" and {{ feature_req_filter }}
- type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "valid_inspected" and {{ feature_req_filter }}
- type == "feat_req" and "{{ feature_id }}" in satisfied_by and status == "invalid" and {{ feature_req_filter }}
-
- .. grid-item::
-
- .. needpie:: {{ feature_title }} 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 fully_verifies_back and {{ feature_req_filter }}
- type == "feat_req" and "{{ feature_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back and {{ feature_req_filter }}
- type == "feat_req" and "{{ feature_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back and {{ feature_req_filter }}
-
- .. grid-item::
- :class: score-centered-grid-item
-
- .. needpie:: {{ feature_title }} Requirements Reference Coverage
- :labels: referenced by lower, not referenced
- :colors: #37a12d, #ca2828
- :legend:
-
- id in {{ id_filter_list(ns_feat_ref.referenced) }}
- id in {{ id_filter_list(ns_feat_ref.missing) }}
+{% set req_base = 'type == "feat_req" and "' ~ feature_id ~ '" in satisfied_by and ' ~ feature_req_filter %}
+{% set feat_asil = feature_reqs|selectattr("safety", "eq", "ASIL_B")|list %}
+{% set feat_qm = feature_reqs|selectattr("safety", "eq", "QM")|list %}
+{% set feat_asil_ref = split_by_reference(feat_asil|map(attribute="id")|list, comp_referenced_ids) %}
+{% set feat_qm_ref = split_by_reference(feat_qm|map(attribute="id")|list, comp_referenced_ids) %}
+{{ safety_pie_row(feature_title, "Requirements", req_base, "ASIL_B", "ASIL B",
+ feat_asil|length, true, feat_asil_ref.referenced, feat_asil_ref.missing) }}
+{{ safety_pie_row(feature_title, "Requirements", req_base, "QM", "QM",
+ feat_qm|length, true, feat_qm_ref.referenced, feat_qm_ref.missing) }}
.. dropdown:: Show requirements table
:animate: fade-in
@@ -274,7 +271,7 @@ Requirements Statistics
:style: table
:columns: id;title;safety;status;testlink
:colwidths: 13,22,8,10,47
- :sort: id
+ :sort: safety
{% if ns_feat_ref.missing %}
.. dropdown:: Not referenced by component requirements ({{ ns_feat_ref.missing|length }})
@@ -286,36 +283,16 @@ Requirements Statistics
:style: table
:columns: id;title;safety;status
:colwidths: 15,45,15,25
- :sort: id
+ :sort: safety
{% endif %}
Architecture Statistics
^^^^^^^^^^^^^^^^^^^^^^^
-
-.. grid:: 1 2 2 2
- :gutter: 3
-
- .. grid-item::
-
- .. needpie:: {{ feature_title }} Architecture Elements Status
- :labels: valid, valid_inspected, invalid
- :colors: #f5c518, #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 == "valid_inspected"
- type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and status == "invalid"
-
- .. grid-item::
-
- .. needpie:: {{ feature_title }} Architecture Elements Test Coverage
- :labels: fully covered, partially covered, not covered
- :colors: #37a12d, #f0a500, #ca2828
- :legend:
-
- type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and fully_verifies_back
- type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and partially_verifies_back and not fully_verifies_back
- type in ["feat_arc_sta", "feat_arc_dyn"] and "{{ feature_id }}" in belongs_to and not fully_verifies_back and not partially_verifies_back
+{% set arc_base = 'type in ["feat_arc_sta", "feat_arc_dyn"] and "' ~ feature_id ~ '" in belongs_to' %}
+{{ safety_pie_row(feature_title, "Architecture Elements", arc_base, "ASIL_B", "ASIL B",
+ feature_arcs|selectattr("safety", "eq", "ASIL_B")|list|length, false, [], []) }}
+{{ safety_pie_row(feature_title, "Architecture Elements", arc_base, "QM", "QM",
+ feature_arcs|selectattr("safety", "eq", "QM")|list|length, false, [], []) }}
.. dropdown:: Show architectural elements table
:animate: fade-in
@@ -325,7 +302,7 @@ Architecture Statistics
:style: table
:columns: id;title;safety;status;tags
:colwidths: 25,30,10,15,20
- :sort: id
+ :sort: safety
Inspection Statistics
^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/needs_templates/tool_qualification_report.need b/src/needs_templates/tool_qualification_report.need
index d27cbf9ff..ff2c2bbe5 100644
--- a/src/needs_templates/tool_qualification_report.need
+++ b/src/needs_templates/tool_qualification_report.need
@@ -29,10 +29,6 @@
{{ ((value|default("", true)|replace("\n", " ")|trim) or fallback)|replace("\\", "\\\\")|replace('"', '\\"') }}
{%- endmacro %}
-{% macro id_filter_list(ids) -%}
-[{% for need_id in ids %}"{{ need_id }}"{% if not loop.last %}, {% endif %}{% endfor %}]
-{%- endmacro %}
-
{% set usecase_malfunction_scope = namespace(with_malfunctions=[], without_malfunctions=[]) %}
{% for usecase in usecases %}
{% if linked_needs(usecase["id"], "parent_needs_back")|list %}