From 4a2ba64a00d4ce6840ea98e8cbe6b8b88bfa2dc7 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 18 Sep 2026 09:19:27 +0000 Subject: [PATCH 1/3] feat: split platform-scoped features out of module_verification_report module_verification_report previously rendered both the module's own components AND every Feature reachable from those components, mixing module-scoped and platform-scoped statistics into a single report. - module_verification_report.need: drop the feature-loop section (Requirements/Architecture/Inspection Statistics per Feature). The report now covers Components only, as its name implies. - platform_verification_report.need (new): the feature-scoped counterpart, listing every Feature in the current build (via the existing needs_of_type("feat") helper) with the same statistics that were removed from the module report. --- .../module_verification_report.need | 137 +------------ .../platform_verification_report.need | 194 ++++++++++++++++++ 2 files changed, 201 insertions(+), 130 deletions(-) create mode 100644 src/needs_templates/platform_verification_report.need diff --git a/src/needs_templates/module_verification_report.need b/src/needs_templates/module_verification_report.need index 64bbbcea8..d2f25cb0f 100644 --- a/src/needs_templates/module_verification_report.need +++ b/src/needs_templates/module_verification_report.need @@ -6,10 +6,13 @@ The report is a ``document`` need whose id encodes the module it covers (``doc___verification_report``) — the ``document`` type has no ``belongs_to`` link, so the module id is recovered from this need's own id - instead. The module's ``includes`` links provide the components, and each - component's ``belongs_to`` link provides the feature. The ``linked_needs`` - helper resolves this graph during the post-collection reread, so titles and - report sections stay driven by the Need model. + instead. The module's ``includes`` links provide the components. The + ``linked_needs`` helper resolves this graph during the post-collection + reread, so titles and report sections stay driven by the Need model. + + This report is module- and component-scoped only. The platform-wide, + feature-scoped counterpart (all Features and their statistics) lives in the + sibling ``platform_verification_report`` template. The template is applied as ``:post_template:``, not ``:template:``. A need's *content* cannot open new sections ("Unexpected section title"), but @@ -22,16 +25,6 @@ may list a component more than once, so deduplicate the NeedItems by ID. #} {% set components_in_mod = linked_needs(module_id, "includes")|unique(attribute="id")|list %} -{# Collect every feature reachable from the module's components. A feature can - be linked by multiple components, so collect all candidates first and then - keep each feature NeedItem only once in first-seen graph order. The - namespace is required because assignments inside a Jinja loop are scoped. #} -{% set feature_candidates = namespace(items=[]) %} -{% for component in components_in_mod %} -{% set feature_candidates.items = feature_candidates.items + linked_needs(component["id"], "belongs_to") %} -{% endfor %} -{% set report_features = feature_candidates.items|unique(attribute="id")|list %} - {% set component_workproducts = [ ["wp__requirements_inspect", "Requirements Inspection"], ["wp__sw_arch_verification", "Architecture Inspection"], @@ -39,10 +32,6 @@ ["wp__sw_component_dfa", "DFA"], ["wp__sw_component_fmea", "FMEA"], ] %} -{% set feature_workproducts = [ - ["wp__requirements_inspect", "Requirements Inspection"], - ["wp__sw_arch_verification", "Architecture Inspection"], - ] %} {#- 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 @@ -88,118 +77,6 @@ .wp-doc-table td .dataTables_wrapper .dataTables_paginate { display: none; } -{#- ===================================================================== -#} -{#- Feature sections resolved from all components' belongs_to links. -#} -{#- ===================================================================== -#} -{% for report_feature in report_features %} -{% set feature_id = report_feature["id"] %} -{% set feature_title = report_feature["title"] %} -{# Derive the work-product document selector from the feature Need reached - through the graph, rather than reconstructing it from the module ID. #} -{% set feature_slug_norm = feature_title|replace("_", "")|replace(" ", "")|lower %} -{% set feature_heading = feature_title if report_features|length == 1 else "Feature: " ~ feature_title %} - -{{ feature_heading }} -{{ "-" * (feature_heading|length) }} - -.. needtable:: - :filter: id == "{{ feature_id }}" - :columns: title as "Name";id as "Id";safety;security;status - :style: table - -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 fully_verifies_back - type == "feat_req" and "{{ feature_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back - type == "feat_req" and "{{ feature_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back - -.. 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;testlink - :colwidths: 13,22,8,10,47 - :sort: id - -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 - -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 -{{ workproduct_rows(feature_slug_norm, feature_workproducts) }} -{% endfor %} - {# ===================================================================== #} {# Components #} {# ===================================================================== #} diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need new file mode 100644 index 000000000..271109350 --- /dev/null +++ b/src/needs_templates/platform_verification_report.need @@ -0,0 +1,194 @@ +{# + score: render-after-needs-collection + + Content template for the ``document`` need type for ``platform_verification_report``. + + Unlike ``module_verification_report``, this report is not scoped to a single + module: it collects every Feature in the current build's Need graph via the + ``needs_of_type`` helper, so it always reflects the full platform surface + regardless of which document instantiates it. This is the platform-wide, + feature-scoped counterpart to ``module_verification_report``'s + module-and-component-scoped report. + + The template is applied as ``:post_template:``, not ``:template:``. A need's + *content* cannot open new sections ("Unexpected section title"), but + post-content is placed after the need at document level, where real headings + work — and real headings are what give the report its TOC entries and + per-feature navigation. +#} +{% set report_features = needs_of_type("feat")|unique(attribute="id")|sort(attribute="title")|list %} + +{% set feature_workproducts = [ + ["wp__requirements_inspect", "Requirements Inspection"], + ["wp__sw_arch_verification", "Architecture Inspection"], + ] %} + +{#- 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 %} + +.. raw:: html + + + +{# ===================================================================== #} +{# Features #} +{# ===================================================================== #} + +Features +-------- + +Feature Overview +~~~~~~~~~~~~~~~~ + +.. needtable:: + :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 + +{% for feature in report_features %} +{% set feature_id = feature["id"] %} +{% set feature_title = feature["title"] %} +{# The feature Need supplies the work-product document selector and the + navigation anchor, the same way components do in module_verification_report. #} +{% set feature_slug_norm = feature_title|replace("_", "")|replace(" ", "")|lower %} +{% set feature_anchor = feature_title|replace("_", "-")|replace(" ", "-")|lower %} + +.. _feat-{{ feature_anchor }}: + +{{ feature_title }} +{{ "~" * (feature_title|length) }} + +.. raw:: html + +
+ +Requirements Statistics +^^^^^^^^^^^^^^^^^^^^^^^ + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: {{ feature_title }} 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_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 + type == "feat_req" and "{{ feature_id }}" in satisfied_by and partially_verifies_back and not fully_verifies_back + type == "feat_req" and "{{ feature_id }}" in satisfied_by and not fully_verifies_back and not partially_verifies_back + +.. 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;testlink + :colwidths: 13,22,8,10,47 + :sort: id + +Architecture Statistics +^^^^^^^^^^^^^^^^^^^^^^^ + +.. grid:: 1 2 2 2 + :gutter: 3 + + .. grid-item:: + + .. needpie:: {{ feature_title }} 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_title }} 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 + +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 +{{ workproduct_rows(feature_slug_norm, feature_workproducts) }} +{% endfor %} From 4464e8ad766701bd854a4e3981c62e17067d97a7 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 18 Sep 2026 11:27:58 +0000 Subject: [PATCH 2/3] feat: scope verification reports to a report_version via valid_from Adds an optional report_version attribute on document Needs. When set, module_verification_report and platform_verification_report only show requirements (and the Features/Components that own them) with valid_from <= report_version. comp_req has no valid_from of its own, so its scope is inherited from the feat_req it is derived_from. Features/Components with no in-scope requirement are dropped from the report entirely instead of rendering an empty section. Unset/empty report_version keeps reports unscoped, matching prior behavior. --- src/extensions/score_metamodel/metamodel.yaml | 4 + .../score_sphinx_needs_templates/__init__.py | 74 +++++++++++++++++++ .../module_verification_report.need | 46 ++++++++++-- .../platform_verification_report.need | 44 +++++++++-- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index ed60080bc..fc1041f23 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -230,6 +230,10 @@ needs_types: author: ^.*$ approver: ^.*$ reviewer: ^.*$ + # Scopes module_verification_report/platform_verification_report to + # requirements with valid_from <= report_version. Unset means unscoped + # (all requirements shown), used e.g. for the "_latest" reports. + report_version: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$ # req-Id: tool_req__docs_doc_generic_mandatory mandatory_links: realizes: workproduct diff --git a/src/extensions/score_sphinx_needs_templates/__init__.py b/src/extensions/score_sphinx_needs_templates/__init__.py index a7b33e859..6ecca6991 100644 --- a/src/extensions/score_sphinx_needs_templates/__init__.py +++ b/src/extensions/score_sphinx_needs_templates/__init__.py @@ -178,6 +178,76 @@ def __call__(self, need_type: str) -> list[NeedItem]: _needs_of_type_callable = _NeedsOfType() +def _parse_version(value: str) -> tuple[int, int, int]: + """Parse a ``valid_from``/``report_version``-style milestone string. + + Accepts ``vMAJOR.MINOR`` or ``vMAJOR.MINOR.PATCH`` (e.g. ``v0.8`` or + ``v1.0.1``), matching the format enforced by the metamodel for + ``valid_from``/``valid_until``/``report_version``. + """ + numbers = [int(part) for part in value.strip().lstrip("vV").split(".")] + while len(numbers) < 3: + numbers.append(0) + return (numbers[0], numbers[1], numbers[2]) + + +class _RequirementInScope: + """Decide whether a requirement Need belongs to a ``report_version`` scope. + + ``feat_req`` (and ``stkh_req``) carry ``valid_from`` directly. ``comp_req`` + has no ``valid_from`` of its own, so its scope is inherited from the + ``feat_req`` Need(s) it is ``derived_from``. A requirement without a + resolvable ``valid_from`` (directly or through ``derived_from``) is + excluded whenever a ``report_version`` scope is active, matching the rule + that only requirements with ``valid_from`` set are considered relevant for + a given release. + + Calling with an empty/``None`` ``report_version`` always returns ``True``, + which keeps unscoped reports (e.g. "latest") showing every requirement as + before. + """ + + def __call__(self, need: NeedItem, report_version: str | None) -> bool: + if not report_version: + return True + + valid_from = need.get("valid_from") + if valid_from: + try: + return _parse_version(valid_from) <= _parse_version(report_version) + except ValueError: + return False + + linked_feat_reqs = _linked_needs_callable(need["id"], "derived_from") + return any( + self(feat_req, report_version) for feat_req in linked_feat_reqs + ) + + +_req_in_scope_callable = _RequirementInScope() + + +class _AnyRequirementInScope: + """Decide whether a Feature/Component has any requirement in scope. + + Used to drop an entire Feature/Component section from the report when + ``report_version`` is set and none of its requirements qualify, instead of + rendering an empty section. An empty/``None`` ``report_version`` always + returns ``True`` (unscoped reports keep every Feature/Component, even + ones without any requirement at all, as before). + """ + + def __call__( + self, reqs: list[NeedItem], report_version: str | None + ) -> bool: + if not report_version: + return True + return any(_req_in_scope_callable(req, report_version) for req in reqs) + + +_any_req_in_scope_callable = _AnyRequirementInScope() + + def _post_templates_requiring_reread(app: Sphinx) -> set[str]: """Return post-template names opting into the post-merge rendering pass.""" template_folder = _needs_template_folder() @@ -262,6 +332,10 @@ def setup(app: Sphinx) -> dict[str, object]: ) app.config.needs_render_context.setdefault("linked_needs", _linked_needs_callable) app.config.needs_render_context.setdefault("needs_of_type", _needs_of_type_callable) + app.config.needs_render_context.setdefault("req_in_scope", _req_in_scope_callable) + app.config.needs_render_context.setdefault( + "any_req_in_scope", _any_req_in_scope_callable + ) app.connect("builder-inited", _capture_build_environment) # Run after the source-code linker has injected generated testcase Needs and # their verification backlinks (priority 525), so report templates can diff --git a/src/needs_templates/module_verification_report.need b/src/needs_templates/module_verification_report.need index d2f25cb0f..f6dfbeb12 100644 --- a/src/needs_templates/module_verification_report.need +++ b/src/needs_templates/module_verification_report.need @@ -21,9 +21,21 @@ per-component navigation. #} {% set module_id = "mod__" ~ id|replace("doc__", "")|replace("_verification_report", "") %} +{# Unset/empty ``report_version`` keeps the report unscoped (all components + and requirements shown), which is what the "_latest" report relies on. #} +{% set report_version = report_version|default(None, true) %} {# Resolve the component list from the module's outgoing graph links. A module - may list a component more than once, so deduplicate the NeedItems by ID. #} -{% set components_in_mod = linked_needs(module_id, "includes")|unique(attribute="id")|list %} + may list a component more than once, so deduplicate the NeedItems by ID. + A component with no requirement in scope is dropped from the report + entirely instead of rendering an empty section. #} +{% set ns_components = namespace(list=[]) %} +{% for component in linked_needs(module_id, "includes")|unique(attribute="id")|list %} +{% set component_reqs = linked_needs(component["id"], "satisfied_by_back")|selectattr("type", "eq", "comp_req")|list %} +{% if any_req_in_scope(component_reqs, report_version) %} +{% set ns_components.list = ns_components.list + [component] %} +{% endif %} +{% endfor %} +{% set components_in_mod = ns_components.list %} {% set component_workproducts = [ ["wp__requirements_inspect", "Requirements Inspection"], @@ -51,6 +63,13 @@ {%- 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 %} + .. raw:: html