From 063e357cdc83c783eccc2e6aa6c6dcfbccd7511a Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Tue, 23 Jun 2026 10:00:18 +0200 Subject: [PATCH 01/12] first draft --- src/extensions/score_metamodel/metamodel.yaml | 68 +++++++++++++++++++ .../tests/test_metamodel_load.py | 35 ++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 42d2c0a73..920824a03 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -919,6 +919,66 @@ needs_types: fully_verifies: ANY partially_verifies: ANY + mod_ver_report: + title: Module Verification Report + prefix: mod_vrep__ + mandatory_options: + safety: ^(QM|ASIL_B)$ + security: ^(YES|NO)$ + status: ^(valid|invalid)$ + verification_method: ^.*$ + optional_options: + requirements_coverage_percent: ^(100|[1-9]?[0-9])$ + structural_coverage_percent: ^(100|[1-9]?[0-9])$ + branch_coverage_percent: ^(100|[1-9]?[0-9])$ + verdict: ^(pass|fail|open)$ + report_version: ^.*$ + release_baseline: ^.*$ + mandatory_links: + belongs_to: mod + optional_links: + contains: ANY + evidence: ANY + covers: ANY + realizes: workproduct + tags: + - verification_report + parts: 3 + + # Formal inspection evidence modeled as a first-class artifact. + mod_insp: + title: Module Inspection Record + prefix: mod_insp__ + mandatory_options: + safety: ^(QM|ASIL_B)$ + security: ^(YES|NO)$ + status: ^(valid|invalid)$ + inspection_type: ^(requirements|architecture|implementation|traceability|safety_analysis|security_analysis|other)$ + inspection_state: ^(planned|in_review|rework_required|approved)$ + checklist_ref: ^.*$ + reviewers: ^.*$ + optional_options: + checklist_type: ^(req|arc|impl|safety|security|custom)$ + moderator: ^.*$ + approver: ^.*$ + findings_total: ^[0-9]+$ + findings_open: ^[0-9]+$ + pr_link: ^https://github\.com/[^/]+/[^/]+/pull/\d+$ + correction_issue: ^https://github\.com/[^/]+/[^/]+/issues/\d+$ + inspection_date: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ + mandatory_links: + belongs_to: mod + inspects: ANY + optional_links: + contains: ANY + evidence: ANY + approved_by: role + supported_by: role + tags: + - inspection + - verification_evidence + parts: 3 + # https://eclipse-score.github.io/process_description/main/permalink.html?id=gd_temp__change_decision_record dec_rec: title: Decision Record @@ -1052,6 +1112,14 @@ needs_extra_links: partially_verifies: incoming: partially_verified_by outgoing: partially_verifies + + evidence: + incoming: evidence_for + outgoing: evidence + + inspects: + incoming: inspected_by + outgoing: inspects ############################################################## # Graph Checks # The graph checks focus on the relation of the needs and their attributes. diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index e8aa0daa0..c23ce8509 100644 --- a/src/extensions/score_metamodel/tests/test_metamodel_load.py +++ b/src/extensions/score_metamodel/tests/test_metamodel_load.py @@ -10,6 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +import json from pathlib import Path from unittest.mock import mock_open, patch @@ -92,3 +93,37 @@ def test_load_metamodel_data(): assert defined_graph_check["check"] == { "link1": "opt1 == test", } + + +def test_default_metamodel_contains_generic_verification_and_inspection_types(): + """Default metamodel contains generic module verification and inspection types.""" + result = load_metamodel_data() + + needs_types = {need_type["directive"]: need_type for need_type in result.needs_types} + + assert "mod_ver_report" in needs_types + assert "mod_insp" in needs_types + + mod_ver_report = needs_types["mod_ver_report"] + assert mod_ver_report["mandatory_links_str"]["belongs_to"] == "mod" + assert mod_ver_report["optional_links_str"]["contains"] == "ANY" + assert mod_ver_report["optional_links_str"]["evidence"] == "ANY" + assert mod_ver_report["optional_links_str"]["covers"] == "ANY" + + mod_insp = needs_types["mod_insp"] + assert mod_insp["mandatory_links_str"]["inspects"] == "ANY" + assert mod_insp["optional_links_str"]["contains"] == "ANY" + assert mod_insp["optional_links_str"]["evidence"] == "ANY" + + assert "evidence" in result.needs_links + assert "inspects" in result.needs_links + + +def test_metamodel_schema_json_is_valid(): + """The metamodel JSON schema file must be syntactically valid JSON.""" + schema_path = Path(__file__).resolve().parent.parent / "metamodel-schema.json" + with open(schema_path, encoding="utf-8") as schema_file: + parsed = json.load(schema_file) + + assert isinstance(parsed, dict) + assert "$schema" in parsed From e9c48ce8364dc33a8412f858b0a693ba8155c032 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 24 Jun 2026 08:57:28 +0000 Subject: [PATCH 02/12] Addded needs to requirements and added test --- docs/internals/requirements/requirements.rst | 42 +++++++ src/extensions/score_metamodel/metamodel.yaml | 14 +++ .../test_options_verification_evidence.rst | 113 ++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst diff --git a/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index edfa719a4..bb47d4550 100644 --- a/docs/internals/requirements/requirements.rst +++ b/docs/internals/requirements/requirements.rst @@ -46,6 +46,7 @@ This section provides an overview of current process requirements and their clar Req, 'tool_req__docs' in id and implemented == "YES" and "Requirements" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Requirements" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Requirements" in tags and status == "valid", 'tool_req__docs' in id and "Requirements" in tags and status != "valid" Arch, 'tool_req__docs' in id and implemented == "YES" and "Architecture" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Architecture" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Architecture" in tags and status == "valid", 'tool_req__docs' in id and "Architecture" in tags and status != "valid" DDesign, 'tool_req__docs' in id and implemented == "YES" and "Detailed Design & Code" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Detailed Design & Code" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Detailed Design & Code" in tags and status == "valid", 'tool_req__docs' in id and "Detailed Design & Code" in tags and status != "valid" + Verif, 'tool_req__docs' in id and implemented == "YES" and "Verification Evidence" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Verification Evidence" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Verification Evidence" in tags and status == "valid", 'tool_req__docs' in id and "Verification Evidence" in tags and status != "valid" TVR, 'tool_req__docs' in id and implemented == "YES" and "Tool Verification Reports" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Tool Verification Reports" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Tool Verification Reports" in tags and status == "valid", 'tool_req__docs' in id and "Tool Verification Reports" in tags and status != "valid" Other, 'tool_req__docs' in id and implemented == "YES" and "Process / Other" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Process / Other" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Process / Other" in tags and status == "valid", 'tool_req__docs' in id and "Process / Other" in tags and status != "valid" SftyAn, 'tool_req__docs' in id and implemented == "YES" and "Safety Analysis" in tags and status == "valid", 'tool_req__docs' in id and implemented == "PARTIAL" and "Safety Analysis" in tags and status == "valid", 'tool_req__docs' in id and implemented == "NO" and "Safety Analysis" in tags and status == "valid", 'tool_req__docs' in id and "Safety Analysis" in tags and status != "valid" @@ -864,6 +865,47 @@ Testing Docs-AS-Code shall provide a way to gather statistics on linkages to implementation(source_code_links) & tests(testlink) for all needs. It shall also be possible to filter these by type and use the provided statistics in the documentation (via diagrams drawn from it etc.) +๐Ÿ”Ž Verification Evidence +######################## + +.. tool_req:: Support machine-readable module verification reports + :id: tool_req__docs_verification_report_need + :tags: Verification Evidence + :implemented: YES + :version: 1 + :satisfies: gd_req__verification_reporting + :parent_covered: NO: process wording is broader than the currently modeled report artifact. + + Docs-as-Code shall support a machine-readable module verification report need type. + + The need type shall: + + * use ``mod_ver_report`` as directive type + * classify the report by ``safety``, ``security``, ``status`` and ``verification_method`` + * link the report to the verified module via ``belongs_to`` + * allow links to contained verification evidence via ``contains`` + * allow links to covered artifacts via ``covers`` + * allow links to backing documents or work products via ``evidence`` and ``realizes`` + +.. tool_req:: Support machine-readable inspection records + :id: tool_req__docs_inspection_record_need + :tags: Verification Evidence + :implemented: YES + :version: 1 + :satisfies: gd_req__verification_checks + :parent_covered: NO: process wording defines verification checks, while the tool models a first-class inspection record artifact. + + Docs-as-Code shall support a machine-readable inspection record need type. + + The need type shall: + + * use ``mod_insp`` as directive type + * classify the inspection by ``inspection_type`` and ``inspection_state`` + * record the checklist reference and reviewer list via ``checklist_ref`` and ``reviewers`` + * link the inspection to the verified module via ``belongs_to`` + * link the inspected artifacts via ``inspects`` + * allow links to backing evidence via ``evidence`` + ๐Ÿงช Tool Verification Reports ############################ diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 920824a03..427ed52b4 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -919,13 +919,18 @@ needs_types: fully_verifies: ANY partially_verifies: ANY + # req-Id: tool_req__docs_verification_report_need mod_ver_report: title: Module Verification Report prefix: mod_vrep__ 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_verification_report_need verification_method: ^.*$ optional_options: requirements_coverage_percent: ^(100|[1-9]?[0-9])$ @@ -935,8 +940,10 @@ needs_types: report_version: ^.*$ release_baseline: ^.*$ mandatory_links: + # req-Id: tool_req__docs_verification_report_need belongs_to: mod optional_links: + # req-Id: tool_req__docs_verification_report_need contains: ANY evidence: ANY covers: ANY @@ -946,13 +953,18 @@ needs_types: parts: 3 # Formal inspection evidence modeled as a first-class artifact. + # req-Id: tool_req__docs_inspection_record_need mod_insp: title: Module Inspection Record prefix: mod_insp__ 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_record_need inspection_type: ^(requirements|architecture|implementation|traceability|safety_analysis|security_analysis|other)$ inspection_state: ^(planned|in_review|rework_required|approved)$ checklist_ref: ^.*$ @@ -967,9 +979,11 @@ needs_types: correction_issue: ^https://github\.com/[^/]+/[^/]+/issues/\d+$ inspection_date: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ mandatory_links: + # req-Id: tool_req__docs_inspection_record_need belongs_to: mod inspects: ANY optional_links: + # req-Id: tool_req__docs_inspection_record_need contains: ANY evidence: ANY approved_by: role diff --git a/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst b/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst new file mode 100644 index 000000000..14b069078 --- /dev/null +++ b/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst @@ -0,0 +1,113 @@ +.. + # ******************************************************************************* + # 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 + + +.. Base architecture and requirement objects used by verification evidence tests + +.. feat:: Verification Feature + :id: feat__verification_feature + :security: YES + :safety: ASIL_B + :status: valid + +.. comp:: Verification Component + :id: comp__verification_component + :security: YES + :safety: ASIL_B + :status: valid + :belongs_to: feat__verification_feature + +.. mod:: Verification Module + :id: mod__verification_module + :security: YES + :safety: ASIL_B + :status: valid + :includes: comp__verification_component + +.. comp_req:: Verification Requirement + :id: comp_req__verification__sample + :reqtype: Functional + :security: YES + :safety: ASIL_B + :status: valid + :content: Requirement text for verification evidence tests. + + +.. Valid machine-readable verification report need +#EXPECT-NOT[+2]: does not follow pattern + +.. mod_ver_report:: Verification Report Valid + :id: mod_vrep__verification__valid + :safety: ASIL_B + :security: YES + :status: valid + :verification_method: test_and_inspection + :requirements_coverage_percent: 95 + :structural_coverage_percent: 90 + :branch_coverage_percent: 85 + :verdict: pass + :report_version: 1.0.0 + :release_baseline: main + :belongs_to: mod__verification_module + :covers: comp_req__verification__sample + + +.. Invalid verdict value in module verification report +#EXPECT[+2]: mod_vrep__verification__bad_verdict.verdict (pending): does not follow pattern + +.. mod_ver_report:: Verification Report Invalid Verdict + :id: mod_vrep__verification__bad_verdict + :safety: ASIL_B + :security: YES + :status: invalid + :verification_method: inspection + :verdict: pending + :belongs_to: mod__verification_module + + +.. Valid machine-readable inspection record need +#EXPECT-NOT[+2]: does not follow pattern + +.. mod_insp:: Inspection Record Valid + :id: mod_insp__verification__valid + :safety: ASIL_B + :security: YES + :status: valid + :inspection_type: requirements + :inspection_state: approved + :checklist_ref: gd_chklst__req_inspection + :reviewers: reviewer_a,reviewer_b + :checklist_type: req + :findings_total: 1 + :findings_open: 0 + :inspection_date: 2026-06-24 + :belongs_to: mod__verification_module + :inspects: comp_req__verification__sample + + +.. Invalid inspection_state value in module inspection record +#EXPECT[+2]: mod_insp__verification__bad_state.inspection_state (approved_late): does not follow pattern + +.. mod_insp:: Inspection Record Invalid State + :id: mod_insp__verification__bad_state + :safety: ASIL_B + :security: YES + :status: invalid + :inspection_type: architecture + :inspection_state: approved_late + :checklist_ref: gd_chklst__arch_inspection_checklist + :reviewers: reviewer_a + :belongs_to: mod__verification_module + :inspects: comp_req__verification__sample From 9155fd68d557f811246c60eeea593e2f1c7fa9d2 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 24 Jun 2026 09:02:34 +0000 Subject: [PATCH 03/12] fixed lint --- src/extensions/score_metamodel/tests/test_metamodel_load.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index c23ce8509..77243eba1 100644 --- a/src/extensions/score_metamodel/tests/test_metamodel_load.py +++ b/src/extensions/score_metamodel/tests/test_metamodel_load.py @@ -99,7 +99,9 @@ def test_default_metamodel_contains_generic_verification_and_inspection_types(): """Default metamodel contains generic module verification and inspection types.""" result = load_metamodel_data() - needs_types = {need_type["directive"]: need_type for need_type in result.needs_types} + needs_types = { + need_type["directive"]: need_type for need_type in result.needs_types + } assert "mod_ver_report" in needs_types assert "mod_insp" in needs_types From 3db43c8908340d5d22f3f745731ee606cf84bad4 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:43:29 +0000 Subject: [PATCH 04/12] Add sphinx-needs filtering, Markdown and TRLC export rules (WIP playground) Add Bazel macros and Python tools to post-process needs.json: - filter_needs_json / component_requirements / feature_requirements / assumptions_of_use: extract a subset of sphinx-needs elements - sphinx_needs_to_md: render needs as a Markdown document - sphinx_needs_to_trlc: convert S-CORE requirements to TRLC WIP / demonstration only. --- docs.bzl | 223 +++++++++++++++++++++++ scripts_bazel/BUILD | 24 +++ scripts_bazel/README_needs_rules.md | 83 +++++++++ scripts_bazel/filter_needs_json.py | 166 ++++++++++++++++++ scripts_bazel/sphinx_needs_to_md.py | 173 ++++++++++++++++++ scripts_bazel/sphinx_needs_to_trlc.py | 244 ++++++++++++++++++++++++++ 6 files changed, 913 insertions(+) create mode 100644 scripts_bazel/README_needs_rules.md create mode 100644 scripts_bazel/filter_needs_json.py create mode 100644 scripts_bazel/sphinx_needs_to_md.py create mode 100644 scripts_bazel/sphinx_needs_to_trlc.py diff --git a/docs.bzl b/docs.bzl index 7f5ad0ced..e5fbc4f15 100644 --- a/docs.bzl +++ b/docs.bzl @@ -96,6 +96,229 @@ def _merge_sourcelinks(name, sourcelinks, known_good = None): tools = [merge_sourcelinks_tool], ) +def filtered_needs_json( + name, + src, + types = [], + components = [], + component_attr = "component", + visibility = None): + """Extract a subset of sphinx-needs elements from a needs.json file. + + Produces a `.json` file containing only the needs that match all of + the given filters. This is useful to hand a downstream consumer just the + elements (e.g. `feat_req`) of one or more particular components. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output (a directory containing + `needs.json`), e.g. `":needs_json"` or `"@score_process//:needs_json"`. + types: Optional list of sphinx-needs element types to keep + (e.g. `["feat_req", "comp_req"]`). If empty, all types are kept. + components: Optional list of component names to keep. If empty, all + components are kept. + component_attr: Need attribute matched against `components`. + Defaults to `"component"`. + visibility: Standard Bazel visibility for the generated target. + """ + filter_tool = Label("//scripts_bazel:filter_needs_json") + + type_args = " ".join(["--type '%s'" % t for t in types]) + component_args = " ".join(["--component '%s'" % c for c in components]) + + native.genrule( + name = name, + srcs = [src], + outs = [name + ".json"], + cmd = """ + $(location {filter_tool}) \ + --output $@ \ + --component-attr '{component_attr}' \ + {type_args} \ + {component_args} \ + $(location {src})/needs.json + """.format( + filter_tool = filter_tool, + component_attr = component_attr, + type_args = type_args, + component_args = component_args, + src = src, + ), + tools = [filter_tool], + visibility = visibility, + ) + +def component_requirements( + name, + src = "//:needs_json", + component = None, + visibility = None): + """Extract the component requirements (`comp_req`) from a needs.json file. + + Convenience wrapper around `filtered_needs_json`. Produces a `.json` + file containing only `comp_req` elements. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output. Defaults to the calling + package's `//:needs_json`. + component: Optional component name. If given, only component requirements + tagged with that component are kept; if omitted, all component + requirements are kept. + visibility: Standard Bazel visibility for the generated target. + """ + filtered_needs_json( + name = name, + src = src, + types = ["comp_req"], + components = [component] if component else [], + component_attr = "tags", + visibility = visibility, + ) + +def feature_requirements( + name, + src = "//:needs_json", + feature = None, + visibility = None): + """Extract the feature requirements (`feat_req`) from a needs.json file. + + Convenience wrapper around `filtered_needs_json`. Produces a `.json` + file containing only `feat_req` elements. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output. Defaults to the calling + package's `//:needs_json`. + feature: Optional feature name. If given, only feature requirements + tagged with that feature are kept; if omitted, all feature + requirements are kept. + visibility: Standard Bazel visibility for the generated target. + """ + filtered_needs_json( + name = name, + src = src, + types = ["feat_req"], + components = [feature] if feature else [], + component_attr = "tags", + visibility = visibility, + ) + +def assumptions_of_use( + name, + src = "//:needs_json", + component = None, + visibility = None): + """Extract the assumptions of use (`aou_req`) from a needs.json file. + + Convenience wrapper around `filtered_needs_json`. Produces a `.json` + file containing only `aou_req` elements. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output. Defaults to the calling + package's `//:needs_json`. + component: Optional component name. If given, only assumptions of use + tagged with that component are kept; if omitted, all assumptions of + use are kept. + visibility: Standard Bazel visibility for the generated target. + """ + filtered_needs_json( + name = name, + src = src, + types = ["aou_req"], + components = [component] if component else [], + component_attr = "tags", + visibility = visibility, + ) + +def sphinx_needs_to_md( + name, + src, + title = "Sphinx-needs elements", + visibility = None): + """Render the sphinx-needs elements of a needs.json file as a Markdown document. + + Produces a `.md` file containing a human readable description of every + sphinx-needs element found in `src`. Typically `src` is the output of a + `filtered_needs_json` target, but any `needs.json`-style file works. + + Args: + name: Name of the generated target. The output file is `.md`. + src: Label of a needs.json file, e.g. a `filtered_needs_json` target + (`":my_feat_reqs"`) or a `needs_json` directory output. + title: Title rendered at the top of the generated document. + visibility: Standard Bazel visibility for the generated target. + """ + sphinx_needs_to_md_tool = Label("//scripts_bazel:sphinx_needs_to_md") + + native.genrule( + name = name, + srcs = [src], + outs = [name + ".md"], + cmd = """ + $(location {sphinx_needs_to_md_tool}) \ + --output $@ \ + --title '{title}' \ + $(location {src}) + """.format( + sphinx_needs_to_md_tool = sphinx_needs_to_md_tool, + title = title, + src = src, + ), + tools = [sphinx_needs_to_md_tool], + visibility = visibility, + ) + +def sphinx_needs_to_trlc( + name, + src, + package = "Needs", + visibility = None): + """Convert the requirement sphinx-needs elements of a needs.json file into TRLC. + + TRLC ("Treat Requirements Like Code", + https://github.com/bmw-software-engineering/trlc) is requirements-only tooling. + Only the S-CORE requirement element types are converted; everything else is + ignored: + + * `feat_req` -> `ScoreReq.FeatReq` (feature requirement) + * `comp_req` -> `ScoreReq.CompReq` (component requirement) + * `aou_req` -> `ScoreReq.AoU` (assumption of use) + + Produces a `.trlc` data file in package `package` targeting the S-CORE + requirements metamodel (package `ScoreReq`) from + https://github.com/eclipse-score/tooling/tree/main/bazel/rules/rules_score/trlc. + Validate the output together with that metamodel (e.g. via `trlc_requirements` + using `score_requirements_model` as `spec`). + + Args: + name: Name of the generated target. The output file is `.trlc`. + src: Label of a needs.json file, e.g. a `filtered_needs_json` target + (`":my_feat_reqs"`) or a `needs_json` directory output. + package: TRLC package name used for the generated requirements. + visibility: Standard Bazel visibility for the generated target. + """ + sphinx_needs_to_trlc_tool = Label("//scripts_bazel:sphinx_needs_to_trlc") + + native.genrule( + name = name, + srcs = [src], + outs = [name + ".trlc"], + cmd = """ + $(location {sphinx_needs_to_trlc_tool}) \ + --output $@ \ + --package '{package}' \ + $(location {src}) + """.format( + sphinx_needs_to_trlc_tool = sphinx_needs_to_trlc_tool, + package = package, + src = src, + ), + tools = [sphinx_needs_to_trlc_tool], + visibility = visibility, + ) + def _missing_requirements(deps): """Add Python hub dependencies if they are missing.""" found = [] diff --git a/scripts_bazel/BUILD b/scripts_bazel/BUILD index e2d0402d2..50eac7eb3 100644 --- a/scripts_bazel/BUILD +++ b/scripts_bazel/BUILD @@ -45,3 +45,27 @@ py_binary( visibility = ["//visibility:public"], deps = [], ) + +py_binary( + name = "filter_needs_json", + srcs = ["filter_needs_json.py"], + main = "filter_needs_json.py", + visibility = ["//visibility:public"], + deps = [], +) + +py_binary( + name = "sphinx_needs_to_md", + srcs = ["sphinx_needs_to_md.py"], + main = "sphinx_needs_to_md.py", + visibility = ["//visibility:public"], + deps = [], +) + +py_binary( + name = "sphinx_needs_to_trlc", + srcs = ["sphinx_needs_to_trlc.py"], + main = "sphinx_needs_to_trlc.py", + visibility = ["//visibility:public"], + deps = [], +) diff --git a/scripts_bazel/README_needs_rules.md b/scripts_bazel/README_needs_rules.md new file mode 100644 index 000000000..b3f6ae5c8 --- /dev/null +++ b/scripts_bazel/README_needs_rules.md @@ -0,0 +1,83 @@ +# Sphinx-needs processing rules (playground) + +> **WIP / demonstration only.** This branch (`ankr_rules_score_playground`) is a +> proof of concept to explore how `rules_score` could post-process sphinx-needs +> data. It is **not** production ready and is shared for demonstration purposes. + +## Why these changes + +The documentation build already produces a `needs.json` file that contains every +sphinx-needs element (requirements, assumptions of use, ...). Downstream tooling +often needs only a *subset* of that data, or the data in a *different format*. + +This change adds a small set of Bazel macros (in [docs.bzl](../docs.bzl)) plus +the backing Python tools (in this folder) to: + +1. **Filter** a `needs.json` down to selected element types and/or components. +2. **Render** the selected elements as a human readable Markdown document. +3. **Convert** S-CORE requirement elements into [TRLC](https://github.com/bmw-software-engineering/trlc) + data targeting the S-CORE requirements metamodel. + +The goal is to show how requirements managed as sphinx-needs can be bridged to +other consumers (review docs, TRLC-based tooling) without manual copying. + +## What was added + +### Bazel macros (`docs.bzl`) + +| Macro | Output | Purpose | +| --- | --- | --- | +| `filtered_needs_json` | `.json` | Keep only needs matching the given `types` / `components`. | +| `component_requirements` | `.json` | Convenience wrapper for `comp_req` elements. | +| `feature_requirements` | `.json` | Convenience wrapper for `feat_req` elements. | +| `assumptions_of_use` | `.json` | Convenience wrapper for `aou_req` elements. | +| `sphinx_needs_to_md` | `.md` | Render needs as a Markdown document. | +| `sphinx_needs_to_trlc` | `.trlc` | Convert S-CORE requirements to TRLC. | + +### Python tools (`scripts_bazel/`) + +- [filter_needs_json.py](filter_needs_json.py) โ€” extract a subset of needs. +- [sphinx_needs_to_md.py](sphinx_needs_to_md.py) โ€” render needs as Markdown. +- [sphinx_needs_to_trlc.py](sphinx_needs_to_trlc.py) โ€” convert needs to TRLC. + +The matching `py_binary` targets are declared in [BUILD](BUILD). + +## How to use + +In a `BUILD` file that already has a `needs_json` target, load the macros and +chain them: + +```starlark +load("@docs-as-code//:docs.bzl", "feature_requirements", "sphinx_needs_to_md", "sphinx_needs_to_trlc") + +# 1. Filter: keep only the feature requirements of one feature. +feature_requirements( + name = "my_feature_reqs", + src = "//:needs_json", + feature = "my_feature", +) + +# 2. Render the filtered set as Markdown. +sphinx_needs_to_md( + name = "my_feature_reqs_md", + src = ":my_feature_reqs", + title = "My feature requirements", +) + +# 3. Convert the filtered set to TRLC. +sphinx_needs_to_trlc( + name = "my_feature_reqs_trlc", + src = ":my_feature_reqs", + package = "MyFeature", +) +``` + +Build any of the targets to produce the corresponding output file: + +```bash +bazel build //path/to:my_feature_reqs_md +bazel build //path/to:my_feature_reqs_trlc +``` + +You can also call `filtered_needs_json` directly for full control over the +`types`, `components`, and `component_attr` filters. diff --git a/scripts_bazel/filter_needs_json.py b/scripts_bazel/filter_needs_json.py new file mode 100644 index 000000000..c078afafd --- /dev/null +++ b/scripts_bazel/filter_needs_json.py @@ -0,0 +1,166 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Extract a subset of sphinx-needs elements from a needs.json file. + +A need is kept when it matches *all* of the active filters: + +* ``--type``: the value of the need's ``type`` attribute is in the requested + list of element types (e.g. ``feat_req``). If no ``--type`` is given, needs + of any type are kept. +* ``--component``: the value of the need's component attribute (configurable + via ``--component-attr``, default ``component``) matches one of the requested + component names. The attribute may hold a single string or a list of strings; + a need is kept when any of its values matches. If no ``--component`` is given, + needs of any component are kept. + +The top-level structure of the needs.json file is preserved; only the per-need +entries are filtered. +""" + +import argparse +import json +import logging +import sys +from pathlib import Path +from typing import Any + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def _attribute_values(need: dict[str, Any], attr: str) -> list[str]: + """Return the values of ``attr`` on a need as a list of strings.""" + value = need.get(attr) + if value is None: + return [] + if isinstance(value, list): + return [str(v) for v in value] # pyright: ignore[reportUnknownVariableType] + return [str(value)] + + +def _keep_need( + need: dict[str, Any], + types: set[str], + components: set[str], + component_attr: str, +) -> bool: + if types and need.get("type") not in types: + return False + if components: + values = set(_attribute_values(need, component_attr)) + if values.isdisjoint(components): + return False + return True + + +def filter_needs( + data: dict[str, Any], + types: set[str], + components: set[str], + component_attr: str, +) -> dict[str, Any]: + """Return a copy of ``data`` keeping only the needs that match the filters.""" + for version in data.get("versions", {}).values(): + needs = version.get("needs", {}) + version["needs"] = { + need_id: need + for need_id, need in needs.items() + if _keep_need(need, types, components, component_attr) + } + return data + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Extract a subset of sphinx-needs elements from a needs.json file." + ) + ) + _ = parser.add_argument( + "--output", + required=True, + type=Path, + help="Path of the filtered needs.json file to write.", + ) + _ = parser.add_argument( + "--type", + dest="types", + action="append", + default=[], + metavar="ELEMENT_TYPE", + help=( + "Sphinx-needs element type to keep (e.g. 'feat_req'). " + "May be given multiple times. If omitted, all types are kept." + ), + ) + _ = parser.add_argument( + "--component", + dest="components", + action="append", + default=[], + metavar="COMPONENT", + help=( + "Component name to keep. May be given multiple times. " + "If omitted, all components are kept." + ), + ) + _ = parser.add_argument( + "--component-attr", + default="component", + help=( + "Need attribute matched against the values given via --component. " + "Defaults to 'component'." + ), + ) + _ = parser.add_argument( + "input", + type=Path, + help="Input needs.json file to filter.", + ) + + args = parser.parse_args() + + with open(args.input) as f: + data = json.load(f) + + filtered = filter_needs( + data, + types=set(args.types), + components=set(args.components), + component_attr=args.component_attr, + ) + + kept = sum( + len(version.get("needs", {})) + for version in filtered.get("versions", {}).values() + ) + logger.info( + "Filtered '%s' -> '%s' (%d needs kept, types=%s, components=%s)", + args.input, + args.output, + kept, + sorted(args.types) or "ALL", + sorted(args.components) or "ALL", + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with open(args.output, "w") as f: + json.dump(filtered, f, indent=2, sort_keys=True) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts_bazel/sphinx_needs_to_md.py b/scripts_bazel/sphinx_needs_to_md.py new file mode 100644 index 000000000..e71a1c55c --- /dev/null +++ b/scripts_bazel/sphinx_needs_to_md.py @@ -0,0 +1,173 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Render the sphinx-needs elements of a needs.json file as a Markdown document. + +The input is a needs.json file (typically the output of ``filtered_needs_json``). +Each need is rendered as a Markdown section. Needs are grouped by their ``type`` +and sorted by ``id`` so the output is stable and diff friendly. +""" + +import argparse +import json +import logging +import sys +from pathlib import Path +from typing import Any + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# Attributes rendered (in this order) as a table for every need. +# Only attributes that are present and non-empty on a need are shown. +_HEADER_ATTRS: list[tuple[str, str]] = [ + ("title", "Title"), + ("type_name", "Type name"), + ("status", "Status"), + ("safety", "Safety"), + ("security", "Security"), + ("reqtype", "Requirement type"), + ("tags", "Tags"), + ("docname", "Document"), +] + + +def _format_value(value: object) -> str: + """Render an attribute value as a single line Markdown-safe string.""" + text = ( + ", ".join(str(v) for v in value) # pyright: ignore[reportUnknownVariableType] + if isinstance(value, list) + else str(value) + ) + # Escape the Markdown table cell separator. + return text.replace("|", "\\|") + + +def _collect_needs(data: dict[str, Any]) -> list[dict[str, Any]]: + """Return all needs across every version, sorted by id.""" + needs: list[dict[str, Any]] = [] + for version in data.get("versions", {}).values(): + needs.extend(version.get("needs", {}).values()) + return sorted(needs, key=lambda need: str(need.get("id", ""))) + + +def _render_need(need: dict[str, Any]) -> str: + """Render a single need as a Markdown block.""" + lines: list[str] = [] + need_id = str(need.get("id", "")) + lines.append(f"### `{need_id}`") + lines.append("") + + rows = [ + (label, _format_value(value)) + for attr, label in _HEADER_ATTRS + if (value := need.get(attr)) not in (None, "", []) + ] + if rows: + lines.append("| Attribute | Value |") + lines.append("| --- | --- |") + for label, value in rows: + lines.append(f"| {label} | {value} |") + lines.append("") + + content = str(need.get("content", "")).strip() + if content: + lines.append("**Content:**") + lines.append("") + lines.append("```") + lines.extend(content.splitlines()) + lines.append("```") + return "\n".join(lines).rstrip() + + +def render_document(data: dict[str, Any], title: str) -> str: + """Render the whole needs document as Markdown.""" + needs = _collect_needs(data) + types: dict[str, int] = {} + for need in needs: + type_ = str(need.get("type", "")) + types[type_] = types.get(type_, 0) + 1 + + blocks: list[str] = [] + blocks.append(f"# {title}") + blocks.append(f"Total needs: **{len(needs)}**") + + if types: + summary_lines = ["| Type | Count |", "| --- | --- |"] + summary_lines.extend( + f"| `{type_}` | {count} |" for type_, count in sorted(types.items()) + ) + blocks.append("\n".join(summary_lines)) + + current_type = None + for need in sorted( + needs, key=lambda n: (str(n.get("type", "")), str(n.get("id", ""))) + ): + type_ = str(need.get("type", "")) + if type_ != current_type: + current_type = type_ + blocks.append(f"## `{type_}`") + blocks.append(_render_need(need)) + + return "\n\n".join(blocks) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Render the sphinx-needs elements of a needs.json file as Markdown." + ) + ) + _ = parser.add_argument( + "--output", + required=True, + type=Path, + help="Path of the Markdown file to write.", + ) + _ = parser.add_argument( + "--title", + default="Sphinx-needs elements", + help="Title rendered at the top of the document.", + ) + _ = parser.add_argument( + "input", + type=Path, + help="Input needs.json file to document.", + ) + + args = parser.parse_args() + + with open(args.input) as f: + data = json.load(f) + + document = render_document(data, title=args.title) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with open(args.output, "w") as f: + _ = f.write(document) + + need_count = sum( + len(version.get("needs", {})) for version in data.get("versions", {}).values() + ) + logger.info( + "Documented '%s' -> '%s' (%d needs)", + args.input, + args.output, + need_count, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts_bazel/sphinx_needs_to_trlc.py b/scripts_bazel/sphinx_needs_to_trlc.py new file mode 100644 index 000000000..5eda3f652 --- /dev/null +++ b/scripts_bazel/sphinx_needs_to_trlc.py @@ -0,0 +1,244 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Convert the requirement sphinx-needs elements of a needs.json file into TRLC. + +TRLC ("Treat Requirements Like Code", https://github.com/bmw-software-engineering/trlc) +is a requirements-only tooling. Therefore only the S-CORE requirement element +types are converted: + +* ``feat_req`` -> ``ScoreReq.FeatReq`` (feature requirement) +* ``comp_req`` -> ``ScoreReq.CompReq`` (component requirement) +* ``aou_req`` -> ``ScoreReq.AoU`` (assumption of use) + +All other sphinx-needs elements are ignored. + +The generated ``.trlc`` data file targets the S-CORE requirements metamodel +(package ``ScoreReq``) defined in +https://github.com/eclipse-score/tooling/tree/main/bazel/rules/rules_score/trlc +and must be validated together with that metamodel. +""" + +import argparse +import json +import logging +import re +import sys +from pathlib import Path +from typing import Any + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# sphinx-needs type -> ScoreReq metamodel type. +_TYPE_MAP: dict[str, str] = { + "feat_req": "FeatReq", + "comp_req": "CompReq", + "aou_req": "AoU", +} + +# ScoreReq type -> the metamodel type its ``derived_from`` references, if any. +# Only references that resolve to an emitted object of this type are rendered. +_DERIVED_FROM_TARGET: dict[str, str] = { + "FeatReq": "AssumedSystemReq", + "CompReq": "FeatReq", +} + + +def _string_literal(value: str) -> str: + """Return a TRLC string literal for ``value``.""" + text = str(value) + if "\n" in text: + if '"""' not in text: + return f'"""\n{text}\n"""' + if "'''" not in text: + return f"'''\n{text}\n'''" + return '"""\n{text}\n"""'.format(text=text.replace('"""', '\\"\\"\\"')) + return '"{text}"'.format(text=text.replace('"', '\\"')) + + +def _asil(value: object) -> str: + """Map a sphinx-needs safety value to a ScoreReq.Asil enum literal.""" + text = str(value or "").upper().replace("ASIL", "").strip(" _-") + if text == "B": + return "ScoreReq.Asil.B" + if text == "D": + return "ScoreReq.Asil.D" + return "ScoreReq.Asil.QM" + + +def _version(need: dict[str, Any]) -> int: + """Return the integer version of a need, defaulting to 1.""" + try: + return int(need.get("version", 1)) + except (TypeError, ValueError): + return 1 + + +def _identifier(raw: str, used: set[str]) -> str: + """Turn an arbitrary need id into a unique, valid TRLC identifier.""" + candidate = re.sub(r"[^A-Za-z0-9_]", "_", str(raw)) + if not candidate or not (candidate[0].isalpha() or candidate[0] == "_"): + candidate = "n_" + candidate + unique = candidate + suffix = 2 + while unique in used: + unique = f"{candidate}_{suffix}" + suffix += 1 + used.add(unique) + return unique + + +def _collect_requirement_needs(data: dict[str, Any]) -> list[dict[str, Any]]: + """Return all needs whose type maps to a ScoreReq requirement, sorted.""" + needs: list[dict[str, Any]] = [] + for version in data.get("versions", {}).values(): + for need in version.get("needs", {}).values(): + if need.get("type") in _TYPE_MAP: + needs.append(need) + needs.sort(key=lambda n: (str(n.get("type", "")), str(n.get("id", "")))) + return needs + + +def _build_id_map(needs: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Map original need id -> {ident, score_type, version} for every need.""" + id_map: dict[str, dict[str, Any]] = {} + used: set[str] = set() + for need in needs: + original = str(need.get("id", "need")) + id_map[original] = { + "ident": _identifier(original, used), + "score_type": _TYPE_MAP[need["type"]], + "version": _version(need), + } + return id_map + + +def _render_object(need: dict[str, Any], id_map: dict[str, dict[str, Any]]) -> str: + """Render a single requirement need as a ScoreReq object.""" + info = id_map[str(need.get("id"))] + score_type = info["score_type"] + description = str(need.get("content") or need.get("title") or info["ident"]) + + lines = [ + "ScoreReq.{score_type} {ident} {{".format( + score_type=score_type, ident=info["ident"] + ), + f" description = {_string_literal(description)}", + f" version = {_version(need)}", + " safety = {value}".format(value=_asil(need.get("safety"))), + ] + + target_type = _DERIVED_FROM_TARGET.get(score_type) + if target_type: + refs: list[str] = [] + for ref in need.get("derived_from") or []: # pyright: ignore[reportUnknownVariableType] + ref_info = id_map.get(str(ref)) + if ref_info and ref_info["score_type"] == target_type: + refs.append( + "{ident} @ {version}".format( + ident=ref_info["ident"], version=ref_info["version"] + ) + ) + if refs: + lines.append(" derived_from = [{refs}]".format(refs=", ".join(refs))) + + lines.append("}") + return "\n".join(lines) + + +def render_trlc(data: dict[str, Any], package: str) -> str: + """Render the requirements data file (``.trlc``).""" + needs = _collect_requirement_needs(data) + id_map = _build_id_map(needs) + + blocks = [ + f"package {package}", + "", + "import ScoreReq", + ] + + body: list[str] = [] + current_type = None + section_open = False + for need in needs: + type_ = str(need.get("type")) + if type_ != current_type: + if section_open: + body.append("}") + current_type = type_ + body.append("") + body.append(f"section {_string_literal(type_)} {{") + section_open = True + obj = _render_object(need, id_map) + body.append("\n".join(" " + line for line in obj.splitlines())) + if section_open: + body.append("}") + + blocks.append("\n".join(body)) + return "\n".join(blocks).rstrip() + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Convert the requirement sphinx-needs elements of a needs.json file " + "into TRLC targeting the S-CORE requirements metamodel (ScoreReq)." + ), + ) + _ = parser.add_argument( + "--output", + required=True, + type=Path, + help="Path of the TRLC data file (.trlc) to write.", + ) + _ = parser.add_argument( + "--package", + default="Needs", + help="TRLC package name used for the generated requirements.", + ) + _ = parser.add_argument( + "input", + type=Path, + help="Input needs.json file to convert.", + ) + + args = parser.parse_args() + + package = re.sub(r"[^A-Za-z0-9_]", "_", args.package) + if not package or not (package[0].isalpha() or package[0] == "_"): + package = "Needs" + + with open(args.input) as f: + data = json.load(f) + + objects = render_trlc(data, package=package) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with open(args.output, "w") as f: + _ = f.write(objects) + + converted = len(_collect_requirement_needs(data)) + logger.info( + "Converted '%s' -> '%s' (%d requirements, package '%s')", + args.input, + args.output, + converted, + package, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 52fb989f2cd7b9cae53cac0b958389e55f469d2d Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:52:38 +0000 Subject: [PATCH 05/12] Add requirement checklist need type and validation rule (WIP playground) Adds a 'req_chklst' sphinx-needs type that pins the reviewed state of build outputs via a SHA256 hash, plus a 'requirements_checklist' Bazel rule and 'validate_checklist.py' tool that recompute the hash over the validated target outputs and fail the build when it drifts. --- docs.bzl | 85 ++++++++++- scripts_bazel/BUILD | 10 +- scripts_bazel/README_needs_rules.md | 64 ++++++++ scripts_bazel/validate_checklist.py | 139 ++++++++++++++++++ src/extensions/score_metamodel/metamodel.yaml | 29 ++++ 5 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 scripts_bazel/validate_checklist.py diff --git a/docs.bzl b/docs.bzl index e5fbc4f15..0388f7748 100644 --- a/docs.bzl +++ b/docs.bzl @@ -63,8 +63,10 @@ def _rewrite_needs_json_to_sourcelinks(labels): s = str(x) if s.endswith("//:needs_json"): out.append(s.replace("//:needs_json", "//:sourcelinks_json")) + #Items which do not end up with '//:needs_json' shall not be appended to 'out'. #They are treated separately and are not related to source code linking. + return out def _merge_sourcelinks(name, sourcelinks, known_good = None): @@ -319,19 +321,90 @@ def sphinx_needs_to_trlc( visibility = visibility, ) +def requirements_checklist( + name, + checklist_id, + deps, + src = "//:needs_json", + visibility = None): + """Validate a requirement checklist (`req_chklst`) against its build output. + + Building this target recomputes the SHA256 over the concatenated outputs of + `deps` and compares it to the `sha256` attribute of the `req_chklst` need + `checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when + the hashes differ, i.e. when a validated target output has changed since the + checklist was last reviewed. + + Typical usage validates the extracted requirements of a component against the + checklist that reviewed them: + + component_requirements( + name = "bitmanipulation_comp_reqs", + component = "bitmanipulation", + ) + + requirements_checklist( + name = "bitmanipulation_req_checklist", + checklist_id = "req_chklst__bitmanipulation__comp_req", + deps = [":bitmanipulation_comp_reqs"], + ) + + Run with `bazel build //:bitmanipulation_req_checklist`. On the first run (or + after the requirements change) the build fails and prints the actual SHA256; + copy it into the `sha256` attribute of the checklist need once the checklist + has been (re-)reviewed. + + Args: + name: Name of the generated target. The output file is `.sha256`. + checklist_id: Id of the `req_chklst` need to validate + (e.g. `"req_chklst__bitmanipulation__comp_req"`). + deps: List of labels whose outputs are hashed and validated. Usually a + single `component_requirements`/`filtered_needs_json` target. + src: Label of a `needs_json` build output containing the checklist need. + Defaults to the calling package's `//:needs_json`. + visibility: Standard Bazel visibility for the generated target. + """ + validate_tool = Label("//scripts_bazel:validate_checklist") + + dep_args = " ".join(["$(locations %s)" % d for d in deps]) + + native.genrule( + name = name, + srcs = [src] + deps, + outs = [name + ".sha256"], + cmd = """ + $(location {validate_tool}) \ + --needs-json $(location {src})/needs.json \ + --checklist-id '{checklist_id}' \ + --output $@ \ + {dep_args} + """.format( + validate_tool = validate_tool, + checklist_id = checklist_id, + src = src, + dep_args = dep_args, + ), + tools = [validate_tool], + visibility = visibility, + ) + def _missing_requirements(deps): """Add Python hub dependencies if they are missing.""" found = [] missing = [] + def _target_to_packagename(target): return str(target).split("/")[-1].split(":")[0] + all_packages = [_target_to_packagename(pkg) for pkg in all_requirements] + def _find(pkg): for dep in deps: dep_pkg = _target_to_packagename(dep) if dep_pkg == pkg: return True return False + for pkg in all_packages: if _find(pkg): found.append(pkg) @@ -453,7 +526,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = docs_data, deps = deps, - env = docs_env + env = docs_env, ) docs_sources_env["ACTION"] = "incremental" @@ -463,7 +536,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = combo_data, deps = deps, - env = docs_sources_env + env = docs_sources_env, ) native.alias( @@ -479,7 +552,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = docs_data, deps = deps, - env = docs_env + env = docs_env, ) docs_env["ACTION"] = "check" @@ -489,7 +562,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = docs_data, deps = deps, - env = docs_env + env = docs_env, ) docs_env["ACTION"] = "live_preview" @@ -499,7 +572,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = docs_data, deps = deps, - env = docs_env + env = docs_env, ) docs_sources_env["ACTION"] = "live_preview" @@ -509,7 +582,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = srcs = [incremental_src], data = combo_data, deps = deps, - env = docs_sources_env + env = docs_sources_env, ) py_venv( diff --git a/scripts_bazel/BUILD b/scripts_bazel/BUILD index 50eac7eb3..2ecb3199f 100644 --- a/scripts_bazel/BUILD +++ b/scripts_bazel/BUILD @@ -33,9 +33,9 @@ py_binary( py_binary( name = "merge_sourcelinks", srcs = ["merge_sourcelinks.py"], - deps= [ "//src/extensions/score_source_code_linker"], main = "merge_sourcelinks.py", visibility = ["//visibility:public"], + deps = ["//src/extensions/score_source_code_linker"], ) py_binary( @@ -69,3 +69,11 @@ py_binary( visibility = ["//visibility:public"], deps = [], ) + +py_binary( + name = "validate_checklist", + srcs = ["validate_checklist.py"], + main = "validate_checklist.py", + visibility = ["//visibility:public"], + deps = [], +) diff --git a/scripts_bazel/README_needs_rules.md b/scripts_bazel/README_needs_rules.md index b3f6ae5c8..9abaf9a1d 100644 --- a/scripts_bazel/README_needs_rules.md +++ b/scripts_bazel/README_needs_rules.md @@ -17,6 +17,9 @@ the backing Python tools (in this folder) to: 2. **Render** the selected elements as a human readable Markdown document. 3. **Convert** S-CORE requirement elements into [TRLC](https://github.com/bmw-software-engineering/trlc) data targeting the S-CORE requirements metamodel. +4. **Validate** a reviewable *requirement checklist* against the build output it + was reviewed against, by pinning a SHA256 hash in a `req_chklst` sphinx-needs + element and failing the build when the output drifts. The goal is to show how requirements managed as sphinx-needs can be bridged to other consumers (review docs, TRLC-based tooling) without manual copying. @@ -33,15 +36,25 @@ other consumers (review docs, TRLC-based tooling) without manual copying. | `assumptions_of_use` | `.json` | Convenience wrapper for `aou_req` elements. | | `sphinx_needs_to_md` | `.md` | Render needs as a Markdown document. | | `sphinx_needs_to_trlc` | `.trlc` | Convert S-CORE requirements to TRLC. | +| `requirements_checklist` | `.sha256` | Validate a `req_chklst` need against its target output via SHA256. | ### Python tools (`scripts_bazel/`) - [filter_needs_json.py](filter_needs_json.py) โ€” extract a subset of needs. - [sphinx_needs_to_md.py](sphinx_needs_to_md.py) โ€” render needs as Markdown. - [sphinx_needs_to_trlc.py](sphinx_needs_to_trlc.py) โ€” convert needs to TRLC. +- [validate_checklist.py](validate_checklist.py) โ€” validate a checklist hash. The matching `py_binary` targets are declared in [BUILD](BUILD). +### Metamodel + +A new `req_chklst` need type is added in +[metamodel.yaml](../src/extensions/score_metamodel/metamodel.yaml). It carries a +mandatory `sha256` attribute, an optional `targets` attribute (the Bazel labels +it validates), and an optional `checklist` link to the rendered checklist +document. + ## How to use In a `BUILD` file that already has a `needs_json` target, load the macros and @@ -81,3 +94,54 @@ bazel build //path/to:my_feature_reqs_trlc You can also call `filtered_needs_json` directly for full control over the `types`, `components`, and `component_attr` filters. + +## Requirement checklists + +A *requirement checklist* couples a human review (a checklist `.rst` document) +with the exact build output that was reviewed. The state of that output is +pinned via a SHA256 hash stored on a `req_chklst` sphinx-needs element. When the +output later changes, the checklist is considered stale and the build fails +until the checklist is re-reviewed and the hash updated. + +### 1. Declare the checklist need + +Add a `req_chklst` element (e.g. next to the checklist `.rst`). It references the +checklist document, the validated Bazel target(s), and the expected hash: + +```rst +.. req_chklst:: Bitmanipulation Component Requirements Checklist + :id: req_chklst__bitmanipulation__comp_req + :status: valid + :checklist: doc__bitmanipulation_req_inspection + :targets: //:bitmanipulation_comp_reqs + :sha256: 0000000000000000000000000000000000000000000000000000000000000000 +``` + +### 2. Declare the validation target + +```starlark +load("@docs-as-code//:docs.bzl", "component_requirements", "requirements_checklist") + +component_requirements( + name = "bitmanipulation_comp_reqs", + component = "bitmanipulation", +) + +requirements_checklist( + name = "bitmanipulation_req_checklist", + checklist_id = "req_chklst__bitmanipulation__comp_req", + deps = [":bitmanipulation_comp_reqs"], +) +``` + +### 3. Validate + +```bash +bazel build //:bitmanipulation_req_checklist +``` + +The build hashes the `deps` output and compares it to the `sha256` on the +checklist need. On the first run (placeholder hash) the build **fails** and +prints the actual hash โ€” review the checklist, then paste that hash into the +`sha256` attribute. From then on the build passes until the validated +requirements change again, at which point it fails and asks for a re-review. diff --git a/scripts_bazel/validate_checklist.py b/scripts_bazel/validate_checklist.py new file mode 100644 index 000000000..e6f8d21f7 --- /dev/null +++ b/scripts_bazel/validate_checklist.py @@ -0,0 +1,139 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Validate a requirement checklist against the build output it was reviewed against. + +A ``req_chklst`` sphinx-needs element pins the state of one or more build +outputs (e.g. the extracted component requirements) via a ``sha256`` attribute. +This script: + +1. Reads ``needs.json`` and looks up the checklist need by its id. +2. Computes the SHA256 over the concatenated input files (sorted by path, so the + result is independent of the order in which Bazel passes them). +3. Compares the computed hash with the ``sha256`` attribute of the checklist need. + +On match it writes the verified hash to ``--output`` and exits ``0``. On mismatch +(or when the need / attribute is missing) it logs the expected and actual hashes +and exits ``1``, which fails the Bazel build. +""" + +import argparse +import hashlib +import json +import logging +import sys +from pathlib import Path +from typing import Any + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def find_need(data: dict[str, Any], need_id: str) -> dict[str, Any] | None: + """Return the need with id ``need_id`` from a needs.json structure.""" + for version in data.get("versions", {}).values(): + needs = version.get("needs", {}) + if need_id in needs: + return needs[need_id] + return None + + +def compute_sha256(paths: list[Path]) -> str: + """Return the SHA256 over the concatenated contents of ``paths`` (sorted).""" + digest = hashlib.sha256() + for path in sorted(paths, key=lambda p: p.name): + digest.update(path.read_bytes()) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Validate a requirement checklist (req_chklst) against the SHA256 of " + "the build output it was reviewed against." + ) + ) + _ = parser.add_argument( + "--needs-json", + required=True, + type=Path, + help="Path of the needs.json file containing the checklist need.", + ) + _ = parser.add_argument( + "--checklist-id", + required=True, + help="Id of the req_chklst need to validate (e.g. 'req_chklst__foo').", + ) + _ = parser.add_argument( + "--output", + required=True, + type=Path, + help="Path of the stamp file to write with the verified hash on success.", + ) + _ = parser.add_argument( + "inputs", + nargs="+", + type=Path, + help="Build output files whose combined SHA256 is validated.", + ) + + args = parser.parse_args() + + with open(args.needs_json) as f: + data = json.load(f) + + need = find_need(data, args.checklist_id) + if need is None: + logger.error( + "Checklist need '%s' not found in '%s'.", + args.checklist_id, + args.needs_json, + ) + return 1 + + expected = need.get("sha256") + if not expected: + logger.error( + "Checklist need '%s' has no 'sha256' attribute.", + args.checklist_id, + ) + return 1 + + actual = compute_sha256(args.inputs) + + if expected != actual: + logger.error( + "Checklist '%s' is OUT OF DATE.\n" + " expected (sha256 in need): %s\n" + " actual (build output): %s\n" + "The validated target output has changed since the checklist was " + "last reviewed. Re-review the checklist and update its 'sha256' " + "attribute to '%s'.", + args.checklist_id, + expected, + actual, + actual, + ) + return 1 + + logger.info("Checklist '%s' is up to date (sha256=%s).", args.checklist_id, actual) + + args.output.parent.mkdir(parents=True, exist_ok=True) + _ = args.output.write_text(actual + "\n") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 42d2c0a73..431cff832 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -402,6 +402,30 @@ needs_types: - requirement_excl_process parts: 3 + # Requirement Checklist + # A reviewable checklist element that pins the state of a set of build outputs + # (e.g. the extracted component requirements) via a SHA256 hash. It references + # the checklist document (the rendered `.rst`) and the Bazel target(s) whose + # output is validated against `sha256`. See the `requirements_checklist` + # Bazel rule in `docs.bzl`. + req_chklst: + title: Requirement Checklist + prefix: req_chklst__ + mandatory_options: + # req-Id: tool_req__docs_common_attr_status + status: ^(valid|draft|invalid)$ + # SHA256 (64 lowercase hex chars) of the concatenated target outputs that + # this checklist was reviewed against. + sha256: ^[0-9a-f]{64}$ + optional_options: + # Bazel target label(s) whose output is validated against `sha256`. + # Multiple labels may be separated by spaces or commas. + targets: ^.*$ + optional_links: + # Link to the checklist document (the rendered `.rst` checklist file). + checklist: document + parts: 3 + # - Architecture - # Architecture Element @@ -993,6 +1017,11 @@ needs_extra_links: incoming: covered by outgoing: covers + # Requirement Checklist -> checklist document (rendered .rst) + checklist: + incoming: is checklist for + outgoing: checklist document + # Architecture consists_of: incoming: forms part of From 60b3493c7b3a5be5ecab37b1750d67e975d68989 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Thu, 25 Jun 2026 07:03:48 +0000 Subject: [PATCH 06/12] removed overlapping test --- .../tests/test_metamodel_load.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index 77243eba1..b5c7bfeb8 100644 --- a/src/extensions/score_metamodel/tests/test_metamodel_load.py +++ b/src/extensions/score_metamodel/tests/test_metamodel_load.py @@ -94,33 +94,6 @@ def test_load_metamodel_data(): "link1": "opt1 == test", } - -def test_default_metamodel_contains_generic_verification_and_inspection_types(): - """Default metamodel contains generic module verification and inspection types.""" - result = load_metamodel_data() - - needs_types = { - need_type["directive"]: need_type for need_type in result.needs_types - } - - assert "mod_ver_report" in needs_types - assert "mod_insp" in needs_types - - mod_ver_report = needs_types["mod_ver_report"] - assert mod_ver_report["mandatory_links_str"]["belongs_to"] == "mod" - assert mod_ver_report["optional_links_str"]["contains"] == "ANY" - assert mod_ver_report["optional_links_str"]["evidence"] == "ANY" - assert mod_ver_report["optional_links_str"]["covers"] == "ANY" - - mod_insp = needs_types["mod_insp"] - assert mod_insp["mandatory_links_str"]["inspects"] == "ANY" - assert mod_insp["optional_links_str"]["contains"] == "ANY" - assert mod_insp["optional_links_str"]["evidence"] == "ANY" - - assert "evidence" in result.needs_links - assert "inspects" in result.needs_links - - def test_metamodel_schema_json_is_valid(): """The metamodel JSON schema file must be syntactically valid JSON.""" schema_path = Path(__file__).resolve().parent.parent / "metamodel-schema.json" From 72bdbf9717c5460155024c0a07ce9c0f0d8e50fc Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Thu, 25 Jun 2026 07:09:00 +0000 Subject: [PATCH 07/12] fix formatting --- src/extensions/score_metamodel/tests/test_metamodel_load.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index b5c7bfeb8..f70bf055d 100644 --- a/src/extensions/score_metamodel/tests/test_metamodel_load.py +++ b/src/extensions/score_metamodel/tests/test_metamodel_load.py @@ -94,6 +94,7 @@ def test_load_metamodel_data(): "link1": "opt1 == test", } + def test_metamodel_schema_json_is_valid(): """The metamodel JSON schema file must be syntactically valid JSON.""" schema_path = Path(__file__).resolve().parent.parent / "metamodel-schema.json" From 7c107040628971988ecb6d08cbd98f0c16c18bb2 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:13:38 +0000 Subject: [PATCH 08/12] Add architecture extraction & checklist rules, empty-SHA reporting - Add feature_architecture / component_architecture macros (no static/dynamic split) - Add architecture_checklist macro and arch_chklst need type - Report computed SHA256 on empty sha256 attribute in validate_checklist - Make req_chklst/arch_chklst sha256 optional so empty values build - Document new macros and need type --- docs.bzl | 128 +++++++++++++ scripts_bazel/README_needs_rules.md | 177 ++++++++++++++++++ scripts_bazel/validate_checklist.py | 12 +- src/extensions/score_metamodel/metamodel.yaml | 32 +++- 4 files changed, 344 insertions(+), 5 deletions(-) diff --git a/docs.bzl b/docs.bzl index 0388f7748..d5a1e40c9 100644 --- a/docs.bzl +++ b/docs.bzl @@ -234,6 +234,66 @@ def assumptions_of_use( visibility = visibility, ) +def feature_architecture( + name, + src = "//:needs_json", + feature = None, + visibility = None): + """Extract the feature architecture from a needs.json file. + + Convenience wrapper around `filtered_needs_json`. Produces a `.json` + file containing the feature architecture elements. Static (`feat_arc_sta`) + and dynamic (`feat_arc_dyn`) architecture are not differentiated; both are + kept. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output. Defaults to the calling + package's `//:needs_json`. + feature: Optional feature name. If given, only feature architecture + elements tagged with that feature are kept; if omitted, all feature + architecture elements are kept. + visibility: Standard Bazel visibility for the generated target. + """ + filtered_needs_json( + name = name, + src = src, + types = ["feat_arc_sta", "feat_arc_dyn"], + components = [feature] if feature else [], + component_attr = "tags", + visibility = visibility, + ) + +def component_architecture( + name, + src = "//:needs_json", + component = None, + visibility = None): + """Extract the component architecture from a needs.json file. + + Convenience wrapper around `filtered_needs_json`. Produces a `.json` + file containing the component architecture elements. Static (`comp_arc_sta`) + and dynamic (`comp_arc_dyn`) architecture are not differentiated; both are + kept. + + Args: + name: Name of the generated target. The output file is `.json`. + src: Label of a `needs_json` build output. Defaults to the calling + package's `//:needs_json`. + component: Optional component name. If given, only component architecture + elements tagged with that component are kept; if omitted, all + component architecture elements are kept. + visibility: Standard Bazel visibility for the generated target. + """ + filtered_needs_json( + name = name, + src = src, + types = ["comp_arc_sta", "comp_arc_dyn"], + components = [component] if component else [], + component_attr = "tags", + visibility = visibility, + ) + def sphinx_needs_to_md( name, src, @@ -388,6 +448,74 @@ def requirements_checklist( visibility = visibility, ) +def architecture_checklist( + name, + checklist_id, + deps, + src = "//:needs_json", + visibility = None): + """Validate an architecture checklist (`arch_chklst`) against its build output. + + Building this target recomputes the SHA256 over the concatenated outputs of + `deps` and compares it to the `sha256` attribute of the `arch_chklst` need + `checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when + the hashes differ, i.e. when a validated target output has changed since the + checklist was last reviewed. + + Typical usage validates the extracted architecture of a component against the + checklist that reviewed it: + + component_architecture( + name = "bitmanipulation_comp_arch", + component = "bitmanipulation", + ) + + architecture_checklist( + name = "bitmanipulation_arch_checklist", + checklist_id = "arch_chklst__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], + ) + + Run with `bazel build //:bitmanipulation_arch_checklist`. On the first run (or + after the architecture changes) the build fails and prints the actual SHA256; + copy it into the `sha256` attribute of the checklist need once the checklist + has been (re-)reviewed. + + Args: + name: Name of the generated target. The output file is `.sha256`. + checklist_id: Id of the `arch_chklst` need to validate + (e.g. `"arch_chklst__bitmanipulation__comp_arc"`). + deps: List of labels whose outputs are hashed and validated. Usually a + single `feature_architecture`/`component_architecture`/ + `filtered_needs_json` target. + src: Label of a `needs_json` build output containing the checklist need. + Defaults to the calling package's `//:needs_json`. + visibility: Standard Bazel visibility for the generated target. + """ + validate_tool = Label("//scripts_bazel:validate_checklist") + + dep_args = " ".join(["$(locations %s)" % d for d in deps]) + + native.genrule( + name = name, + srcs = [src] + deps, + outs = [name + ".sha256"], + cmd = """ + $(location {validate_tool}) \ + --needs-json $(location {src})/needs.json \ + --checklist-id '{checklist_id}' \ + --output $@ \ + {dep_args} + """.format( + validate_tool = validate_tool, + checklist_id = checklist_id, + src = src, + dep_args = dep_args, + ), + tools = [validate_tool], + visibility = visibility, + ) + def _missing_requirements(deps): """Add Python hub dependencies if they are missing.""" found = [] diff --git a/scripts_bazel/README_needs_rules.md b/scripts_bazel/README_needs_rules.md index 9abaf9a1d..2e55c86c8 100644 --- a/scripts_bazel/README_needs_rules.md +++ b/scripts_bazel/README_needs_rules.md @@ -34,9 +34,12 @@ other consumers (review docs, TRLC-based tooling) without manual copying. | `component_requirements` | `.json` | Convenience wrapper for `comp_req` elements. | | `feature_requirements` | `.json` | Convenience wrapper for `feat_req` elements. | | `assumptions_of_use` | `.json` | Convenience wrapper for `aou_req` elements. | +| `feature_architecture` | `.json` | Convenience wrapper for `feat_arc_sta` / `feat_arc_dyn` elements. | +| `component_architecture` | `.json` | Convenience wrapper for `comp_arc_sta` / `comp_arc_dyn` elements. | | `sphinx_needs_to_md` | `.md` | Render needs as a Markdown document. | | `sphinx_needs_to_trlc` | `.trlc` | Convert S-CORE requirements to TRLC. | | `requirements_checklist` | `.sha256` | Validate a `req_chklst` need against its target output via SHA256. | +| `architecture_checklist` | `.sha256` | Validate an `arch_chklst` need against its target output via SHA256. | ### Python tools (`scripts_bazel/`) @@ -55,6 +58,10 @@ mandatory `sha256` attribute, an optional `targets` attribute (the Bazel labels it validates), and an optional `checklist` link to the rendered checklist document. +The analogous `arch_chklst` need type (validated by `architecture_checklist`) +is defined in the same file and works the same way for feature/component +architecture outputs. + ## How to use In a `BUILD` file that already has a `needs_json` target, load the macros and @@ -95,6 +102,29 @@ bazel build //path/to:my_feature_reqs_trlc You can also call `filtered_needs_json` directly for full control over the `types`, `components`, and `component_attr` filters. +## Incremental builds and caching + +Because every step is a separate Bazel target, Bazel only re-runs the work that +is actually affected by a change. Edit any `.rst` file and the documentation +build (the `needs_json` target) is re-executed, because its inputs changed. + +However, the filtering, rendering, conversion and checklist targets sit +*downstream* of `needs_json` and Bazel compares their inputs before re-running +them. If your edit does not touch a given subset โ€” for example you change an +unrelated feature and the `component_requirements` output for a component stays +byte-for-byte identical โ€” then that `component_requirements` target produces the +same output as before, and **every target that depends on it +(`sphinx_needs_to_md`, `sphinx_needs_to_trlc`, `requirements_checklist`, ...) is +not re-executed**. Bazel serves their previous results from cache instead. + +In practice this means: + +- Changing an `.rst` file only re-runs the doc build plus the filtered targets + whose content actually changed. +- A `requirements_checklist` (or `architecture_checklist`) only re-validates โ€” + and can only fail โ€” when the requirements/architecture it pins really change. + Unrelated edits elsewhere in the documentation leave it untouched. + ## Requirement checklists A *requirement checklist* couples a human review (a checklist `.rst` document) @@ -145,3 +175,150 @@ checklist need. On the first run (placeholder hash) the build **fails** and prints the actual hash โ€” review the checklist, then paste that hash into the `sha256` attribute. From then on the build passes until the validated requirements change again, at which point it fails and asks for a re-review. + +## Architecture checklists + +An *architecture checklist* works exactly like a requirement checklist, but +pins the reviewed state of an *architecture* output (a feature or component +architecture) instead of requirements. The review (a checklist `.rst` +document) is coupled to the extracted architecture via a SHA256 hash stored on +an `arch_chklst` sphinx-needs element. When the architecture later changes, the +checklist is considered stale and the build fails until it is re-reviewed and +the hash updated. + +### 1. Declare the checklist need + +Add an `arch_chklst` element (e.g. next to the architecture `.rst`). It +references the checklist document, the validated Bazel target(s), and the +expected hash: + +```rst +.. arch_chklst:: Baselibs Feature Architecture Checklist + :id: arch_chklst__baselibs__feat_arc + :status: valid + :checklist: doc__baselibs_architecture + :targets: //:baselibs_feature_arch + :sha256: 0000000000000000000000000000000000000000000000000000000000000000 +``` + +### 2. Declare the validation target + +```starlark +load("@docs-as-code//:docs.bzl", "feature_architecture", "architecture_checklist") + +feature_architecture( + name = "baselibs_feature_arch", + feature = "baselibs", +) + +architecture_checklist( + name = "baselibs_feat_arch_checklist", + checklist_id = "arch_chklst__baselibs__feat_arc", + deps = [":baselibs_feature_arch"], +) +``` + +Use `component_architecture` instead of `feature_architecture` to validate a +single component's architecture. + +### 3. Validate + +```bash +bazel build //:baselibs_feat_arch_checklist +``` + +The build hashes the `deps` output and compares it to the `sha256` on the +checklist need. On the first run (placeholder hash) the build **fails** and +prints the actual hash โ€” review the checklist, then paste that hash into the +`sha256` attribute. From then on the build passes until the validated +architecture changes again, at which point it fails and asks for a re-review. + +## Worked example: bitmanipulation (baselibs) + +The [baselibs](https://github.com/eclipse-score/baselibs) repository wires both +checklist kinds for its `bitmanipulation` component. The flow below is a +complete, real example that you can copy. + +### Requirements checklist + +`BUILD`: + +```starlark +component_requirements( + name = "bitmanipulation_comp_reqs", + component = "bitmanipulation", +) + +requirements_checklist( + name = "bitmanipulation_req_checklist", + checklist_id = "req_chklst__bitmanipulation__comp_req", + deps = [":bitmanipulation_comp_reqs"], +) +``` + +Checklist need (next to the requirements inspection `.rst`): + +```rst +.. req_chklst:: Bitmanipulation Component Requirements Checklist + :id: req_chklst__bitmanipulation__comp_req + :status: valid + :checklist: doc__bitmanipulation_req_inspection + :targets: //:bitmanipulation_comp_reqs + :sha256: +``` + +### Architecture checklist + +`BUILD`: + +```starlark +component_architecture( + name = "bitmanipulation_comp_arch", + component = "bitmanipulation", +) + +architecture_checklist( + name = "bitmanipulation_arch_checklist", + checklist_id = "arch_chklst__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], +) +``` + +Checklist need (next to the architecture inspection `.rst`): + +```rst +.. arch_chklst:: Bitmanipulation Component Architecture Checklist + :id: arch_chklst__bitmanipulation__comp_arc + :status: valid + :checklist: doc__bitmanipulation_arc_inspection + :targets: //:bitmanipulation_comp_arch + :sha256: +``` + +### Gotcha: the component filter matches on `tags` + +`component_requirements` / `component_architecture` keep only needs whose +`tags` contain the requested component name (see +[filtered_needs_json](filter_needs_json.py), `--component-attr tags`). In +baselibs that tag is *not* set on each element directly โ€” it is injected for a +whole document via `needextend`, e.g. for the requirements: + +```rst +.. needextend:: "__bitmanipulation__" in id + :+tags: baselibs, bitmanipulation +``` + +The architecture view ids do **not** contain `__bitmanipulation__` (they read +`comp_arc_sta__baselibs__bit_manipulation`), so that `needextend` does not tag +them and `component_architecture(component = "bitmanipulation")` would extract +*zero* needs. Add a matching `needextend` in the architecture document so the +views get the component tag: + +```rst +.. needextend:: docname is not None and "bitmanipulation" in docname and "architecture" in docname and type in ["comp_arc_sta", "comp_arc_dyn"] + :+tags: baselibs, bitmanipulation +``` + +After that, `bazel build //:bitmanipulation_comp_arch` keeps the architecture +view(s) and the checklist validates as expected. If a `component_*` target +unexpectedly extracts `0 needs`, check the `tags` of the elements first. diff --git a/scripts_bazel/validate_checklist.py b/scripts_bazel/validate_checklist.py index e6f8d21f7..75d7b8b80 100644 --- a/scripts_bazel/validate_checklist.py +++ b/scripts_bazel/validate_checklist.py @@ -103,15 +103,21 @@ def main() -> int: return 1 expected = need.get("sha256") + + actual = compute_sha256(args.inputs) + if not expected: logger.error( - "Checklist need '%s' has no 'sha256' attribute.", + "Checklist '%s' has an EMPTY 'sha256' attribute.\n" + "Review the target output and, if correct, pin it by setting the " + "checklist's 'sha256' attribute to:\n" + "\n" + " %s\n", args.checklist_id, + actual, ) return 1 - actual = compute_sha256(args.inputs) - if expected != actual: logger.error( "Checklist '%s' is OUT OF DATE.\n" diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 431cff832..28e741247 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -414,10 +414,38 @@ needs_types: mandatory_options: # req-Id: tool_req__docs_common_attr_status status: ^(valid|draft|invalid)$ + optional_options: # SHA256 (64 lowercase hex chars) of the concatenated target outputs that - # this checklist was reviewed against. - sha256: ^[0-9a-f]{64}$ + # this checklist was reviewed against. May be left empty to let the + # `requirements_checklist` build fail and report the computed SHA256 to + # pin (see `validate_checklist.py`). + sha256: ^([0-9a-f]{64})?$ + # Bazel target label(s) whose output is validated against `sha256`. + # Multiple labels may be separated by spaces or commas. + targets: ^.*$ + optional_links: + # Link to the checklist document (the rendered `.rst` checklist file). + checklist: document + parts: 3 + + # Architecture Checklist + # A reviewable checklist element that pins the state of a set of build outputs + # (e.g. the extracted feature/component architecture) via a SHA256 hash. It + # references the checklist document (the rendered `.rst`) and the Bazel + # target(s) whose output is validated against `sha256`. See the + # `architecture_checklist` Bazel rule in `docs.bzl`. + arch_chklst: + title: Architecture Checklist + prefix: arch_chklst__ + mandatory_options: + # req-Id: tool_req__docs_common_attr_status + status: ^(valid|draft|invalid)$ optional_options: + # SHA256 (64 lowercase hex chars) of the concatenated target outputs that + # this checklist was reviewed against. May be left empty to let the + # `architecture_checklist` build fail and report the computed SHA256 to + # pin (see `validate_checklist.py`). + sha256: ^([0-9a-f]{64})?$ # Bazel target label(s) whose output is validated against `sha256`. # Multiple labels may be separated by spaces or commas. targets: ^.*$ From adf58f464f0dce12f71494ff5276057d79575f72 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:19:14 +0000 Subject: [PATCH 09/12] feat(checklist): hash transitive dependencies in requirement/architecture checklists requirements_checklist and architecture_checklist now follow the sphinx-needs link graph recursively from the elements in deps and hash the whole closure, so a checklist goes out of date when an upstream dependency (e.g. a linked stakeholder requirement) changes, not only when a root element changes. - validate_checklist.py: add transitive mode via repeatable --link-field; follow links recursively, normalize version-constrained link targets (id[version==1] -> id), hash canonical serialization of the closure. - docs.bzl: add link_fields arg to both macros (defaults cover the requirement / architecture link chains); pass --link-field through. link_fields = [] restores the previous flat hashing. - metamodel.yaml + README_needs_rules.md: document the recursive behavior. --- How-to: S-Core process step-by-step.md | 13 + docs.bzl | 165 +++++++---- scripts_bazel/README_needs_rules.md | 279 ++++++++++-------- scripts_bazel/filter_needs_json.py | 88 +++--- scripts_bazel/validate_checklist.py | 209 ++++++++++++- src/extensions/score_metamodel/metamodel.yaml | 28 +- 6 files changed, 557 insertions(+), 225 deletions(-) create mode 100644 How-to: S-Core process step-by-step.md diff --git a/How-to: S-Core process step-by-step.md b/How-to: S-Core process step-by-step.md new file mode 100644 index 000000000..8bd34425f --- /dev/null +++ b/How-to: S-Core process step-by-step.md @@ -0,0 +1,13 @@ +# How-to: S-Core process step-by-step + +## 1. Feature & Feature requirements in main score repository + +### a. What to do + +1. Define Feature +2. Define Feature Requirements and map them to the stakeholder requirements + +### b. How to check + +1. Automated checks for requirements in docs-as-code. +2. Checklist for things, that can not be automated -> automated check, that checklist is up to date. diff --git a/docs.bzl b/docs.bzl index d5a1e40c9..fe77770c0 100644 --- a/docs.bzl +++ b/docs.bzl @@ -102,14 +102,13 @@ def filtered_needs_json( name, src, types = [], - components = [], - component_attr = "component", + names = [], visibility = None): """Extract a subset of sphinx-needs elements from a needs.json file. Produces a `.json` file containing only the needs that match all of the given filters. This is useful to hand a downstream consumer just the - elements (e.g. `feat_req`) of one or more particular components. + elements (e.g. `feat_req`) of one or more particular features/components. Args: name: Name of the generated target. The output file is `.json`. @@ -117,16 +116,16 @@ def filtered_needs_json( `needs.json`), e.g. `":needs_json"` or `"@score_process//:needs_json"`. types: Optional list of sphinx-needs element types to keep (e.g. `["feat_req", "comp_req"]`). If empty, all types are kept. - components: Optional list of component names to keep. If empty, all - components are kept. - component_attr: Need attribute matched against `components`. - Defaults to `"component"`. + names: Optional list of feature/component names to keep, matched against + the second `__`-separated segment of each need ID (the + `____...` naming convention). If empty, all + features/components are kept. visibility: Standard Bazel visibility for the generated target. """ filter_tool = Label("//scripts_bazel:filter_needs_json") type_args = " ".join(["--type '%s'" % t for t in types]) - component_args = " ".join(["--component '%s'" % c for c in components]) + name_args = " ".join(["--name '%s'" % n for n in names]) native.genrule( name = name, @@ -135,15 +134,13 @@ def filtered_needs_json( cmd = """ $(location {filter_tool}) \ --output $@ \ - --component-attr '{component_attr}' \ {type_args} \ - {component_args} \ + {name_args} \ $(location {src})/needs.json """.format( filter_tool = filter_tool, - component_attr = component_attr, type_args = type_args, - component_args = component_args, + name_args = name_args, src = src, ), tools = [filter_tool], @@ -165,16 +162,16 @@ def component_requirements( src: Label of a `needs_json` build output. Defaults to the calling package's `//:needs_json`. component: Optional component name. If given, only component requirements - tagged with that component are kept; if omitted, all component - requirements are kept. + named with that component (per the `____...` + convention) are kept; if omitted, all component requirements are + kept. visibility: Standard Bazel visibility for the generated target. """ filtered_needs_json( name = name, src = src, types = ["comp_req"], - components = [component] if component else [], - component_attr = "tags", + names = [component] if component else [], visibility = visibility, ) @@ -193,16 +190,15 @@ def feature_requirements( src: Label of a `needs_json` build output. Defaults to the calling package's `//:needs_json`. feature: Optional feature name. If given, only feature requirements - tagged with that feature are kept; if omitted, all feature - requirements are kept. + named with that feature (per the `____...` convention) + are kept; if omitted, all feature requirements are kept. visibility: Standard Bazel visibility for the generated target. """ filtered_needs_json( name = name, src = src, types = ["feat_req"], - components = [feature] if feature else [], - component_attr = "tags", + names = [feature] if feature else [], visibility = visibility, ) @@ -221,16 +217,15 @@ def assumptions_of_use( src: Label of a `needs_json` build output. Defaults to the calling package's `//:needs_json`. component: Optional component name. If given, only assumptions of use - tagged with that component are kept; if omitted, all assumptions of - use are kept. + named with that component (per the `____...` + convention) are kept; if omitted, all assumptions of use are kept. visibility: Standard Bazel visibility for the generated target. """ filtered_needs_json( name = name, src = src, types = ["aou_req"], - components = [component] if component else [], - component_attr = "tags", + names = [component] if component else [], visibility = visibility, ) @@ -251,16 +246,16 @@ def feature_architecture( src: Label of a `needs_json` build output. Defaults to the calling package's `//:needs_json`. feature: Optional feature name. If given, only feature architecture - elements tagged with that feature are kept; if omitted, all feature - architecture elements are kept. + elements named with that feature (per the `____...` + convention) are kept; if omitted, all feature architecture elements + are kept. visibility: Standard Bazel visibility for the generated target. """ filtered_needs_json( name = name, src = src, types = ["feat_arc_sta", "feat_arc_dyn"], - components = [feature] if feature else [], - component_attr = "tags", + names = [feature] if feature else [], visibility = visibility, ) @@ -281,16 +276,16 @@ def component_architecture( src: Label of a `needs_json` build output. Defaults to the calling package's `//:needs_json`. component: Optional component name. If given, only component architecture - elements tagged with that component are kept; if omitted, all - component architecture elements are kept. + elements named with that component (per the `____...` + convention) are kept; if omitted, all component architecture + elements are kept. visibility: Standard Bazel visibility for the generated target. """ filtered_needs_json( name = name, src = src, types = ["comp_arc_sta", "comp_arc_dyn"], - components = [component] if component else [], - component_attr = "tags", + names = [component] if component else [], visibility = visibility, ) @@ -386,14 +381,26 @@ def requirements_checklist( checklist_id, deps, src = "//:needs_json", + link_fields = ["derived_from", "satisfies", "covers"], + extra_needs = [], visibility = None): """Validate a requirement checklist (`req_chklst`) against its build output. - Building this target recomputes the SHA256 over the concatenated outputs of - `deps` and compares it to the `sha256` attribute of the `req_chklst` need - `checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when - the hashes differ, i.e. when a validated target output has changed since the - checklist was last reviewed. + Building this target recomputes the SHA256 over the requirements in `deps` + **and**, by default, over everything they depend on transitively, and + compares it to the `sha256` attribute of the `req_chklst` need `checklist_id` + (looked up in `src`'s `needs.json`). The build **fails** when the hashes + differ, i.e. when a validated requirement *or one of its (recursive) + dependencies* has changed since the checklist was last reviewed. + + The dependency graph is the sphinx-needs link graph: starting from the + requirements in `deps` (the *roots*), the `link_fields` (by default + `derived_from`, `satisfies` and `covers`) are followed recursively through + `src`'s `needs.json`. For feature requirements this means the linked + stakeholder requirements (and their parents in turn) are part of the hash, so + changing a relevant stakeholder requirement makes this checklist go out of + date. Pass `link_fields = []` to restore the old behaviour of hashing only + the requirements in `deps`. Typical usage validates the extracted requirements of a component against the checklist that reviewed them: @@ -418,30 +425,50 @@ def requirements_checklist( name: Name of the generated target. The output file is `.sha256`. checklist_id: Id of the `req_chklst` need to validate (e.g. `"req_chklst__bitmanipulation__comp_req"`). - deps: List of labels whose outputs are hashed and validated. Usually a - single `component_requirements`/`filtered_needs_json` target. - src: Label of a `needs_json` build output containing the checklist need. - Defaults to the calling package's `//:needs_json`. + deps: List of labels whose outputs define the root requirements that are + hashed and validated. Usually a single + `component_requirements`/`feature_requirements`/`filtered_needs_json` + target. + src: Label of a `needs_json` build output containing the checklist need + and the full link graph. Defaults to the calling package's + `//:needs_json`. + link_fields: Sphinx-needs link fields followed recursively from the root + requirements to include their (transitive) dependencies in the hash. + Defaults to `["derived_from", "satisfies", "covers"]`. Set to `[]` to + hash only the requirements in `deps`. + extra_needs: Optional list of additional `needs_json` build outputs that + provide the full content of needs referenced from the validated + requirements but not contained in `src` (e.g. stakeholder + requirements imported from an upstream repository). Without them such + external needs are hashed as `` and changes to them are not + detected. Typically the same upstream `needs_json` targets passed as + `data` to `docs(...)` (e.g. `["@score_platform//:needs_json"]`). visibility: Standard Bazel visibility for the generated target. """ validate_tool = Label("//scripts_bazel:validate_checklist") dep_args = " ".join(["$(locations %s)" % d for d in deps]) + link_args = " ".join(["--link-field '%s'" % f for f in link_fields]) + extra_args = " ".join(["--extra-needs-json $(location %s)/needs.json" % e for e in extra_needs]) native.genrule( name = name, - srcs = [src] + deps, + srcs = [src] + extra_needs + deps, outs = [name + ".sha256"], cmd = """ $(location {validate_tool}) \ --needs-json $(location {src})/needs.json \ --checklist-id '{checklist_id}' \ --output $@ \ + {link_args} \ + {extra_args} \ {dep_args} """.format( validate_tool = validate_tool, checklist_id = checklist_id, src = src, + link_args = link_args, + extra_args = extra_args, dep_args = dep_args, ), tools = [validate_tool], @@ -453,14 +480,27 @@ def architecture_checklist( checklist_id, deps, src = "//:needs_json", + link_fields = ["fulfils", "includes", "uses", "provides", "derived_from", "satisfies", "covers"], + extra_needs = [], visibility = None): """Validate an architecture checklist (`arch_chklst`) against its build output. - Building this target recomputes the SHA256 over the concatenated outputs of - `deps` and compares it to the `sha256` attribute of the `arch_chklst` need - `checklist_id` (looked up in `src`'s `needs.json`). The build **fails** when - the hashes differ, i.e. when a validated target output has changed since the - checklist was last reviewed. + Building this target recomputes the SHA256 over the architecture in `deps` + **and**, by default, over everything they depend on transitively, and + compares it to the `sha256` attribute of the `arch_chklst` need `checklist_id` + (looked up in `src`'s `needs.json`). The build **fails** when the hashes + differ, i.e. when a validated architecture element *or one of its (recursive) + dependencies* has changed since the checklist was last reviewed. + + The dependency graph is the sphinx-needs link graph: starting from the + architecture elements in `deps` (the *roots*), the `link_fields` are followed + recursively through `src`'s `needs.json`. The defaults follow the structural + architecture links (`includes`, `uses`, `provides`) as well as `fulfils` and + the requirement links (`derived_from`, `satisfies`, `covers`), so the closure + reaches the fulfilled requirements and their parents. Changing a fulfilled + requirement (or a stakeholder requirement it derives from) therefore makes + this checklist go out of date. Pass `link_fields = []` to restore the old + behaviour of hashing only the elements in `deps`. Typical usage validates the extracted architecture of a component against the checklist that reviewed it: @@ -485,31 +525,50 @@ def architecture_checklist( name: Name of the generated target. The output file is `.sha256`. checklist_id: Id of the `arch_chklst` need to validate (e.g. `"arch_chklst__bitmanipulation__comp_arc"`). - deps: List of labels whose outputs are hashed and validated. Usually a - single `feature_architecture`/`component_architecture`/ - `filtered_needs_json` target. - src: Label of a `needs_json` build output containing the checklist need. - Defaults to the calling package's `//:needs_json`. + deps: List of labels whose outputs define the root architecture elements + that are hashed and validated. Usually a single + `feature_architecture`/`component_architecture`/`filtered_needs_json` + target. + src: Label of a `needs_json` build output containing the checklist need + and the full link graph. Defaults to the calling package's + `//:needs_json`. + link_fields: Sphinx-needs link fields followed recursively from the root + architecture elements to include their (transitive) dependencies in + the hash. Defaults to the structural architecture links plus the + requirement links. Set to `[]` to hash only the elements in `deps`. + extra_needs: Optional list of additional `needs_json` build outputs that + provide the full content of needs referenced from the validated + architecture elements but not contained in `src` (e.g. feature + requirements imported from an upstream repository). Without them such + external needs are hashed as `` and changes to them are not + detected. Typically the same upstream `needs_json` targets passed as + `data` to `docs(...)` (e.g. `["@score_platform//:needs_json"]`). visibility: Standard Bazel visibility for the generated target. """ validate_tool = Label("//scripts_bazel:validate_checklist") dep_args = " ".join(["$(locations %s)" % d for d in deps]) + link_args = " ".join(["--link-field '%s'" % f for f in link_fields]) + extra_args = " ".join(["--extra-needs-json $(location %s)/needs.json" % e for e in extra_needs]) native.genrule( name = name, - srcs = [src] + deps, + srcs = [src] + extra_needs + deps, outs = [name + ".sha256"], cmd = """ $(location {validate_tool}) \ --needs-json $(location {src})/needs.json \ --checklist-id '{checklist_id}' \ --output $@ \ + {link_args} \ + {extra_args} \ {dep_args} """.format( validate_tool = validate_tool, checklist_id = checklist_id, src = src, + link_args = link_args, + extra_args = extra_args, dep_args = dep_args, ), tools = [validate_tool], diff --git a/scripts_bazel/README_needs_rules.md b/scripts_bazel/README_needs_rules.md index 2e55c86c8..b9c22c5f2 100644 --- a/scripts_bazel/README_needs_rules.md +++ b/scripts_bazel/README_needs_rules.md @@ -100,7 +100,7 @@ bazel build //path/to:my_feature_reqs_trlc ``` You can also call `filtered_needs_json` directly for full control over the -`types`, `components`, and `component_attr` filters. +`types` and `names` filters. ## Incremental builds and caching @@ -121,34 +121,66 @@ In practice this means: - Changing an `.rst` file only re-runs the doc build plus the filtered targets whose content actually changed. -- A `requirements_checklist` (or `architecture_checklist`) only re-validates โ€” - and can only fail โ€” when the requirements/architecture it pins really change. - Unrelated edits elsewhere in the documentation leave it untouched. +- A `requirements_checklist` (or `architecture_checklist`) re-validates whenever + the full `needs.json` changes, but it only **fails** when the elements it pins + โ€” the roots in `deps` *or any of their transitive dependencies* (see + [Transitive dependencies](#transitive-dependencies)) โ€” really change. Edits to + unrelated, unreachable elements leave its hash untouched. -## Requirement checklists +## Checklists -A *requirement checklist* couples a human review (a checklist `.rst` document) -with the exact build output that was reviewed. The state of that output is -pinned via a SHA256 hash stored on a `req_chklst` sphinx-needs element. When the -output later changes, the checklist is considered stale and the build fails -until the checklist is re-reviewed and the hash updated. +A *checklist* couples a human review (a checklist `.rst` document) with the exact +build output that was reviewed. The state of that output is pinned via a SHA256 +hash stored on a sphinx-needs element. When the output later changes, the +checklist is considered stale and the build fails until it is re-reviewed and the +hash updated. + +There are two checklist kinds. They behave identically and differ only in the +need type they pin and the macro that validates them: + +| Kind | Need type | Validating macro | Pins the reviewed state of โ€ฆ | +| --- | --- | --- | --- | +| Requirements | `req_chklst` | `requirements_checklist` | extracted requirements (`component_requirements` / `feature_requirements`) and, by default, their transitive dependencies | +| Architecture | `arch_chklst` | `architecture_checklist` | extracted architecture (`component_architecture` / `feature_architecture`) and, by default, their transitive dependencies | + +The flow below is a complete, real example from the +[baselibs](https://github.com/eclipse-score/baselibs) repository, which wires +both kinds for its `bitmanipulation` component. You can copy it directly. ### 1. Declare the checklist need -Add a `req_chklst` element (e.g. next to the checklist `.rst`). It references the -checklist document, the validated Bazel target(s), and the expected hash: +Add the checklist element next to the inspection `.rst`. It references the +checklist document and the expected hash. On the +first build leave the `sha256` as the placeholder (all zeros) and pin the real +value afterwards. + +Requirements (`req_chklst`): ```rst .. req_chklst:: Bitmanipulation Component Requirements Checklist :id: req_chklst__bitmanipulation__comp_req :status: valid :checklist: doc__bitmanipulation_req_inspection - :targets: //:bitmanipulation_comp_reqs + :sha256: 0000000000000000000000000000000000000000000000000000000000000000 +``` + +Architecture (`arch_chklst`): + +```rst +.. arch_chklst:: Bitmanipulation Component Architecture Checklist + :id: arch_chklst__bitmanipulation__comp_arc + :status: valid + :checklist: doc__bitmanipulation_arc_inspection :sha256: 0000000000000000000000000000000000000000000000000000000000000000 ``` ### 2. Declare the validation target +Extract the reviewed output (`component_*` wrapper) and validate it against the +checklist need. + +Requirements `BUILD`: + ```starlark load("@docs-as-code//:docs.bzl", "component_requirements", "requirements_checklist") @@ -161,164 +193,171 @@ requirements_checklist( name = "bitmanipulation_req_checklist", checklist_id = "req_chklst__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], + # Resolve upstream stakeholder requirements so changes to them are detected. + extra_needs = ["@score_platform//:needs_json"], ) ``` -### 3. Validate - -```bash -bazel build //:bitmanipulation_req_checklist -``` - -The build hashes the `deps` output and compares it to the `sha256` on the -checklist need. On the first run (placeholder hash) the build **fails** and -prints the actual hash โ€” review the checklist, then paste that hash into the -`sha256` attribute. From then on the build passes until the validated -requirements change again, at which point it fails and asks for a re-review. - -## Architecture checklists - -An *architecture checklist* works exactly like a requirement checklist, but -pins the reviewed state of an *architecture* output (a feature or component -architecture) instead of requirements. The review (a checklist `.rst` -document) is coupled to the extracted architecture via a SHA256 hash stored on -an `arch_chklst` sphinx-needs element. When the architecture later changes, the -checklist is considered stale and the build fails until it is re-reviewed and -the hash updated. - -### 1. Declare the checklist need - -Add an `arch_chklst` element (e.g. next to the architecture `.rst`). It -references the checklist document, the validated Bazel target(s), and the -expected hash: - -```rst -.. arch_chklst:: Baselibs Feature Architecture Checklist - :id: arch_chklst__baselibs__feat_arc - :status: valid - :checklist: doc__baselibs_architecture - :targets: //:baselibs_feature_arch - :sha256: 0000000000000000000000000000000000000000000000000000000000000000 -``` - -### 2. Declare the validation target +Architecture `BUILD`: ```starlark -load("@docs-as-code//:docs.bzl", "feature_architecture", "architecture_checklist") +load("@docs-as-code//:docs.bzl", "component_architecture", "architecture_checklist") -feature_architecture( - name = "baselibs_feature_arch", - feature = "baselibs", +component_architecture( + name = "bitmanipulation_comp_arch", + component = "bitmanipulation", ) architecture_checklist( - name = "baselibs_feat_arch_checklist", - checklist_id = "arch_chklst__baselibs__feat_arc", - deps = [":baselibs_feature_arch"], + name = "bitmanipulation_arch_checklist", + checklist_id = "arch_chklst__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], + # Resolve upstream (feature/stakeholder) requirements so changes are detected. + extra_needs = ["@score_platform//:needs_json"], ) ``` -Use `component_architecture` instead of `feature_architecture` to validate a -single component's architecture. +Use the `feature_requirements` / `feature_architecture` wrappers instead of the +`component_*` ones to validate a whole feature rather than a single component. + +The `extra_needs` argument is explained in +[Resolving external needs (`extra_needs`)](#resolving-external-needs-extra_needs) +below; drop it if all transitively linked elements live in the same repository. ### 3. Validate ```bash -bazel build //:baselibs_feat_arch_checklist +bazel build //:bitmanipulation_req_checklist +bazel build //:bitmanipulation_arch_checklist ``` The build hashes the `deps` output and compares it to the `sha256` on the checklist need. On the first run (placeholder hash) the build **fails** and prints the actual hash โ€” review the checklist, then paste that hash into the `sha256` attribute. From then on the build passes until the validated -architecture changes again, at which point it fails and asks for a re-review. - -## Worked example: bitmanipulation (baselibs) - -The [baselibs](https://github.com/eclipse-score/baselibs) repository wires both -checklist kinds for its `bitmanipulation` component. The flow below is a -complete, real example that you can copy. - -### Requirements checklist - -`BUILD`: +requirements/architecture change again, at which point it fails and asks for a +re-review. + +### Transitive dependencies + +By default neither `requirements_checklist` nor `architecture_checklist` hash +only the elements in `deps`; they also follow the elements' sphinx-needs links +recursively and include every reachable element in the hash. The followed link +fields are configurable via the `link_fields` argument: + +| Macro | Default `link_fields` | +| --- | --- | +| `requirements_checklist` | `derived_from`, `satisfies`, `covers` | +| `architecture_checklist` | `fulfils`, `includes`, `uses`, `provides`, `derived_from`, `satisfies`, `covers` | + +The mechanism is generic and works for any element type and any link fields: + +- **Feature requirements** โ†’ linked **stakeholder requirements** (via + `derived_from`/`satisfies`) are part of the hash. +- **Component requirements** โ†’ linked **feature requirements** (and their + stakeholder requirements in turn) are part of the hash. Just point `deps` at a + `component_requirements` target; the defaults already cover the + `comp_req โ†’ feat_req โ†’ stkh_req` chain. +- **Architecture elements** โ†’ the requirements they `fulfils` (and those + requirements' parents) plus structurally linked architecture elements + (`includes`/`uses`/`provides`) are part of the hash. + +This means a checklist also goes out of date when an **upstream** dependency +changes โ€” e.g. when a stakeholder requirement that a feature requirement is +`derived_from` is edited. The roots (the elements in `deps`) define the entry +points; the validator walks the link graph in the full `needs.json` (`src`) and +hashes the canonical serialization of the whole closure (roots + all +transitively linked elements). Link targets carrying a version constraint +(`stkh_req__foo[version==1]`) are matched against the plain need id. ```starlark -component_requirements( - name = "bitmanipulation_comp_reqs", - component = "bitmanipulation", -) - +# Component requirements: transitively pins feature + stakeholder requirements. requirements_checklist( name = "bitmanipulation_req_checklist", checklist_id = "req_chklst__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], + # default: link_fields = ["derived_from", "satisfies", "covers"] +) + +# Architecture: transitively pins fulfilled requirements (and their parents). +architecture_checklist( + name = "bitmanipulation_arch_checklist", + checklist_id = "arch_chklst__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], ) ``` -Checklist need (next to the requirements inspection `.rst`): +Pass `link_fields = []` to restore the old behaviour of hashing only the +elements in `deps`. The full need dict of every element in the closure is +hashed, so any change to a linked element โ€” its content, attributes *or* links +(including newly added/removed back-links) โ€” triggers a re-review. -```rst -.. req_chklst:: Bitmanipulation Component Requirements Checklist - :id: req_chklst__bitmanipulation__comp_req - :status: valid - :checklist: doc__bitmanipulation_req_inspection - :targets: //:bitmanipulation_comp_reqs - :sha256: -``` +### Resolving external needs (`extra_needs`) -### Architecture checklist +The transitive closure is walked through the link graph of the checklist's +`src` (`//:needs_json` by default). When a linked element lives in **another +repository** โ€” e.g. a component requirement is `derived_from` a stakeholder +requirement that is imported from an upstream repo โ€” that element's full content +is not contained in the local `needs.json`. The validator then hashes it as +``, so **changes to such upstream elements go undetected** and the +checklist does not go stale even though one of its (transitive) parents changed. -`BUILD`: +Pass the upstream `needs_json` build outputs via `extra_needs` so the validator +can resolve the full content of those external needs and include them in the +hash. Typically these are the same upstream `needs_json` targets you already +pass as `data` to `docs(...)`: ```starlark -component_architecture( - name = "bitmanipulation_comp_arch", - component = "bitmanipulation", +requirements_checklist( + name = "bitmanipulation_req_checklist", + checklist_id = "req_chklst__bitmanipulation__comp_req", + deps = [":bitmanipulation_comp_reqs"], + extra_needs = ["@score_platform//:needs_json"], ) architecture_checklist( name = "bitmanipulation_arch_checklist", checklist_id = "arch_chklst__bitmanipulation__comp_arc", deps = [":bitmanipulation_comp_arch"], + extra_needs = ["@score_platform//:needs_json"], ) ``` -Checklist need (next to the architecture inspection `.rst`): +`extra_needs` is available on both `requirements_checklist` and +`architecture_checklist` and only fills in needs that are *missing* locally: the +main `src` keeps precedence for local content, and the extracted root elements +in `deps` keep precedence over both. You only need it when the closure reaches +elements that are not defined in the checklist's own repository โ€” if everything +is local, leave it at its default (`[]`). -```rst -.. arch_chklst:: Bitmanipulation Component Architecture Checklist - :id: arch_chklst__bitmanipulation__comp_arc - :status: valid - :checklist: doc__bitmanipulation_arc_inspection - :targets: //:bitmanipulation_comp_arch - :sha256: -``` -### Gotcha: the component filter matches on `tags` -`component_requirements` / `component_architecture` keep only needs whose -`tags` contain the requested component name (see -[filtered_needs_json](filter_needs_json.py), `--component-attr tags`). In -baselibs that tag is *not* set on each element directly โ€” it is injected for a -whole document via `needextend`, e.g. for the requirements: +### Gotcha: the feature/component filter matches on the need ID -```rst -.. needextend:: "__bitmanipulation__" in id - :+tags: baselibs, bitmanipulation +`component_requirements` / `component_architecture` / +`feature_requirements` / `feature_architecture` keep only needs whose ID +encodes the requested feature/component name. Need IDs follow the +`____` naming convention, so the second `__`-separated +segment is the feature/component name (see +[filtered_needs_json](filter_needs_json.py), `--name`). For example, the +following all belong to `bitmanipulation`: + +```text +comp_req__bitmanipulation__shift +feat_arc_sta__bitmanipulation__static_view ``` -The architecture view ids do **not** contain `__bitmanipulation__` (they read -`comp_arc_sta__baselibs__bit_manipulation`), so that `needextend` does not tag -them and `component_architecture(component = "bitmanipulation")` would extract -*zero* needs. Add a matching `needextend` in the architecture document so the -views get the component tag: +The `comp_arc_sta` and `comp_arc_dyn` types are an exception: their IDs follow +`____`, so the *third* segment holds the +component name used for matching. For example, the following belongs to the +`filesystem` component: -```rst -.. needextend:: docname is not None and "bitmanipulation" in docname and "architecture" in docname and type in ["comp_arc_sta", "comp_arc_dyn"] - :+tags: baselibs, bitmanipulation +```text +comp_arc_sta__baselibs__filesystem ``` -After that, `bazel build //:bitmanipulation_comp_arch` keeps the architecture -view(s) and the checklist validates as expected. If a `component_*` target -unexpectedly extracts `0 needs`, check the `tags` of the elements first. +No tags or `needextend` injection are required โ€” extraction is driven purely by +the ID. Just make sure each element's ID follows the convention with the +intended feature/component name as its name segment. If a `component_*` / +`feature_*` target unexpectedly extracts `0 needs`, check that the element IDs +use the expected name segment. diff --git a/scripts_bazel/filter_needs_json.py b/scripts_bazel/filter_needs_json.py index c078afafd..6964936c9 100644 --- a/scripts_bazel/filter_needs_json.py +++ b/scripts_bazel/filter_needs_json.py @@ -19,11 +19,15 @@ * ``--type``: the value of the need's ``type`` attribute is in the requested list of element types (e.g. ``feat_req``). If no ``--type`` is given, needs of any type are kept. -* ``--component``: the value of the need's component attribute (configurable - via ``--component-attr``, default ``component``) matches one of the requested - component names. The attribute may hold a single string or a list of strings; - a need is kept when any of its values matches. If no ``--component`` is given, - needs of any component are kept. +* ``--name``: the feature/component name encoded in the need's ID matches one + of the requested names. Need IDs follow the convention + ``____`` (e.g. ``feat_req__baselibs__core_utilities``), so + the second ``__``-separated segment is the feature/component name. The + ``comp_arc_sta`` and ``comp_arc_dyn`` types are an exception: their IDs follow + ``____`` (e.g. + ``comp_arc_sta__baselibs__filesystem``), so the *third* segment holds the + component name used for matching. If no ``--name`` is given, needs of any + feature/component are kept. The top-level structure of the needs.json file is preserved; only the per-need entries are filtered. @@ -40,27 +44,43 @@ logger = logging.getLogger(__name__) -def _attribute_values(need: dict[str, Any], attr: str) -> list[str]: - """Return the values of ``attr`` on a need as a list of strings.""" - value = need.get(attr) - if value is None: - return [] - if isinstance(value, list): - return [str(v) for v in value] # pyright: ignore[reportUnknownVariableType] - return [str(value)] +# Element types whose IDs follow ``____``, +# i.e. the component name used for matching is the *third* ``__`` segment. +_COMPONENT_NAME_THIRD_SEGMENT_TYPES = frozenset({"comp_arc_sta", "comp_arc_dyn"}) + + +def _id_name_segment(need_id: str, need_type: str | None = None) -> str | None: + """Return the feature/component name encoded in a need ID. + + Need IDs follow the convention ``____`` (e.g. + ``feat_req__baselibs__core_utilities``); the second ``__``-separated segment + is the feature/component name. The ``comp_arc_sta`` and ``comp_arc_dyn`` + types are an exception: their IDs follow + ``____`` (e.g. + ``comp_arc_sta__baselibs__filesystem``), so the *third* segment holds the + component name. Returns ``None`` when the ID does not follow the convention. + """ + parts = need_id.split("__") + if need_type in _COMPONENT_NAME_THIRD_SEGMENT_TYPES: + if len(parts) < 3 or not parts[2]: + return None + return parts[2] + if len(parts) < 2 or not parts[1]: + return None + return parts[1] def _keep_need( + need_id: str, need: dict[str, Any], types: set[str], - components: set[str], - component_attr: str, + names: set[str], ) -> bool: if types and need.get("type") not in types: return False - if components: - values = set(_attribute_values(need, component_attr)) - if values.isdisjoint(components): + if names: + segment = _id_name_segment(need_id, need.get("type")) + if segment is None or segment not in names: return False return True @@ -68,8 +88,7 @@ def _keep_need( def filter_needs( data: dict[str, Any], types: set[str], - components: set[str], - component_attr: str, + names: set[str], ) -> dict[str, Any]: """Return a copy of ``data`` keeping only the needs that match the filters.""" for version in data.get("versions", {}).values(): @@ -77,7 +96,7 @@ def filter_needs( version["needs"] = { need_id: need for need_id, need in needs.items() - if _keep_need(need, types, components, component_attr) + if _keep_need(need_id, need, types, names) } return data @@ -106,22 +125,16 @@ def main() -> int: ), ) _ = parser.add_argument( - "--component", - dest="components", + "--name", + dest="names", action="append", default=[], - metavar="COMPONENT", - help=( - "Component name to keep. May be given multiple times. " - "If omitted, all components are kept." - ), - ) - _ = parser.add_argument( - "--component-attr", - default="component", + metavar="NAME", help=( - "Need attribute matched against the values given via --component. " - "Defaults to 'component'." + "Feature/component name to keep, matched against the second " + "'__'-separated segment of each need ID (the '____...' " + "naming convention). May be given multiple times. If omitted, all " + "features/components are kept." ), ) _ = parser.add_argument( @@ -138,8 +151,7 @@ def main() -> int: filtered = filter_needs( data, types=set(args.types), - components=set(args.components), - component_attr=args.component_attr, + names=set(args.names), ) kept = sum( @@ -147,12 +159,12 @@ def main() -> int: for version in filtered.get("versions", {}).values() ) logger.info( - "Filtered '%s' -> '%s' (%d needs kept, types=%s, components=%s)", + "Filtered '%s' -> '%s' (%d needs kept, types=%s, names=%s)", args.input, args.output, kept, sorted(args.types) or "ALL", - sorted(args.components) or "ALL", + sorted(args.names) or "ALL", ) args.output.parent.mkdir(parents=True, exist_ok=True) diff --git a/scripts_bazel/validate_checklist.py b/scripts_bazel/validate_checklist.py index 75d7b8b80..9bbf42aa9 100644 --- a/scripts_bazel/validate_checklist.py +++ b/scripts_bazel/validate_checklist.py @@ -19,10 +19,30 @@ This script: 1. Reads ``needs.json`` and looks up the checklist need by its id. -2. Computes the SHA256 over the concatenated input files (sorted by path, so the - result is independent of the order in which Bazel passes them). +2. Computes the SHA256 over the validated build output. 3. Compares the computed hash with the ``sha256`` attribute of the checklist need. +There are two hashing modes: + +* **Flat** (no ``--link-field`` given): the SHA256 is computed over the + concatenated input files (sorted by path, so the result is independent of the + order in which Bazel passes them). This pins exactly the elements contained in + the input files. +* **Transitive** (one or more ``--link-field`` given): the input files only + define the *root* elements (e.g. the extracted feature requirements). Starting + from those roots, the given link fields (e.g. ``derived_from``, ``satisfies``) + are followed recursively through the full ``needs.json``, collecting every + reachable element (e.g. the stakeholder requirements a feature requirement is + derived from, and their parents in turn). The SHA256 is computed over the + canonical serialization of the whole closure. As a result the checklist also + goes out of date when an *upstream* dependency (such as a linked stakeholder + requirement) changes, not just when a root element changes. + + Reachable elements whose full content does not live in ``--needs-json`` (e.g. + requirements or architecture imported from another repository) must be + supplied via ``--extra-needs-json``; otherwise they are hashed as ```` + and changes to them are not detected. + On match it writes the verified hash to ``--output`` and exits ``0``. On mismatch (or when the need / attribute is missing) it logs the expected and actual hashes and exits ``1``, which fails the Bazel build. @@ -49,6 +69,63 @@ def find_need(data: dict[str, Any], need_id: str) -> dict[str, Any] | None: return None +def collect_needs(data: dict[str, Any]) -> dict[str, dict[str, Any]]: + """Return a flat ``{id: need}`` mapping of every need in a needs.json structure.""" + all_needs: dict[str, dict[str, Any]] = {} + for version in data.get("versions", {}).values(): + for need_id, need in version.get("needs", {}).items(): + all_needs[need_id] = need + return all_needs + + +def _link_targets(need: dict[str, Any], field: str) -> list[str]: + """Return the link targets stored under ``field`` of ``need`` as a list. + + sphinx-needs may store link targets with a version constraint suffix such as + ``stkh_req__foo[version==1]``. The constraint is stripped so the target + matches the plain need id used as the key in ``needs.json``. + """ + value = need.get(field) + if value is None: + return [] + raw = value if isinstance(value, list) else [value] + return [_normalize_id(str(v)) for v in raw if str(v).strip()] # pyright: ignore[reportUnknownArgumentType] + + +def _normalize_id(need_id: str) -> str: + """Strip a trailing ``[...]`` version constraint and whitespace from ``need_id``.""" + return need_id.split("[", 1)[0].strip() + + +def compute_closure( + all_needs: dict[str, dict[str, Any]], + roots: set[str], + link_fields: list[str], +) -> set[str]: + """Return the transitive closure of ``roots`` following ``link_fields``. + + Starting from ``roots`` every link target reachable via one of the + ``link_fields`` is collected recursively. Missing link targets (ids that are + not present in ``all_needs``) are kept in the result so that a dangling link + still influences the hash deterministically. + """ + seen: set[str] = set() + stack: list[str] = list(roots) + while stack: + need_id = stack.pop() + if need_id in seen: + continue + seen.add(need_id) + need = all_needs.get(need_id) + if need is None: + continue + for field in link_fields: + for target in _link_targets(need, field): + if target not in seen: + stack.append(target) + return seen + + def compute_sha256(paths: list[Path]) -> str: """Return the SHA256 over the concatenated contents of ``paths`` (sorted).""" digest = hashlib.sha256() @@ -57,6 +134,61 @@ def compute_sha256(paths: list[Path]) -> str: return digest.hexdigest() +def _canonicalize_need(need: dict[str, Any]) -> dict[str, Any]: + """Return a copy of ``need`` with sphinx-needs back-link lists order-normalized. + + sphinx-needs auto-populates the reverse-link fields (every link field has a + corresponding ``_back``) in document-processing order, which is *not* + stable with respect to unrelated changes: editing an unrelated ``.rst`` file + can reorder the entries of a ``*_back`` list without changing its contents. + Because the full need dict is hashed, that spurious reordering would + invalidate the checklist even though nothing semantically relevant changed. + + Sorting the (string) entries of every ``*_back`` list makes the serialization + order-independent so only genuine changes to the set of back-links affect the + hash. Forward link fields are author-specified and deterministic, so they are + left untouched. + """ + normalized = dict(need) + for key, value in normalized.items(): + if key.endswith("_back") and isinstance(value, list): + normalized[key] = sorted( # pyright: ignore[reportUnknownArgumentType] + value, + key=lambda item: str(item), # pyright: ignore[reportUnknownLambdaType] + ) + return normalized + + +def compute_closure_sha256( + all_needs: dict[str, dict[str, Any]], + ids: set[str], +) -> str: + """Return the SHA256 over the canonical serialization of ``ids`` in ``all_needs``. + + Needs are serialized sorted by id, each as a deterministic JSON object + (``sort_keys=True``). The full need dict is hashed, so any change to a + reachable element - including its content, attributes or links - changes the + result. The auto-generated ``*_back`` link lists are order-normalized first + (see ``_canonicalize_need``) so that unrelated edits which only reshuffle + those reverse links do not invalidate the hash. Ids without a matching need + are still mixed into the digest so that a dangling link is detected too. + """ + digest = hashlib.sha256() + for need_id in sorted(ids): + digest.update(need_id.encode("utf-8")) + digest.update(b"\x00") + need = all_needs.get(need_id) + if need is None: + digest.update(b"") + else: + payload = json.dumps( + _canonicalize_need(need), sort_keys=True, ensure_ascii=False + ) + digest.update(payload.encode("utf-8")) + digest.update(b"\x00") + return digest.hexdigest() + + def main() -> int: parser = argparse.ArgumentParser( description=( @@ -81,6 +213,35 @@ def main() -> int: type=Path, help="Path of the stamp file to write with the verified hash on success.", ) + _ = parser.add_argument( + "--link-field", + dest="link_fields", + action="append", + default=[], + metavar="FIELD", + help=( + "Link field to follow recursively from the input (root) elements when " + "computing the hash (e.g. 'derived_from'). May be given multiple " + "times. If omitted, only the input files themselves are hashed " + "(no transitive dependencies)." + ), + ) + _ = parser.add_argument( + "--extra-needs-json", + dest="extra_needs_json", + action="append", + default=[], + type=Path, + metavar="PATH", + help=( + "Additional needs.json file providing the full content of needs that " + "are referenced from the validated elements but are not contained in " + "the main --needs-json (e.g. requirements/architecture imported from " + "an upstream repository). Used in transitive mode to resolve and hash " + "such external needs; without it they are hashed as and " + "changes to them go undetected. May be given multiple times." + ), + ) _ = parser.add_argument( "inputs", nargs="+", @@ -104,7 +265,49 @@ def main() -> int: expected = need.get("sha256") - actual = compute_sha256(args.inputs) + if args.link_fields: + # Transitive mode: the input files define the root elements; follow the + # given link fields recursively through the full needs.json and hash the + # whole closure (roots + all reachable dependencies). + root_needs: dict[str, dict[str, Any]] = {} + for path in args.inputs: + with open(path) as f: + root_needs.update(collect_needs(json.load(f))) + roots = set(root_needs.keys()) + + # Build the authoritative content/link graph. Extra needs.json sources + # (e.g. an upstream repository) only fill in needs that are missing + # locally, so the main --needs-json keeps precedence for local content, + # and the extracted root elements keep precedence over both. Without the + # extra sources, external needs referenced by the validated elements are + # absent from the graph and get hashed as , so changes to them + # are invisible to the checklist. + all_needs: dict[str, dict[str, Any]] = {} + for extra_path in args.extra_needs_json: + with open(extra_path) as f: + all_needs.update(collect_needs(json.load(f))) + all_needs.update(collect_needs(data)) + all_needs.update(root_needs) + + closure = compute_closure(all_needs, roots, args.link_fields) + dependencies = closure - roots + logger.info( + "Checklist '%s': hashing %d element(s) (%d root(s) + %d " + "transitive dependency/ies via %s).", + args.checklist_id, + len(closure), + len(roots), + len(dependencies), + ", ".join(args.link_fields), + ) + if dependencies: + logger.info( + " transitive dependencies: %s", + ", ".join(sorted(dependencies)), + ) + actual = compute_closure_sha256(all_needs, closure) + else: + actual = compute_sha256(args.inputs) if not expected: logger.error( diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index 28e741247..f31665845 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -406,8 +406,11 @@ needs_types: # A reviewable checklist element that pins the state of a set of build outputs # (e.g. the extracted component requirements) via a SHA256 hash. It references # the checklist document (the rendered `.rst`) and the Bazel target(s) whose - # output is validated against `sha256`. See the `requirements_checklist` - # Bazel rule in `docs.bzl`. + # output is validated against `sha256`. By default the hash also covers the + # transitive sphinx-needs dependencies of those outputs (e.g. the stakeholder + # requirements a feature requirement is derived from), so the checklist goes + # out of date when an upstream element changes too. See the + # `requirements_checklist` Bazel rule in `docs.bzl`. req_chklst: title: Requirement Checklist prefix: req_chklst__ @@ -415,10 +418,10 @@ needs_types: # req-Id: tool_req__docs_common_attr_status status: ^(valid|draft|invalid)$ optional_options: - # SHA256 (64 lowercase hex chars) of the concatenated target outputs that - # this checklist was reviewed against. May be left empty to let the - # `requirements_checklist` build fail and report the computed SHA256 to - # pin (see `validate_checklist.py`). + # SHA256 (64 lowercase hex chars) of the reviewed build output (the root + # target outputs plus their transitive dependencies). May be left empty to + # let the `requirements_checklist` build fail and report the computed + # SHA256 to pin (see `validate_checklist.py`). sha256: ^([0-9a-f]{64})?$ # Bazel target label(s) whose output is validated against `sha256`. # Multiple labels may be separated by spaces or commas. @@ -432,7 +435,10 @@ needs_types: # A reviewable checklist element that pins the state of a set of build outputs # (e.g. the extracted feature/component architecture) via a SHA256 hash. It # references the checklist document (the rendered `.rst`) and the Bazel - # target(s) whose output is validated against `sha256`. See the + # target(s) whose output is validated against `sha256`. By default the hash + # also covers the transitive sphinx-needs dependencies of those outputs (e.g. + # the requirements an architecture element fulfils and their parents), so the + # checklist goes out of date when an upstream element changes too. See the # `architecture_checklist` Bazel rule in `docs.bzl`. arch_chklst: title: Architecture Checklist @@ -441,10 +447,10 @@ needs_types: # req-Id: tool_req__docs_common_attr_status status: ^(valid|draft|invalid)$ optional_options: - # SHA256 (64 lowercase hex chars) of the concatenated target outputs that - # this checklist was reviewed against. May be left empty to let the - # `architecture_checklist` build fail and report the computed SHA256 to - # pin (see `validate_checklist.py`). + # SHA256 (64 lowercase hex chars) of the reviewed build output (the root + # target outputs plus their transitive dependencies). May be left empty to + # let the `architecture_checklist` build fail and report the computed + # SHA256 to pin (see `validate_checklist.py`). sha256: ^([0-9a-f]{64})?$ # Bazel target label(s) whose output is validated against `sha256`. # Multiple labels may be separated by spaces or commas. From f49c22bec63de4dc2f06c3970a90ae74f4349994 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Mon, 29 Jun 2026 06:48:20 +0000 Subject: [PATCH 10/12] docs: align verification evidence requirements and checklist rules --- docs.bzl | 60 +++++++------- docs/internals/requirements/requirements.rst | 4 +- scripts_bazel/README_needs_rules.md | 82 +++++++++++-------- scripts_bazel/validate_checklist.py | 12 +-- src/extensions/score_metamodel/metamodel.yaml | 72 +++------------- .../test_options_verification_evidence.rst | 6 +- 6 files changed, 102 insertions(+), 134 deletions(-) diff --git a/docs.bzl b/docs.bzl index fe77770c0..313657930 100644 --- a/docs.bzl +++ b/docs.bzl @@ -378,20 +378,20 @@ def sphinx_needs_to_trlc( def requirements_checklist( name, - checklist_id, deps, + mod_insp_id, src = "//:needs_json", link_fields = ["derived_from", "satisfies", "covers"], extra_needs = [], visibility = None): - """Validate a requirement checklist (`req_chklst`) against its build output. + """Validate a requirement inspection record against its build output. Building this target recomputes the SHA256 over the requirements in `deps` **and**, by default, over everything they depend on transitively, and - compares it to the `sha256` attribute of the `req_chklst` need `checklist_id` - (looked up in `src`'s `needs.json`). The build **fails** when the hashes - differ, i.e. when a validated requirement *or one of its (recursive) - dependencies* has changed since the checklist was last reviewed. + compares it to the `sha256` attribute of the `mod_insp` inspection record + `mod_insp_id` (looked up in `src`'s `needs.json`). The build **fails** when + the hashes differ, i.e. when a validated requirement *or one of its + (recursive) dependencies* has changed since the inspection was last reviewed. The dependency graph is the sphinx-needs link graph: starting from the requirements in `deps` (the *roots*), the `link_fields` (by default @@ -403,7 +403,7 @@ def requirements_checklist( the requirements in `deps`. Typical usage validates the extracted requirements of a component against the - checklist that reviewed them: + inspection record that reviewed them: component_requirements( name = "bitmanipulation_comp_reqs", @@ -412,24 +412,24 @@ def requirements_checklist( requirements_checklist( name = "bitmanipulation_req_checklist", - checklist_id = "req_chklst__bitmanipulation__comp_req", + mod_insp_id = "mod_insp__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], ) Run with `bazel build //:bitmanipulation_req_checklist`. On the first run (or after the requirements change) the build fails and prints the actual SHA256; - copy it into the `sha256` attribute of the checklist need once the checklist - has been (re-)reviewed. + copy it into the `sha256` attribute of the inspection record once it has + been (re-)reviewed. Args: name: Name of the generated target. The output file is `.sha256`. - checklist_id: Id of the `req_chklst` need to validate - (e.g. `"req_chklst__bitmanipulation__comp_req"`). deps: List of labels whose outputs define the root requirements that are hashed and validated. Usually a single `component_requirements`/`feature_requirements`/`filtered_needs_json` target. - src: Label of a `needs_json` build output containing the checklist need + mod_insp_id: Id of the `mod_insp` inspection record to validate + (e.g. `"mod_insp__bitmanipulation__comp_req"`). + src: Label of a `needs_json` build output containing the inspection record and the full link graph. Defaults to the calling package's `//:needs_json`. link_fields: Sphinx-needs link fields followed recursively from the root @@ -458,14 +458,14 @@ def requirements_checklist( cmd = """ $(location {validate_tool}) \ --needs-json $(location {src})/needs.json \ - --checklist-id '{checklist_id}' \ + --checklist-id '{mod_insp_id}' \ --output $@ \ {link_args} \ {extra_args} \ {dep_args} """.format( validate_tool = validate_tool, - checklist_id = checklist_id, + mod_insp_id = mod_insp_id, src = src, link_args = link_args, extra_args = extra_args, @@ -477,20 +477,20 @@ def requirements_checklist( def architecture_checklist( name, - checklist_id, deps, + mod_insp_id, src = "//:needs_json", link_fields = ["fulfils", "includes", "uses", "provides", "derived_from", "satisfies", "covers"], extra_needs = [], visibility = None): - """Validate an architecture checklist (`arch_chklst`) against its build output. + """Validate an architecture inspection record against its build output. Building this target recomputes the SHA256 over the architecture in `deps` **and**, by default, over everything they depend on transitively, and - compares it to the `sha256` attribute of the `arch_chklst` need `checklist_id` - (looked up in `src`'s `needs.json`). The build **fails** when the hashes - differ, i.e. when a validated architecture element *or one of its (recursive) - dependencies* has changed since the checklist was last reviewed. + compares it to the `sha256` attribute of the `mod_insp` inspection record + `mod_insp_id` (looked up in `src`'s `needs.json`). The build **fails** when + the hashes differ, i.e. when a validated architecture element *or one of its + (recursive) dependencies* has changed since the inspection was last reviewed. The dependency graph is the sphinx-needs link graph: starting from the architecture elements in `deps` (the *roots*), the `link_fields` are followed @@ -503,7 +503,7 @@ def architecture_checklist( behaviour of hashing only the elements in `deps`. Typical usage validates the extracted architecture of a component against the - checklist that reviewed it: + inspection record that reviewed it: component_architecture( name = "bitmanipulation_comp_arch", @@ -512,24 +512,24 @@ def architecture_checklist( architecture_checklist( name = "bitmanipulation_arch_checklist", - checklist_id = "arch_chklst__bitmanipulation__comp_arc", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", deps = [":bitmanipulation_comp_arch"], ) Run with `bazel build //:bitmanipulation_arch_checklist`. On the first run (or after the architecture changes) the build fails and prints the actual SHA256; - copy it into the `sha256` attribute of the checklist need once the checklist - has been (re-)reviewed. + copy it into the `sha256` attribute of the inspection record once it has + been (re-)reviewed. Args: name: Name of the generated target. The output file is `.sha256`. - checklist_id: Id of the `arch_chklst` need to validate - (e.g. `"arch_chklst__bitmanipulation__comp_arc"`). deps: List of labels whose outputs define the root architecture elements that are hashed and validated. Usually a single `feature_architecture`/`component_architecture`/`filtered_needs_json` target. - src: Label of a `needs_json` build output containing the checklist need + mod_insp_id: Id of the `mod_insp` inspection record to validate + (e.g. `"mod_insp__baselibs__feat_arc"`). + src: Label of a `needs_json` build output containing the inspection record and the full link graph. Defaults to the calling package's `//:needs_json`. link_fields: Sphinx-needs link fields followed recursively from the root @@ -558,14 +558,14 @@ def architecture_checklist( cmd = """ $(location {validate_tool}) \ --needs-json $(location {src})/needs.json \ - --checklist-id '{checklist_id}' \ + --checklist-id '{mod_insp_id}' \ --output $@ \ {link_args} \ {extra_args} \ {dep_args} """.format( validate_tool = validate_tool, - checklist_id = checklist_id, + mod_insp_id = mod_insp_id, src = src, link_args = link_args, extra_args = extra_args, diff --git a/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index bb47d4550..71550ce65 100644 --- a/docs/internals/requirements/requirements.rst +++ b/docs/internals/requirements/requirements.rst @@ -901,8 +901,10 @@ Testing * use ``mod_insp`` as directive type * classify the inspection by ``inspection_type`` and ``inspection_state`` - * record the checklist reference and reviewer list via ``checklist_ref`` and ``reviewers`` + * record the checklist template reference via ``checklist_template`` + * record the reviewer list via ``reviewers`` * link the inspection to the verified module via ``belongs_to`` + * link the filled checklist document via ``checklist`` * link the inspected artifacts via ``inspects`` * allow links to backing evidence via ``evidence`` diff --git a/scripts_bazel/README_needs_rules.md b/scripts_bazel/README_needs_rules.md index b9c22c5f2..c6b14541a 100644 --- a/scripts_bazel/README_needs_rules.md +++ b/scripts_bazel/README_needs_rules.md @@ -17,9 +17,9 @@ the backing Python tools (in this folder) to: 2. **Render** the selected elements as a human readable Markdown document. 3. **Convert** S-CORE requirement elements into [TRLC](https://github.com/bmw-software-engineering/trlc) data targeting the S-CORE requirements metamodel. -4. **Validate** a reviewable *requirement checklist* against the build output it - was reviewed against, by pinning a SHA256 hash in a `req_chklst` sphinx-needs - element and failing the build when the output drifts. +4. **Validate** a reviewable *requirement / architecture checklist* against the + build output it was reviewed against, by pinning a SHA256 hash on a + `mod_insp` inspection record and failing the build when the output drifts. The goal is to show how requirements managed as sphinx-needs can be bridged to other consumers (review docs, TRLC-based tooling) without manual copying. @@ -38,8 +38,8 @@ other consumers (review docs, TRLC-based tooling) without manual copying. | `component_architecture` | `.json` | Convenience wrapper for `comp_arc_sta` / `comp_arc_dyn` elements. | | `sphinx_needs_to_md` | `.md` | Render needs as a Markdown document. | | `sphinx_needs_to_trlc` | `.trlc` | Convert S-CORE requirements to TRLC. | -| `requirements_checklist` | `.sha256` | Validate a `req_chklst` need against its target output via SHA256. | -| `architecture_checklist` | `.sha256` | Validate an `arch_chklst` need against its target output via SHA256. | +| `requirements_checklist` | `.sha256` | Validate a `mod_insp` record against its requirements target output via SHA256. | +| `architecture_checklist` | `.sha256` | Validate a `mod_insp` record against its architecture target output via SHA256. | ### Python tools (`scripts_bazel/`) @@ -52,15 +52,12 @@ The matching `py_binary` targets are declared in [BUILD](BUILD). ### Metamodel -A new `req_chklst` need type is added in -[metamodel.yaml](../src/extensions/score_metamodel/metamodel.yaml). It carries a -mandatory `sha256` attribute, an optional `targets` attribute (the Bazel labels -it validates), and an optional `checklist` link to the rendered checklist -document. - -The analogous `arch_chklst` need type (validated by `architecture_checklist`) -is defined in the same file and works the same way for feature/component -architecture outputs. +The `mod_insp` inspection record need type in +[metamodel.yaml](../src/extensions/score_metamodel/metamodel.yaml) carries an +optional `sha256` attribute (the reviewed build output hash), a mandatory +`checklist` link to the rendered checklist document, and an `inspects` link to +the inspected artifacts. The same need type is validated by both +`requirements_checklist` and `architecture_checklist`. ## How to use @@ -136,40 +133,57 @@ checklist is considered stale and the build fails until it is re-reviewed and th hash updated. There are two checklist kinds. They behave identically and differ only in the -need type they pin and the macro that validates them: +macro that validates them; both pin the reviewed state on a `mod_insp` +inspection record: | Kind | Need type | Validating macro | Pins the reviewed state of โ€ฆ | | --- | --- | --- | --- | -| Requirements | `req_chklst` | `requirements_checklist` | extracted requirements (`component_requirements` / `feature_requirements`) and, by default, their transitive dependencies | -| Architecture | `arch_chklst` | `architecture_checklist` | extracted architecture (`component_architecture` / `feature_architecture`) and, by default, their transitive dependencies | +| Requirements | `mod_insp` | `requirements_checklist` | extracted requirements (`component_requirements` / `feature_requirements`) and, by default, their transitive dependencies | +| Architecture | `mod_insp` | `architecture_checklist` | extracted architecture (`component_architecture` / `feature_architecture`) and, by default, their transitive dependencies | The flow below is a complete, real example from the [baselibs](https://github.com/eclipse-score/baselibs) repository, which wires both kinds for its `bitmanipulation` component. You can copy it directly. -### 1. Declare the checklist need +### 1. Declare the inspection record need -Add the checklist element next to the inspection `.rst`. It references the -checklist document and the expected hash. On the -first build leave the `sha256` as the placeholder (all zeros) and pin the real -value afterwards. +Add the `mod_insp` inspection record next to the inspection `.rst`. It links the +inspected artifacts (`inspects`), the filled checklist document (`checklist`) +and stores the expected hash. On the first build leave the `sha256` as the +placeholder (all zeros) and pin the real value afterwards. -Requirements (`req_chklst`): +Requirements (`mod_insp`): ```rst -.. req_chklst:: Bitmanipulation Component Requirements Checklist - :id: req_chklst__bitmanipulation__comp_req +.. mod_insp:: Bitmanipulation Component Requirements Inspection Record + :id: mod_insp__bitmanipulation__comp_req :status: valid + :safety: ASIL_B + :security: YES + :inspection_type: requirements + :inspection_state: approved + :checklist_template: gd_chklst__req_inspection + :reviewers: mihajlo-k + :belongs_to: mod__baselibs + :inspects: comp_req__bitmanipulation__bit_operations :checklist: doc__bitmanipulation_req_inspection :sha256: 0000000000000000000000000000000000000000000000000000000000000000 ``` -Architecture (`arch_chklst`): +Architecture (`mod_insp`): ```rst -.. arch_chklst:: Bitmanipulation Component Architecture Checklist - :id: arch_chklst__bitmanipulation__comp_arc +.. mod_insp:: Bitmanipulation Component Architecture Inspection Record + :id: mod_insp__bitmanipulation__comp_arc :status: valid + :safety: ASIL_B + :security: YES + :inspection_type: architecture + :inspection_state: approved + :checklist_template: gd_chklst__arch_inspection_checklist + :reviewers: aschemmel-tech + :belongs_to: mod__baselibs + :inspects: comp_arc_sta__bitmanipulation__component_view :checklist: doc__bitmanipulation_arc_inspection :sha256: 0000000000000000000000000000000000000000000000000000000000000000 ``` @@ -191,7 +205,7 @@ component_requirements( requirements_checklist( name = "bitmanipulation_req_checklist", - checklist_id = "req_chklst__bitmanipulation__comp_req", + mod_insp_id = "mod_insp__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], # Resolve upstream stakeholder requirements so changes to them are detected. extra_needs = ["@score_platform//:needs_json"], @@ -210,7 +224,7 @@ component_architecture( architecture_checklist( name = "bitmanipulation_arch_checklist", - checklist_id = "arch_chklst__bitmanipulation__comp_arc", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", deps = [":bitmanipulation_comp_arch"], # Resolve upstream (feature/stakeholder) requirements so changes are detected. extra_needs = ["@score_platform//:needs_json"], @@ -274,7 +288,7 @@ transitively linked elements). Link targets carrying a version constraint # Component requirements: transitively pins feature + stakeholder requirements. requirements_checklist( name = "bitmanipulation_req_checklist", - checklist_id = "req_chklst__bitmanipulation__comp_req", + mod_insp_id = "mod_insp__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], # default: link_fields = ["derived_from", "satisfies", "covers"] ) @@ -282,7 +296,7 @@ requirements_checklist( # Architecture: transitively pins fulfilled requirements (and their parents). architecture_checklist( name = "bitmanipulation_arch_checklist", - checklist_id = "arch_chklst__bitmanipulation__comp_arc", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", deps = [":bitmanipulation_comp_arch"], ) ``` @@ -310,14 +324,14 @@ pass as `data` to `docs(...)`: ```starlark requirements_checklist( name = "bitmanipulation_req_checklist", - checklist_id = "req_chklst__bitmanipulation__comp_req", + mod_insp_id = "mod_insp__bitmanipulation__comp_req", deps = [":bitmanipulation_comp_reqs"], extra_needs = ["@score_platform//:needs_json"], ) architecture_checklist( name = "bitmanipulation_arch_checklist", - checklist_id = "arch_chklst__bitmanipulation__comp_arc", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", deps = [":bitmanipulation_comp_arch"], extra_needs = ["@score_platform//:needs_json"], ) diff --git a/scripts_bazel/validate_checklist.py b/scripts_bazel/validate_checklist.py index 9bbf42aa9..3f3b44427 100644 --- a/scripts_bazel/validate_checklist.py +++ b/scripts_bazel/validate_checklist.py @@ -12,15 +12,15 @@ # ******************************************************************************* """ -Validate a requirement checklist against the build output it was reviewed against. +Validate an inspection record against the build output it was reviewed against. -A ``req_chklst`` sphinx-needs element pins the state of one or more build +A ``mod_insp`` sphinx-needs element pins the state of one or more build outputs (e.g. the extracted component requirements) via a ``sha256`` attribute. This script: -1. Reads ``needs.json`` and looks up the checklist need by its id. +1. Reads ``needs.json`` and looks up the inspection record by its id. 2. Computes the SHA256 over the validated build output. -3. Compares the computed hash with the ``sha256`` attribute of the checklist need. +3. Compares the computed hash with the ``sha256`` attribute of the record. There are two hashing modes: @@ -192,7 +192,7 @@ def compute_closure_sha256( def main() -> int: parser = argparse.ArgumentParser( description=( - "Validate a requirement checklist (req_chklst) against the SHA256 of " + "Validate an inspection record (mod_insp) against the SHA256 of " "the build output it was reviewed against." ) ) @@ -205,7 +205,7 @@ def main() -> int: _ = parser.add_argument( "--checklist-id", required=True, - help="Id of the req_chklst need to validate (e.g. 'req_chklst__foo').", + help="Id of the mod_insp need to validate (e.g. 'mod_insp__foo').", ) _ = parser.add_argument( "--output", diff --git a/src/extensions/score_metamodel/metamodel.yaml b/src/extensions/score_metamodel/metamodel.yaml index afc6bac3c..1bfcdc2ea 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -402,64 +402,6 @@ needs_types: - requirement_excl_process parts: 3 - # Requirement Checklist - # A reviewable checklist element that pins the state of a set of build outputs - # (e.g. the extracted component requirements) via a SHA256 hash. It references - # the checklist document (the rendered `.rst`) and the Bazel target(s) whose - # output is validated against `sha256`. By default the hash also covers the - # transitive sphinx-needs dependencies of those outputs (e.g. the stakeholder - # requirements a feature requirement is derived from), so the checklist goes - # out of date when an upstream element changes too. See the - # `requirements_checklist` Bazel rule in `docs.bzl`. - req_chklst: - title: Requirement Checklist - prefix: req_chklst__ - mandatory_options: - # req-Id: tool_req__docs_common_attr_status - status: ^(valid|draft|invalid)$ - optional_options: - # SHA256 (64 lowercase hex chars) of the reviewed build output (the root - # target outputs plus their transitive dependencies). May be left empty to - # let the `requirements_checklist` build fail and report the computed - # SHA256 to pin (see `validate_checklist.py`). - sha256: ^([0-9a-f]{64})?$ - # Bazel target label(s) whose output is validated against `sha256`. - # Multiple labels may be separated by spaces or commas. - targets: ^.*$ - optional_links: - # Link to the checklist document (the rendered `.rst` checklist file). - checklist: document - parts: 3 - - # Architecture Checklist - # A reviewable checklist element that pins the state of a set of build outputs - # (e.g. the extracted feature/component architecture) via a SHA256 hash. It - # references the checklist document (the rendered `.rst`) and the Bazel - # target(s) whose output is validated against `sha256`. By default the hash - # also covers the transitive sphinx-needs dependencies of those outputs (e.g. - # the requirements an architecture element fulfils and their parents), so the - # checklist goes out of date when an upstream element changes too. See the - # `architecture_checklist` Bazel rule in `docs.bzl`. - arch_chklst: - title: Architecture Checklist - prefix: arch_chklst__ - mandatory_options: - # req-Id: tool_req__docs_common_attr_status - status: ^(valid|draft|invalid)$ - optional_options: - # SHA256 (64 lowercase hex chars) of the reviewed build output (the root - # target outputs plus their transitive dependencies). May be left empty to - # let the `architecture_checklist` build fail and report the computed - # SHA256 to pin (see `validate_checklist.py`). - sha256: ^([0-9a-f]{64})?$ - # Bazel target label(s) whose output is validated against `sha256`. - # Multiple labels may be separated by spaces or commas. - targets: ^.*$ - optional_links: - # Link to the checklist document (the rendered `.rst` checklist file). - checklist: document - parts: 3 - # - Architecture - # Architecture Element @@ -1025,7 +967,7 @@ needs_types: # req-Id: tool_req__docs_inspection_record_need inspection_type: ^(requirements|architecture|implementation|traceability|safety_analysis|security_analysis|other)$ inspection_state: ^(planned|in_review|rework_required|approved)$ - checklist_ref: ^.*$ + checklist_template: ^.*$ reviewers: ^.*$ optional_options: checklist_type: ^(req|arc|impl|safety|security|custom)$ @@ -1036,10 +978,18 @@ needs_types: pr_link: ^https://github\.com/[^/]+/[^/]+/pull/\d+$ correction_issue: ^https://github\.com/[^/]+/[^/]+/issues/\d+$ inspection_date: ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ + # SHA256 (64 lowercase hex chars) of the reviewed build output (the root + # target outputs plus their transitive dependencies). May be left empty to + # let the checklist build fail and report the computed SHA256 to pin + # (see `validate_checklist.py`). + sha256: ^([0-9a-f]{64})?$ mandatory_links: # req-Id: tool_req__docs_inspection_record_need - belongs_to: mod inspects: ANY + # Link to the filled checklist document (the rendered `.rst` checklist file). + checklist: document + # Link to the verified module. + belongs_to: mod optional_links: # req-Id: tool_req__docs_inspection_record_need contains: ANY @@ -1125,7 +1075,7 @@ needs_extra_links: incoming: covered by outgoing: covers - # Requirement Checklist -> checklist document (rendered .rst) + # Inspection record / checklist -> checklist document (rendered .rst) checklist: incoming: is checklist for outgoing: checklist document diff --git a/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst b/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst index 14b069078..24728db89 100644 --- a/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst +++ b/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst @@ -87,7 +87,7 @@ :status: valid :inspection_type: requirements :inspection_state: approved - :checklist_ref: gd_chklst__req_inspection + :checklist_template: gd_chklst__req_inspection :reviewers: reviewer_a,reviewer_b :checklist_type: req :findings_total: 1 @@ -95,6 +95,7 @@ :inspection_date: 2026-06-24 :belongs_to: mod__verification_module :inspects: comp_req__verification__sample + :checklist: doc__verification__filled_checklist .. Invalid inspection_state value in module inspection record @@ -107,7 +108,8 @@ :status: invalid :inspection_type: architecture :inspection_state: approved_late - :checklist_ref: gd_chklst__arch_inspection_checklist + :checklist_template: gd_chklst__arch_inspection_checklist :reviewers: reviewer_a :belongs_to: mod__verification_module :inspects: comp_req__verification__sample + :checklist: doc__verification__filled_checklist From a91b52da5ccfc8f36b1c5c92c9123d0754ecf4f1 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:35:13 +0000 Subject: [PATCH 11/12] Add score_module and score_component macros --- docs.bzl | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/docs.bzl b/docs.bzl index 313657930..50687e57d 100644 --- a/docs.bzl +++ b/docs.bzl @@ -575,6 +575,160 @@ def architecture_checklist( visibility = visibility, ) +def score_component( + name, + component = None, + req_insp_id = None, + arc_insp_id = None, + src = "//:needs_json", + extra_needs = [], + visibility = None): + """Bundle the requirement and architecture checklists of a single component. + + Creates the component requirement/architecture extraction targets plus their + inspection-record checklists and aggregates the two checklists into a single + target `name`. Building `name` builds both the component requirements + checklist and the component architecture checklist, so the build fails when + either drifts from its reviewed inspection record. + + The following targets are generated: + + * `_comp_reqs` - extracted component requirements + * `_req_checklist` - requirements inspection-record checklist + * `_comp_arch` - extracted component architecture + * `_arch_checklist` - architecture inspection-record checklist + * `` - filegroup bundling both checklists + + Args: + name: Name of the aggregate target and prefix for the generated targets. + component: Component name used to filter needs. Defaults to `name`. + req_insp_id: Id of the `mod_insp` record for the component requirements. + Defaults to `mod_insp____comp_req`. + arc_insp_id: Id of the `mod_insp` record for the component architecture. + Defaults to `mod_insp____comp_arc`. + src: Label of a `needs_json` build output. Defaults to `//:needs_json`. + extra_needs: Additional `needs_json` build outputs forwarded to both + checklists (e.g. `["@score_platform//:needs_json"]`). + visibility: Standard Bazel visibility for the generated targets. + """ + component = component or name + req_insp_id = req_insp_id or "mod_insp__{}__comp_req".format(component) + arc_insp_id = arc_insp_id or "mod_insp__{}__comp_arc".format(component) + + component_requirements( + name = name + "_comp_reqs", + src = src, + component = component, + visibility = visibility, + ) + requirements_checklist( + name = name + "_req_checklist", + mod_insp_id = req_insp_id, + deps = [":" + name + "_comp_reqs"], + extra_needs = extra_needs, + visibility = visibility, + ) + component_architecture( + name = name + "_comp_arch", + src = src, + component = component, + visibility = visibility, + ) + architecture_checklist( + name = name + "_arch_checklist", + mod_insp_id = arc_insp_id, + deps = [":" + name + "_comp_arch"], + extra_needs = extra_needs, + visibility = visibility, + ) + native.filegroup( + name = name, + srcs = [ + ":" + name + "_req_checklist", + ":" + name + "_arch_checklist", + ], + visibility = visibility, + ) + +def score_module( + name, + feature = None, + components = [], + req_insp_id = None, + arc_insp_id = None, + src = "//:needs_json", + extra_needs = [], + visibility = None): + """Bundle the feature checklists of a module with all of its components. + + Creates the feature requirement/architecture extraction targets plus their + inspection-record checklists and aggregates them with every component listed + in `components` into a single target `name`. Building `name` builds the + feature requirements checklist, the feature architecture checklist and all + referenced component targets, so the build fails when any of them drifts from + its reviewed inspection record. + + The following targets are generated: + + * `_feature_reqs` - extracted feature requirements + * `_req_checklist` - feature requirements inspection-record checklist + * `_feature_arch` - extracted feature architecture + * `_arch_checklist` - feature architecture inspection-record checklist + * `` - filegroup bundling both checklists and components + + Args: + name: Name of the aggregate target and prefix for the generated targets. + feature: Feature name used to filter needs. Defaults to `name`. + components: Labels of `score_component` targets making up this module. + All of them are built together with the module checklists. + req_insp_id: Id of the `mod_insp` record for the feature requirements. + Defaults to `mod_insp____feat_req`. + arc_insp_id: Id of the `mod_insp` record for the feature architecture. + Defaults to `mod_insp____feat_arc`. + src: Label of a `needs_json` build output. Defaults to `//:needs_json`. + extra_needs: Additional `needs_json` build outputs forwarded to both + checklists (e.g. `["@score_platform//:needs_json"]`). + visibility: Standard Bazel visibility for the generated targets. + """ + feature = feature or name + req_insp_id = req_insp_id or "mod_insp__{}__feat_req".format(feature) + arc_insp_id = arc_insp_id or "mod_insp__{}__feat_arc".format(feature) + + feature_requirements( + name = name + "_feature_reqs", + src = src, + feature = feature, + visibility = visibility, + ) + requirements_checklist( + name = name + "_req_checklist", + mod_insp_id = req_insp_id, + deps = [":" + name + "_feature_reqs"], + extra_needs = extra_needs, + visibility = visibility, + ) + feature_architecture( + name = name + "_feature_arch", + src = src, + feature = feature, + visibility = visibility, + ) + architecture_checklist( + name = name + "_arch_checklist", + mod_insp_id = arc_insp_id, + deps = [":" + name + "_feature_arch"], + extra_needs = extra_needs, + visibility = visibility, + ) + native.filegroup( + name = name, + srcs = [ + ":" + name + "_req_checklist", + ":" + name + "_arch_checklist", + ] + components, + visibility = visibility, + ) + def _missing_requirements(deps): """Add Python hub dependencies if they are missing.""" found = [] From 6dcc1ea25fde7fc8d8218aa45e629f9f01ad9bb4 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov <63401640+antonkri@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:46:23 +0000 Subject: [PATCH 12/12] Add score_module/score_component bundling macros --- docs.bzl | 149 +++++++++---------------------------------------------- 1 file changed, 23 insertions(+), 126 deletions(-) diff --git a/docs.bzl b/docs.bzl index 50687e57d..fdf5ddb4f 100644 --- a/docs.bzl +++ b/docs.bzl @@ -577,155 +577,52 @@ def architecture_checklist( def score_component( name, - component = None, - req_insp_id = None, - arc_insp_id = None, - src = "//:needs_json", - extra_needs = [], + req_chklst = [], + arch_chklst = [], visibility = None): """Bundle the requirement and architecture checklists of a single component. - Creates the component requirement/architecture extraction targets plus their - inspection-record checklists and aggregates the two checklists into a single - target `name`. Building `name` builds both the component requirements - checklist and the component architecture checklist, so the build fails when - either drifts from its reviewed inspection record. - - The following targets are generated: - - * `_comp_reqs` - extracted component requirements - * `_req_checklist` - requirements inspection-record checklist - * `_comp_arch` - extracted component architecture - * `_arch_checklist` - architecture inspection-record checklist - * `` - filegroup bundling both checklists + The checklist targets are defined explicitly in the BUILD file (typically + `requirements_checklist` and `architecture_checklist`) and referenced here. + Building `name` builds every referenced checklist, so the build fails when + any of them drifts from its reviewed inspection record. Args: - name: Name of the aggregate target and prefix for the generated targets. - component: Component name used to filter needs. Defaults to `name`. - req_insp_id: Id of the `mod_insp` record for the component requirements. - Defaults to `mod_insp____comp_req`. - arc_insp_id: Id of the `mod_insp` record for the component architecture. - Defaults to `mod_insp____comp_arc`. - src: Label of a `needs_json` build output. Defaults to `//:needs_json`. - extra_needs: Additional `needs_json` build outputs forwarded to both - checklists (e.g. `["@score_platform//:needs_json"]`). - visibility: Standard Bazel visibility for the generated targets. + name: Name of the aggregate target. + req_chklst: Labels of the component requirements checklist targets. + arch_chklst: Labels of the component architecture checklist targets. + visibility: Standard Bazel visibility for the generated target. """ - component = component or name - req_insp_id = req_insp_id or "mod_insp__{}__comp_req".format(component) - arc_insp_id = arc_insp_id or "mod_insp__{}__comp_arc".format(component) - - component_requirements( - name = name + "_comp_reqs", - src = src, - component = component, - visibility = visibility, - ) - requirements_checklist( - name = name + "_req_checklist", - mod_insp_id = req_insp_id, - deps = [":" + name + "_comp_reqs"], - extra_needs = extra_needs, - visibility = visibility, - ) - component_architecture( - name = name + "_comp_arch", - src = src, - component = component, - visibility = visibility, - ) - architecture_checklist( - name = name + "_arch_checklist", - mod_insp_id = arc_insp_id, - deps = [":" + name + "_comp_arch"], - extra_needs = extra_needs, - visibility = visibility, - ) native.filegroup( name = name, - srcs = [ - ":" + name + "_req_checklist", - ":" + name + "_arch_checklist", - ], + srcs = req_chklst + arch_chklst, visibility = visibility, ) def score_module( name, - feature = None, + req_chklst = [], + arch_chklst = [], components = [], - req_insp_id = None, - arc_insp_id = None, - src = "//:needs_json", - extra_needs = [], visibility = None): """Bundle the feature checklists of a module with all of its components. - Creates the feature requirement/architecture extraction targets plus their - inspection-record checklists and aggregates them with every component listed - in `components` into a single target `name`. Building `name` builds the - feature requirements checklist, the feature architecture checklist and all - referenced component targets, so the build fails when any of them drifts from - its reviewed inspection record. - - The following targets are generated: - - * `_feature_reqs` - extracted feature requirements - * `_req_checklist` - feature requirements inspection-record checklist - * `_feature_arch` - extracted feature architecture - * `_arch_checklist` - feature architecture inspection-record checklist - * `` - filegroup bundling both checklists and components + The checklist targets are defined explicitly in the BUILD file (typically + `requirements_checklist` and `architecture_checklist`) and referenced here. + Building `name` builds the referenced feature checklists and every component + listed in `components`, so the build fails when any of them drifts from its + reviewed inspection record. Args: - name: Name of the aggregate target and prefix for the generated targets. - feature: Feature name used to filter needs. Defaults to `name`. + name: Name of the aggregate target. + req_chklst: Labels of the feature requirements checklist targets. + arch_chklst: Labels of the feature architecture checklist targets. components: Labels of `score_component` targets making up this module. - All of them are built together with the module checklists. - req_insp_id: Id of the `mod_insp` record for the feature requirements. - Defaults to `mod_insp____feat_req`. - arc_insp_id: Id of the `mod_insp` record for the feature architecture. - Defaults to `mod_insp____feat_arc`. - src: Label of a `needs_json` build output. Defaults to `//:needs_json`. - extra_needs: Additional `needs_json` build outputs forwarded to both - checklists (e.g. `["@score_platform//:needs_json"]`). - visibility: Standard Bazel visibility for the generated targets. + visibility: Standard Bazel visibility for the generated target. """ - feature = feature or name - req_insp_id = req_insp_id or "mod_insp__{}__feat_req".format(feature) - arc_insp_id = arc_insp_id or "mod_insp__{}__feat_arc".format(feature) - - feature_requirements( - name = name + "_feature_reqs", - src = src, - feature = feature, - visibility = visibility, - ) - requirements_checklist( - name = name + "_req_checklist", - mod_insp_id = req_insp_id, - deps = [":" + name + "_feature_reqs"], - extra_needs = extra_needs, - visibility = visibility, - ) - feature_architecture( - name = name + "_feature_arch", - src = src, - feature = feature, - visibility = visibility, - ) - architecture_checklist( - name = name + "_arch_checklist", - mod_insp_id = arc_insp_id, - deps = [":" + name + "_feature_arch"], - extra_needs = extra_needs, - visibility = visibility, - ) native.filegroup( name = name, - srcs = [ - ":" + name + "_req_checklist", - ":" + name + "_arch_checklist", - ] + components, + srcs = req_chklst + arch_chklst + components, visibility = visibility, )