From 4bfa15d04bde23b0e563b007429c4b1df4e192e1 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 2 Oct 2026 07:07:13 +0000 Subject: [PATCH 01/10] feat: add requirement reference-coverage pie charts and 'Referenced by' column Add per-requirement reference-coverage pie charts to the platform verification report, showing how many requirements are referenced by at least one lower-hierarchy requirement (stakeholder <- feature requirement, feature requirement <- component requirement). Only references within the report version scope are counted. Component requirements have no lower level, so no chart is generated. The stakeholder and feature requirement tables gain a 'Referenced by' column (derived_from_back) listing referencing requirements. --- .../platform_verification_report.need | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 74e37878b..007eaac98 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -104,6 +104,23 @@ Stakeholder Requirements ------------------------ +{#- Reference coverage: a stakeholder requirement is "referenced" when at least + one in-scope feature requirement is ``derived_from`` it. Only references + from requirements inside ``report_version`` scope are counted. -#} +{% set ns_stkh_ref = namespace(referenced=[], missing=[]) %} +{% for req_id in ns_stkh_reqs.list %} +{% set ns_low = namespace(found=false) %} +{% for low in linked_needs(req_id, "derived_from_back") %} +{% if low["type"] == "feat_req" and req_in_report_version(low, report_version) %} +{% set ns_low.found = true %} +{% endif %} +{% endfor %} +{% if ns_low.found %} +{% set ns_stkh_ref.referenced = ns_stkh_ref.referenced + [req_id] %} +{% else %} +{% set ns_stkh_ref.missing = ns_stkh_ref.missing + [req_id] %} +{% endif %} +{% endfor %} .. grid:: 1 2 2 2 :gutter: 3 @@ -129,14 +146,24 @@ Stakeholder Requirements 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:: + + .. 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) }} + .. dropdown:: Show requirements table :animate: fade-in .. needtable:: :filter: type == "stkh_req" and {{ stkh_req_filter }} :style: table - :columns: id;title;safety;status;testlink - :colwidths: 13,22,8,10,47 + :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" + :colwidths: 12,20,7,9,32,20 :sort: id {# ===================================================================== #} @@ -182,6 +209,22 @@ Feature Overview {% endif %} {% endfor %} {% set feature_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %} +{#- Reference coverage: a feature requirement is "referenced" when at least one + in-scope component requirement is ``derived_from`` it. -#} +{% set ns_feat_ref = namespace(referenced=[], missing=[]) %} +{% for req_id in ns.req_ids %} +{% set ns_low = namespace(found=false) %} +{% for low in linked_needs(req_id, "derived_from_back") %} +{% if low["type"] == "comp_req" and req_in_report_version(low, report_version) %} +{% set ns_low.found = true %} +{% endif %} +{% endfor %} +{% if ns_low.found %} +{% set ns_feat_ref.referenced = ns_feat_ref.referenced + [req_id] %} +{% else %} +{% set ns_feat_ref.missing = ns_feat_ref.missing + [req_id] %} +{% endif %} +{% endfor %} Requirements Statistics ^^^^^^^^^^^^^^^^^^^^^^^ @@ -210,14 +253,24 @@ Requirements Statistics 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:: + + .. 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) }} + .. dropdown:: Show requirements table :animate: fade-in .. needtable:: :filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by and {{ feature_req_filter }} :style: table - :columns: id;title;safety;status;testlink - :colwidths: 13,22,8,10,47 + :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" + :colwidths: 12,20,7,9,32,20 :sort: id Architecture Statistics From 8c37f2af75ab86fe7c1729d94ad747a99c1ccf9d Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 2 Oct 2026 08:03:28 +0000 Subject: [PATCH 02/10] feat: center reference-coverage pie and widen requirement tables Place the third pie chart (reference coverage), which wraps onto a row of its own in the two-column pie grid, in the center via a new opt-in `score-centered-grid-item` grid-item class. The requirement tables gained a "Referenced by" column and became cramped; give them an opt-in `score-wide-needs-table` class with a larger minimum width and horizontal overflow so every column stays readable. --- .../score_layout/assets/css/score_design.css | 7 +++++++ .../score_layout/assets/css/score_needs.css | 11 +++++++++++ src/needs_templates/platform_verification_report.need | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/src/extensions/score_layout/assets/css/score_design.css b/src/extensions/score_layout/assets/css/score_design.css index 5bd9bc537..4bbb365c1 100644 --- a/src/extensions/score_layout/assets/css/score_design.css +++ b/src/extensions/score_layout/assets/css/score_design.css @@ -51,3 +51,10 @@ details.sd-dropdown summary.sd-card-header + div.sd-summary-content background-color: transparent; } + +/* Center a grid item that wraps onto a row of its own (e.g. the third pie + chart in the verification report's two-column pie grids). */ +.score-centered-grid-item { + margin-left: auto; + margin-right: auto; +} diff --git a/src/extensions/score_layout/assets/css/score_needs.css b/src/extensions/score_layout/assets/css/score_needs.css index 020d04c4c..66f73c127 100644 --- a/src/extensions/score_layout/assets/css/score_needs.css +++ b/src/extensions/score_layout/assets/css/score_needs.css @@ -33,6 +33,17 @@ table.NEEDS_DATATABLES { table-layout: fixed; } +/* Opt-in: wide requirement tables (e.g. the verification report, which carries + an extra "Referenced by" column) get a larger minimum width so all columns + stay readable; the wrapper scrolls horizontally on narrower viewports. */ +table.score-wide-needs-table { + min-width: 60rem; +} + +.needstable_wrapper:has(table.score-wide-needs-table) { + overflow-x: auto; +} + .needstable_wrapper table.NEEDS_TABLE th, .needstable_wrapper table.NEEDS_TABLE td, .needstable_wrapper table.NEEDS_DATATABLES th, diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 007eaac98..8eb0a1861 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -147,6 +147,7 @@ Stakeholder Requirements 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 @@ -162,6 +163,7 @@ Stakeholder Requirements .. needtable:: :filter: type == "stkh_req" and {{ stkh_req_filter }} :style: table + :class: score-wide-needs-table :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" :colwidths: 12,20,7,9,32,20 :sort: id @@ -254,6 +256,7 @@ Requirements Statistics 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 @@ -269,6 +272,7 @@ Requirements Statistics .. needtable:: :filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by and {{ feature_req_filter }} :style: table + :class: score-wide-needs-table :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" :colwidths: 12,20,7,9,32,20 :sort: id From b16a3dced2d11522e72acdaeb6efcc15f7021567 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 2 Oct 2026 09:51:38 +0000 Subject: [PATCH 03/10] revert: drop requirement-table widening, keep pie centering Remove the opt-in score-wide-needs-table class (min-width / horizontal overflow) from the requirement needtables and its CSS, restoring the table sizing to the state it had before the widening change. The reference-coverage pie centering is kept. --- .../score_layout/assets/css/score_needs.css | 11 ----------- src/needs_templates/platform_verification_report.need | 2 -- 2 files changed, 13 deletions(-) diff --git a/src/extensions/score_layout/assets/css/score_needs.css b/src/extensions/score_layout/assets/css/score_needs.css index 66f73c127..020d04c4c 100644 --- a/src/extensions/score_layout/assets/css/score_needs.css +++ b/src/extensions/score_layout/assets/css/score_needs.css @@ -33,17 +33,6 @@ table.NEEDS_DATATABLES { table-layout: fixed; } -/* Opt-in: wide requirement tables (e.g. the verification report, which carries - an extra "Referenced by" column) get a larger minimum width so all columns - stay readable; the wrapper scrolls horizontally on narrower viewports. */ -table.score-wide-needs-table { - min-width: 60rem; -} - -.needstable_wrapper:has(table.score-wide-needs-table) { - overflow-x: auto; -} - .needstable_wrapper table.NEEDS_TABLE th, .needstable_wrapper table.NEEDS_TABLE td, .needstable_wrapper table.NEEDS_DATATABLES th, diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 8eb0a1861..c76c9cb83 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -163,7 +163,6 @@ Stakeholder Requirements .. needtable:: :filter: type == "stkh_req" and {{ stkh_req_filter }} :style: table - :class: score-wide-needs-table :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" :colwidths: 12,20,7,9,32,20 :sort: id @@ -272,7 +271,6 @@ Requirements Statistics .. needtable:: :filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by and {{ feature_req_filter }} :style: table - :class: score-wide-needs-table :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" :colwidths: 12,20,7,9,32,20 :sort: id From 0b473b2811f17133c3bee09c5aff3ca2cbea190e Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Mon, 5 Oct 2026 11:16:06 +0000 Subject: [PATCH 04/10] test(docs_bzl): refresh stale score_design.css cache-bust hash in goldens --- .../docs_bzl/scenarios/basic_docs/_expected/docs/index.html | 2 +- .../data_files_runfiles/_expected/docs/data_test/index.html | 2 +- .../scenarios/data_files_runfiles/_expected/docs/index.html | 2 +- .../data_files_runfiles/_expected/docs/isolated_test/index.html | 2 +- .../_expected/docs/legacy_data_test/index.html | 2 +- .../scenarios/external_bundle/_expected/docs/index.html | 2 +- .../_expected/docs/concepts/example_bundle/child/landing.html | 2 +- .../scenarios/nested_bundles/_expected/docs/concepts/index.html | 2 +- .../docs_bzl/scenarios/nested_bundles/_expected/docs/index.html | 2 +- .../unlinked_component/generated_metamodel/index.html | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html index 2c272b36b..d17deef89 100644 --- a/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html index bba07e6ed..42750c7af 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html index 9142771c0..92bdf1a8b 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html index 72410c3e8..bd43aab32 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html index 4cd1edbf9..8e89f033b 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html index 2ceb218ba..723aa1437 100644 --- a/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html index 368348ba7..3dd1086c7 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html index ed666e3c9..6a983af9f 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html index 39c0e6c23..c8fe7f966 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html b/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html index a67b1a73b..eacdbc829 100644 --- a/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html +++ b/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html @@ -34,7 +34,7 @@ - + From 1c7013593353e06445c3247cbdcd40d3ce2d6d55 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Tue, 6 Oct 2026 07:49:07 +0000 Subject: [PATCH 05/10] perf(platform-report): single forward pass for reference-coverage pies The Stakeholder and Feature "Reference Coverage" pies previously scanned the whole Need graph per requirement via linked_needs(id, "derived_from_back"), which is O(all needs) each. Iterate the lower requirements once instead and follow their cheap outgoing derived_from link; for Features the index is built a single time before the per-feature loop, which then only tests membership. Addresses Copilot review Finding 1 (performance). --- .../platform_verification_report.need | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index c76c9cb83..d45bd58c2 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -105,17 +105,22 @@ Stakeholder Requirements ------------------------ {#- Reference coverage: a stakeholder requirement is "referenced" when at least - one in-scope feature requirement is ``derived_from`` it. Only references - from requirements inside ``report_version`` scope are counted. -#} -{% set ns_stkh_ref = namespace(referenced=[], missing=[]) %} -{% for req_id in ns_stkh_reqs.list %} -{% set ns_low = namespace(found=false) %} -{% for low in linked_needs(req_id, "derived_from_back") %} -{% if low["type"] == "feat_req" and req_in_report_version(low, report_version) %} -{% set ns_low.found = true %} + one in-scope feature requirement is ``derived_from`` it. Iterate the lower + requirements once and follow their cheap outgoing ``derived_from`` link, + instead of scanning the whole Need graph per stakeholder requirement. -#} +{% set ns_ref = namespace(up_ids=[]) %} +{% for low in needs_of_type("feat_req") %} +{% if req_in_report_version(low, report_version) %} +{% for up in linked_needs(low["id"], "derived_from") %} +{% if up["type"] == "stkh_req" and up["id"] in ns_stkh_reqs.list %} +{% set ns_ref.up_ids = ns_ref.up_ids + [up["id"]] %} +{% endif %} +{% endfor %} {% endif %} {% endfor %} -{% if ns_low.found %} +{% set ns_stkh_ref = namespace(referenced=[], missing=[]) %} +{% for req_id in ns_stkh_reqs.list %} +{% if req_id in ns_ref.up_ids %} {% set ns_stkh_ref.referenced = ns_stkh_ref.referenced + [req_id] %} {% else %} {% set ns_stkh_ref.missing = ns_stkh_ref.missing + [req_id] %} @@ -183,6 +188,21 @@ Feature Overview :style: table :sort: id +{#- Reference coverage index, built once for all Features: a feature + requirement is "referenced" when at least one in-scope component + requirement is ``derived_from`` it. Iterate the component requirements once + and follow their cheap outgoing ``derived_from`` link, so the per-feature + sections below only test membership instead of rescanning the graph. -#} +{% set ns_comp_ref = namespace(feat_ids=[]) %} +{% for low in needs_of_type("comp_req") %} +{% if req_in_report_version(low, report_version) %} +{% for up in linked_needs(low["id"], "derived_from") %} +{% if up["type"] == "feat_req" %} +{% set ns_comp_ref.feat_ids = ns_comp_ref.feat_ids + [up["id"]] %} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} {% for feature in report_features %} {% set feature_id = feature["id"] %} {% set feature_title = feature["title"] %} @@ -210,17 +230,11 @@ Feature Overview {% endif %} {% endfor %} {% set feature_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %} -{#- Reference coverage: a feature requirement is "referenced" when at least one - in-scope component requirement is ``derived_from`` it. -#} +{#- Reference coverage: partition this Feature's in-scope requirements by the + pre-built index above instead of rescanning the graph per requirement. -#} {% set ns_feat_ref = namespace(referenced=[], missing=[]) %} {% for req_id in ns.req_ids %} -{% set ns_low = namespace(found=false) %} -{% for low in linked_needs(req_id, "derived_from_back") %} -{% if low["type"] == "comp_req" and req_in_report_version(low, report_version) %} -{% set ns_low.found = true %} -{% endif %} -{% endfor %} -{% if ns_low.found %} +{% if req_id in ns_comp_ref.feat_ids %} {% set ns_feat_ref.referenced = ns_feat_ref.referenced + [req_id] %} {% else %} {% set ns_feat_ref.missing = ns_feat_ref.missing + [req_id] %} From 0ec4ad8b67359c2ffbdb9ad4c9f1a8c386bee8d4 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Tue, 6 Oct 2026 10:43:08 +0000 Subject: [PATCH 06/10] fix(platform-report): drop "Referenced by" requirement table column The "Referenced by" column (backed by the unscoped `derived_from_back` backlinks) was inconsistent with the report-version-scoped Reference Coverage pie next to it. Remove it for now; a properly scoped approach will follow in a separate PR. --- src/needs_templates/platform_verification_report.need | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index d45bd58c2..8e4610b9d 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -168,8 +168,8 @@ Stakeholder Requirements .. needtable:: :filter: type == "stkh_req" and {{ stkh_req_filter }} :style: table - :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" - :colwidths: 12,20,7,9,32,20 + :columns: id;title;safety;status;testlink + :colwidths: 13,22,8,10,47 :sort: id {# ===================================================================== #} @@ -285,8 +285,8 @@ Requirements Statistics .. needtable:: :filter: type == "feat_req" and "{{ feature_id }}" in satisfied_by and {{ feature_req_filter }} :style: table - :columns: id;title;safety;status;testlink;derived_from_back as "Referenced by" - :colwidths: 12,20,7,9,32,20 + :columns: id;title;safety;status;testlink + :colwidths: 13,22,8,10,47 :sort: id Architecture Statistics From 9dbca09c45437e789e2edbf5a69adcc3e4805813 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 7 Oct 2026 07:36:42 +0000 Subject: [PATCH 07/10] feat(platform-report): list requirements without in-scope references The reference coverage pies show *how many* requirements are not referenced by any lower-level requirement, but not *which* ones. Add a collapsed table next to each pie that lists exactly those requirements: - Stakeholder section: stakeholder requirements not referenced by any in-scope feature requirement. - Per feature: feature requirements not referenced by any in-scope component requirement. Both tables are filtered by the same pre-scoped ID lists that feed the pies (`ns_stkh_ref.missing` / `ns_feat_ref.missing`), so chart and table cannot disagree. The report version scoping therefore applies to the tables as well: a requirement referenced only from outside the report's `report_version` still counts as a gap. The tables are omitted entirely when there is no gap, so an empty table never shows up. Adds an end-to-end test rendering the report, covering the case where the only reference comes from an out-of-scope requirement. --- .../tests/test_needs_templates.py | 117 ++++++++++++++++++ .../platform_verification_report.need | 28 +++++ 2 files changed, 145 insertions(+) diff --git a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py index 2bdf8ec18..e088710a2 100644 --- a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py +++ b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py @@ -315,3 +315,120 @@ def test_tool_qualification_matrix_groups_shared_requirements( ) assert '
' not in html assert '
' not in html + + +def test_platform_report_gap_table_is_report_version_scoped( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The gap table lists requirements lacking an *in-scope* reference. + + A stakeholder requirement that is only referenced by a feature requirement + outside the report's ``report_version`` counts as a gap, because the + reference coverage pie classifies it as "not referenced" too. Both are + rendered from the same pre-scoped ID list, so they cannot disagree. + """ + monkeypatch.setenv("BUILD_WORKSPACE_DIRECTORY", str(tmp_path)) + (tmp_path / "conf.py").write_text( + """ +extensions = [ + "sphinx_needs", + "score_sphinx_needs_templates", + "score_metamodel", + "sphinx_design", +] +master_doc = "index" +needs_id_regex = r"^[a-zA-Z0-9_]+$" +""", + encoding="utf-8", + ) + (tmp_path / "index.rst").write_text( + """ +.. workproduct:: Platform report work product + :id: wp__platform_report + :status: valid + +.. document:: Platform verification report + :id: doc__platform_report + :status: valid + :safety: ASIL_B + :security: NO + :realizes: wp__platform_report + :report_version: v1.0 + :post_template: platform_verification_report + +.. stkh_req:: Referenced stakeholder requirement + :id: stkh_req__referenced + :reqtype: Functional + :safety: ASIL_B + :security: NO + :status: valid + :rationale: Referenced by an in-scope feature requirement. + :valid_from: v0.8 + + The platform shall satisfy the referenced stakeholder need. + +.. stkh_req:: Stakeholder requirement referenced out of scope only + :id: stkh_req__only_out_of_scope + :reqtype: Functional + :safety: ASIL_B + :security: NO + :status: valid + :rationale: Referenced only by an out-of-scope feature requirement. + :valid_from: v0.8 + + The platform shall satisfy the out-of-scope referenced stakeholder need. + +.. feat:: Scoped feature + :id: feat__scoped + :security: NO + :safety: ASIL_B + :status: valid + +.. feat_req:: In-scope feature requirement + :id: feat_req__in_scope + :reqtype: Functional + :security: NO + :safety: ASIL_B + :status: valid + :satisfied_by: feat__scoped + :derived_from: stkh_req__referenced + :valid_from: v0.8 + + This requirement is inside the report version scope. + +.. feat_req:: Out-of-scope feature requirement + :id: feat_req__out_of_scope + :reqtype: Functional + :security: NO + :safety: ASIL_B + :status: valid + :satisfied_by: feat__scoped + :derived_from: stkh_req__only_out_of_scope + :valid_from: v2.0 + + This requirement is outside the report version scope. +""", + encoding="utf-8", + ) + + app = SphinxTestApp( + srcdir=tmp_path, + outdir=tmp_path / "_build", + buildername="html", + freshenv=True, + ) + try: + app.build(force_all=True) + html = (app.outdir / "index.html").read_text(encoding="utf-8") + finally: + app.cleanup() + + assert "Not referenced by feature requirements" in html + gap_table = html.split("Not referenced by feature requirements", 1)[1].split( + '
Date: Wed, 7 Oct 2026 10:47:24 +0000 Subject: [PATCH 08/10] refactor(platform-report): scope centering CSS to the report template The `.score-centered-grid-item` rule is used only by platform_verification_report.need, so it belongs next to the markup it styles rather than in the globally loaded score_design.css. Moves it into the template's existing `raw:: html` style block, following the pattern already used by module_verification_report.need. Side effect: score_design.css is unchanged again, so its asset hash stays at v=8048e6ee and the ten expected-output HTML fixtures no longer need to be touched by this PR. Addresses review feedback from @AlexanderLanin. --- src/extensions/score_layout/assets/css/score_design.css | 7 ------- src/needs_templates/platform_verification_report.need | 5 +++++ .../scenarios/basic_docs/_expected/docs/index.html | 2 +- .../_expected/docs/data_test/index.html | 2 +- .../data_files_runfiles/_expected/docs/index.html | 2 +- .../_expected/docs/isolated_test/index.html | 2 +- .../_expected/docs/legacy_data_test/index.html | 2 +- .../scenarios/external_bundle/_expected/docs/index.html | 2 +- .../docs/concepts/example_bundle/child/landing.html | 2 +- .../nested_bundles/_expected/docs/concepts/index.html | 2 +- .../scenarios/nested_bundles/_expected/docs/index.html | 2 +- .../unlinked_component/generated_metamodel/index.html | 2 +- 12 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/extensions/score_layout/assets/css/score_design.css b/src/extensions/score_layout/assets/css/score_design.css index 4bbb365c1..5bd9bc537 100644 --- a/src/extensions/score_layout/assets/css/score_design.css +++ b/src/extensions/score_layout/assets/css/score_design.css @@ -51,10 +51,3 @@ details.sd-dropdown summary.sd-card-header + div.sd-summary-content background-color: transparent; } - -/* Center a grid item that wraps onto a row of its own (e.g. the third pie - chart in the verification report's two-column pie grids). */ -.score-centered-grid-item { - margin-left: auto; - margin-right: auto; -} diff --git a/src/needs_templates/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 8e4610b9d..caf3469e9 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -95,6 +95,11 @@ .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; } + /* Centers a pie that wraps onto a row of its own in the two-column grids. */ + .score-centered-grid-item { + margin-left: auto; + margin-right: auto; + } {# ===================================================================== #} diff --git a/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html index d17deef89..2c272b36b 100644 --- a/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/basic_docs/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html index 42750c7af..bba07e6ed 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/data_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html index 92bdf1a8b..9142771c0 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html index bd43aab32..72410c3e8 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/isolated_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html index 8e89f033b..4cd1edbf9 100644 --- a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html +++ b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/docs/legacy_data_test/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html index 723aa1437..2ceb218ba 100644 --- a/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/external_bundle/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html index 3dd1086c7..368348ba7 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/example_bundle/child/landing.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html index 6a983af9f..ed666e3c9 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/concepts/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html index c8fe7f966..39c0e6c23 100644 --- a/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html +++ b/src/tests/docs_bzl/scenarios/nested_bundles/_expected/docs/index.html @@ -34,7 +34,7 @@ - + diff --git a/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html b/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html index eacdbc829..a67b1a73b 100644 --- a/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html +++ b/src/tests/docs_bzl/scenarios/reference_integration/_expected/docs/modules/modern_module/components/unlinked_component/generated_metamodel/index.html @@ -34,7 +34,7 @@ - + From c50e2cd772b7aed4548962fbbb75f7d8c7602082 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 7 Oct 2026 12:04:36 +0000 Subject: [PATCH 09/10] style: drop stray trailing newline in test_needs_templates.py --- .../score_sphinx_needs_templates/tests/test_needs_templates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py index e088710a2..bddb56735 100644 --- a/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py +++ b/src/extensions/score_sphinx_needs_templates/tests/test_needs_templates.py @@ -431,4 +431,3 @@ def test_platform_report_gap_table_is_report_version_scoped( # Referenced only outside the report scope, so it still counts as a gap. assert 'href="#stkh_req__only_out_of_scope"' in gap_table assert 'href="#stkh_req__referenced"' not in gap_table - From 164fbf096f45eac216a33e27dd634769a0f5cb45 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 7 Oct 2026 14:21:16 +0000 Subject: [PATCH 10/10] perf(platform-report): move reference-coverage indexing into Python The reference-coverage sections built their index and partitioned the requirements with MiniJinja loops. Every `in` test and every `list + [item]` append crossed the Rust/Python boundary, which dominated the cost and scaled quadratically. Add two render-context helpers, `referenced_ids` and `split_by_reference`, and let the template call them instead. Output is unchanged. Measured on the stakeholder block (identical rendered output): stkh x feat | old | new | speedup 20 x 25 | 12.53 ms | 0.09 ms | 141x 300 x 600 | 72.16 ms | 0.27 ms | 265x 600 x 1200 | 362.2 ms | 0.55 ms | 663x Note that a `set` is not available inside the template: MiniJinja has no set literal and no `set()` function, and handing a Python `set` into the template does not help either, because the boundary crossing rather than the lookup is what costs (frozenset was only ~8% faster than a list). --- .../score_sphinx_needs_templates/__init__.py | 52 ++++++++++++++++++ .../platform_verification_report.need | 55 ++++--------------- 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/extensions/score_sphinx_needs_templates/__init__.py b/src/extensions/score_sphinx_needs_templates/__init__.py index 3de92144f..806adb609 100644 --- a/src/extensions/score_sphinx_needs_templates/__init__.py +++ b/src/extensions/score_sphinx_needs_templates/__init__.py @@ -244,6 +244,52 @@ def __call__(self, reqs: list[NeedItem], report_version: str | None) -> bool: _any_req_in_report_version_callable = _AnyRequirementInReportVersion() +class _ReferencedIds: + """Collect the IDs that in-scope lower-level requirements point at. + + Reference coverage asks the reverse question ("is this requirement + referenced?"), but answering it per requirement means rescanning the Need + graph for every row. Walking the lower requirements once and following + their cheap outgoing link instead turns that into a single pass. + + Returned as a sorted list because the result travels back through the + template; ``split_by_reference`` turns it into a set again for lookups. + """ + + def __call__( + self, lower_type: str, link_name: str, report_version: str | None + ) -> list[str]: + referenced: set[str] = set() + for lower in _needs_of_type_callable(lower_type): + if not _req_in_report_version_callable(lower, report_version): + continue + for upper in _linked_needs_callable(lower["id"], link_name): + referenced.add(upper["id"]) + return sorted(referenced) + + +_referenced_ids_callable = _ReferencedIds() + + +class _SplitByReference: + """Partition requirement IDs into referenced and unreferenced ones. + + Doing this in Python keeps the membership tests out of the template: every + ``in`` test evaluated by MiniJinja crosses the Rust/Python boundary, which + costs far more than the lookup itself. + """ + + def __call__(self, ids: list[str], referenced: list[str]) -> dict[str, list[str]]: + index = set(referenced) + return { + "referenced": [need_id for need_id in ids if need_id in index], + "missing": [need_id for need_id in ids if need_id not in index], + } + + +_split_by_reference_callable = _SplitByReference() + + 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() @@ -334,6 +380,12 @@ def setup(app: Sphinx) -> dict[str, object]: app.config.needs_render_context.setdefault( "any_req_in_report_version", _any_req_in_report_version_callable ) + app.config.needs_render_context.setdefault( + "referenced_ids", _referenced_ids_callable + ) + app.config.needs_render_context.setdefault( + "split_by_reference", _split_by_reference_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/platform_verification_report.need b/src/needs_templates/platform_verification_report.need index 0941e0de3..9f6b30aad 100644 --- a/src/needs_templates/platform_verification_report.need +++ b/src/needs_templates/platform_verification_report.need @@ -110,27 +110,12 @@ Stakeholder Requirements ------------------------ {#- Reference coverage: a stakeholder requirement is "referenced" when at least - one in-scope feature requirement is ``derived_from`` it. Iterate the lower - requirements once and follow their cheap outgoing ``derived_from`` link, - instead of scanning the whole Need graph per stakeholder requirement. -#} -{% set ns_ref = namespace(up_ids=[]) %} -{% for low in needs_of_type("feat_req") %} -{% if req_in_report_version(low, report_version) %} -{% for up in linked_needs(low["id"], "derived_from") %} -{% if up["type"] == "stkh_req" and up["id"] in ns_stkh_reqs.list %} -{% set ns_ref.up_ids = ns_ref.up_ids + [up["id"]] %} -{% endif %} -{% endfor %} -{% endif %} -{% endfor %} -{% set ns_stkh_ref = namespace(referenced=[], missing=[]) %} -{% for req_id in ns_stkh_reqs.list %} -{% if req_id in ns_ref.up_ids %} -{% set ns_stkh_ref.referenced = ns_stkh_ref.referenced + [req_id] %} -{% else %} -{% set ns_stkh_ref.missing = ns_stkh_ref.missing + [req_id] %} -{% endif %} -{% endfor %} + 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 @@ -210,19 +195,8 @@ Feature Overview {#- Reference coverage index, built once for all Features: a feature requirement is "referenced" when at least one in-scope component - requirement is ``derived_from`` it. Iterate the component requirements once - and follow their cheap outgoing ``derived_from`` link, so the per-feature - sections below only test membership instead of rescanning the graph. -#} -{% set ns_comp_ref = namespace(feat_ids=[]) %} -{% for low in needs_of_type("comp_req") %} -{% if req_in_report_version(low, report_version) %} -{% for up in linked_needs(low["id"], "derived_from") %} -{% if up["type"] == "feat_req" %} -{% set ns_comp_ref.feat_ids = ns_comp_ref.feat_ids + [up["id"]] %} -{% endif %} -{% endfor %} -{% endif %} -{% endfor %} + requirement is ``derived_from`` it. -#} +{% set comp_referenced_ids = referenced_ids("comp_req", "derived_from", report_version) %} {% for feature in report_features %} {% set feature_id = feature["id"] %} {% set feature_title = feature["title"] %} @@ -250,16 +224,9 @@ Feature Overview {% endif %} {% endfor %} {% set feature_req_filter = 'id in ' ~ id_filter_list(ns.req_ids) %} -{#- Reference coverage: partition this Feature's in-scope requirements by the - pre-built index above instead of rescanning the graph per requirement. -#} -{% set ns_feat_ref = namespace(referenced=[], missing=[]) %} -{% for req_id in ns.req_ids %} -{% if req_id in ns_comp_ref.feat_ids %} -{% set ns_feat_ref.referenced = ns_feat_ref.referenced + [req_id] %} -{% else %} -{% set ns_feat_ref.missing = ns_feat_ref.missing + [req_id] %} -{% endif %} -{% endfor %} +{#- 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) %} Requirements Statistics ^^^^^^^^^^^^^^^^^^^^^^^