diff --git a/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index 751f16011..55aa50471 100644 --- a/docs/internals/requirements/requirements.rst +++ b/docs/internals/requirements/requirements.rst @@ -886,6 +886,22 @@ Testing * link the inspected artifacts via ``inspects`` * allow links to backing evidence via ``evidence`` +.. tool_req:: Support machine-readable inspection reports + :id: tool_req__docs_inspection_report_need + :tags: Verification Evidence + :implemented: YES + :version: 1 + :satisfies: gd_req__verification_checks + :parent_covered: NO: process wording defines inspection completeness, while the tool models an explicit expected inspection set. + + Docs-as-Code shall support a machine-readable module inspection report need type. + + The need type shall: + + * aggregate inspection records via ``contains`` and belong to the module via ``belongs_to`` + * declare the required checklist categories via ``expected_inspections`` + * be gated so every expected inspection type has a contained, approved inspection record + 🧪 Tool Verification Reports ############################ diff --git a/src/extensions/score_metamodel/checks/graph_checks.py b/src/extensions/score_metamodel/checks/graph_checks.py index b6c854956..134a9fa58 100644 --- a/src/extensions/score_metamodel/checks/graph_checks.py +++ b/src/extensions/score_metamodel/checks/graph_checks.py @@ -227,6 +227,58 @@ def check_metamodel_graph( log.warning_for_need(need, msg) +@graph_check +def check_inspection_report_completeness( + app: Sphinx, + all_needs: NeedsView, + log: CheckLogger, +): + needs_dict_all = {need["id"]: need for need in all_needs.values()} + for report in all_needs.filter_is_external(False).values(): + if report.get("type") != "mod_insp_report": + continue + + expected_inspections = report.get("expected_inspections") + if ( + not isinstance(expected_inspections, str) + or not expected_inspections.strip() + ): + continue + + expected = [ + inspection_type.strip() + for inspection_type in expected_inspections.split(",") + if inspection_type.strip() + ] + raw_contains = report.get("contains", []) + if isinstance(raw_contains, str): + contains_ids = [raw_contains] + elif isinstance(raw_contains, list | tuple | set): + contains_ids = [str(contained_id) for contained_id in raw_contains] + else: + contains_ids = [] + + covered = { + contained.get("inspection_type") + for contained_id in contains_ids + if (contained := needs_dict_all.get(contained_id)) is not None + and contained.get("type") == "mod_insp" + and contained.get("inspection_state") == "approved" + and contained.get("status") == "valid" + } + missing = [ + inspection_type + for inspection_type in expected + if inspection_type not in covered + ] + if missing: + log.warning_for_need( + report, + "Inspection report is missing approved inspection(s) for: " + + ", ".join(missing), + ) + + @graph_check def check_valid_only_links_to_valid( app: Sphinx, diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index a6f9e68db..a2a4142be 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -1003,6 +1003,32 @@ needs_types: - inspection parts: 3 + # Aggregates a module's inspection records and gates inspection completeness. + # req-Id: tool_req__docs_inspection_report_need + mod_insp_report: + title: Module Inspection Report + prefix: mod_ispr__ + mandatory_options: + # req-Id: tool_req__docs_common_attr_safety + safety: ^(QM|ASIL_B)$ + # req-Id: tool_req__docs_common_attr_security + security: ^(YES|NO)$ + # req-Id: tool_req__docs_common_attr_status + status: ^(valid|invalid)$ + # req-Id: tool_req__docs_inspection_report_need + # comma-separated subset of the three checklist categories the module must cover + expected_inspections: ^(requirements|architecture|implementation)(,(requirements|architecture|implementation))*$ + mandatory_links: + # req-Id: tool_req__docs_inspection_report_need + belongs_to: mod + contains: mod_insp + optional_links: + # req-Id: tool_req__docs_inspection_report_need + evidence: ANY + tags: + - inspection + parts: 3 + # https://eclipse-score.github.io/process_description/main/permalink.html?id=gd_temp__change_decision_record dec_rec: title: Decision Record diff --git a/src/extensions/score_metamodel/tests/rst/graph/test_inspection_report.rst b/src/extensions/score_metamodel/tests/rst/graph/test_inspection_report.rst new file mode 100644 index 000000000..57a356857 --- /dev/null +++ b/src/extensions/score_metamodel/tests/rst/graph/test_inspection_report.rst @@ -0,0 +1,60 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. mod:: Inspection Report Graph Module + :id: mod__inspection_report_graph_module + :security: YES + :safety: ASIL_B + :status: valid + +.. comp_req:: Inspection Report Graph Requirement + :id: comp_req__inspection_report_graph__sample + :reqtype: Functional + :security: YES + :safety: ASIL_B + :status: valid + +.. mod_insp:: Approved Requirements Inspection + :id: mod_insp__inspection_report_graph__requirements + :safety: ASIL_B + :security: YES + :status: valid + :inspection_type: requirements + :inspection_state: approved + :checklist_ref: gd_chklst__req_inspection + :reviewers: reviewer_a + :belongs_to: mod__inspection_report_graph_module + :inspects: comp_req__inspection_report_graph__sample + +#EXPECT[+2]: Inspection report is missing approved inspection(s) for: architecture + +.. mod_insp_report:: Incomplete Inspection Report + :id: mod_ispr__inspection_report_graph__incomplete + :safety: ASIL_B + :security: YES + :status: valid + :expected_inspections: requirements,architecture + :belongs_to: mod__inspection_report_graph_module + :contains: mod_insp__inspection_report_graph__requirements + +#EXPECT-NOT[+2]: Inspection report is missing approved inspection(s) + +.. mod_insp_report:: Complete Inspection Report + :id: mod_ispr__inspection_report_graph__complete + :safety: ASIL_B + :security: YES + :status: valid + :expected_inspections: requirements + :belongs_to: mod__inspection_report_graph_module + :contains: mod_insp__inspection_report_graph__requirements diff --git a/src/extensions/score_metamodel/tests/rst/options/test_options_inspection_report.rst b/src/extensions/score_metamodel/tests/rst/options/test_options_inspection_report.rst new file mode 100644 index 000000000..1f680e12f --- /dev/null +++ b/src/extensions/score_metamodel/tests/rst/options/test_options_inspection_report.rst @@ -0,0 +1,63 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +#CHECK: check_options + +.. mod:: Inspection Report Module + :id: mod__inspection_report_module + :security: YES + :safety: ASIL_B + :status: valid + +.. comp_req:: Inspection Report Requirement + :id: comp_req__inspection_report__sample + :reqtype: Functional + :security: YES + :safety: ASIL_B + :status: valid + +.. mod_insp:: Inspection Report Record + :id: mod_insp__inspection_report__requirements + :safety: ASIL_B + :security: YES + :status: valid + :inspection_type: requirements + :inspection_state: approved + :checklist_ref: gd_chklst__req_inspection + :reviewers: reviewer_a + :belongs_to: mod__inspection_report_module + :inspects: comp_req__inspection_report__sample + +#EXPECT-NOT[+2]: Inspection report is missing approved inspection(s) + +.. mod_insp_report:: Inspection Report Valid + :id: mod_ispr__inspection_report__valid + :safety: ASIL_B + :security: YES + :status: valid + :expected_inspections: requirements + :belongs_to: mod__inspection_report_module + :contains: mod_insp__inspection_report__requirements + +# Invalid expected_inspections value +#EXPECT[+2]: mod_ispr__inspection_report__invalid.expected_inspections (requirements,security): does not follow pattern + +.. mod_insp_report:: Inspection Report Invalid Expected Inspections + :id: mod_ispr__inspection_report__invalid + :safety: ASIL_B + :security: YES + :status: invalid + :expected_inspections: requirements,security + :belongs_to: mod__inspection_report_module + :contains: mod_insp__inspection_report__requirements diff --git a/src/extensions/score_metamodel/tests/test_graph_checks.py b/src/extensions/score_metamodel/tests/test_graph_checks.py index 064d6568f..5e2c32364 100644 --- a/src/extensions/score_metamodel/tests/test_graph_checks.py +++ b/src/extensions/score_metamodel/tests/test_graph_checks.py @@ -13,7 +13,7 @@ from __future__ import annotations -from typing import Any +from typing import cast import pytest @@ -21,16 +21,18 @@ import score_metamodel.checks.graph_checks as graph_checks from score_metamodel.tests import fake_check_logger, need as test_need from sphinx_needs.config import NeedType +from sphinx_needs.data import NeedsView +from sphinx_needs.need_item import NeedItem class DummyNeedsView: """Minimal NeedsView-like test double.""" - def __init__(self, needs: list[dict[str, Any]]) -> None: + def __init__(self, needs: list[NeedItem]) -> None: """Create a view over needs represented as dict-like structures.""" self._needs = needs - def values(self) -> list[dict[str, Any]]: + def values(self) -> list[NeedItem]: """Return all needs.""" return self._needs @@ -180,3 +182,65 @@ def test_filter_needs_by_criteria_unknown_type_logs_warning() -> None: log.assert_warning( "Unknown need type `unknown` in graph check.", expect_location=False ) + + +def test_check_inspection_report_completeness_warns_for_missing_approved_type() -> None: + """Warn when an expected inspection type is not approved and valid.""" + report = test_need( + id="mod_ispr__incomplete", + type="mod_insp_report", + expected_inspections="requirements,architecture", + contains=["mod_insp__requirements"], + ) + inspection = test_need( + id="mod_insp__requirements", + type="mod_insp", + inspection_type="requirements", + inspection_state="approved", + status="valid", + ) + log = fake_check_logger() + + graph_checks.check_inspection_report_completeness( + None, # type: ignore[arg-type] + cast(NeedsView, DummyNeedsView([report, inspection])), + log, + ) + + log.assert_warning("missing approved inspection(s) for: architecture") + + +def test_check_inspection_report_completeness_accepts_complete_report() -> None: + """Do not warn when all expected inspection types are approved and valid.""" + report = test_need( + id="mod_ispr__complete", + type="mod_insp_report", + expected_inspections="requirements,architecture", + contains=["mod_insp__requirements", "mod_insp__architecture"], + ) + requirements_inspection = test_need( + id="mod_insp__requirements", + type="mod_insp", + inspection_type="requirements", + inspection_state="approved", + status="valid", + ) + architecture_inspection = test_need( + id="mod_insp__architecture", + type="mod_insp", + inspection_type="architecture", + inspection_state="approved", + status="valid", + ) + log = fake_check_logger() + + graph_checks.check_inspection_report_completeness( + None, # type: ignore[arg-type] + cast( + NeedsView, + DummyNeedsView([report, requirements_inspection, architecture_inspection]), + ), + log, + ) + + log.assert_no_warnings()