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 7f5ad0ced..fdf5ddb4f 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): @@ -96,19 +98,551 @@ def _merge_sourcelinks(name, sourcelinks, known_good = None): tools = [merge_sourcelinks_tool], ) +def filtered_needs_json( + name, + src, + types = [], + 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 features/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. + 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]) + name_args = " ".join(["--name '%s'" % n for n in names]) + + native.genrule( + name = name, + srcs = [src], + outs = [name + ".json"], + cmd = """ + $(location {filter_tool}) \ + --output $@ \ + {type_args} \ + {name_args} \ + $(location {src})/needs.json + """.format( + filter_tool = filter_tool, + type_args = type_args, + name_args = name_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 + 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"], + names = [component] if component else [], + 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 + 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"], + names = [feature] if feature else [], + 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 + 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"], + names = [component] if component else [], + 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 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"], + names = [feature] if feature else [], + 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 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"], + names = [component] if component else [], + 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 requirements_checklist( + name, + deps, + mod_insp_id, + src = "//:needs_json", + link_fields = ["derived_from", "satisfies", "covers"], + extra_needs = [], + visibility = None): + """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 `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 + `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 + inspection record that reviewed them: + + component_requirements( + name = "bitmanipulation_comp_reqs", + component = "bitmanipulation", + ) + + requirements_checklist( + name = "bitmanipulation_req_checklist", + 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 inspection record once it has + been (re-)reviewed. + + Args: + name: Name of the generated target. The output file is `.sha256`. + 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. + 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 + 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] + extra_needs + deps, + outs = [name + ".sha256"], + cmd = """ + $(location {validate_tool}) \ + --needs-json $(location {src})/needs.json \ + --checklist-id '{mod_insp_id}' \ + --output $@ \ + {link_args} \ + {extra_args} \ + {dep_args} + """.format( + validate_tool = validate_tool, + mod_insp_id = mod_insp_id, + src = src, + link_args = link_args, + extra_args = extra_args, + dep_args = dep_args, + ), + tools = [validate_tool], + visibility = visibility, + ) + +def architecture_checklist( + name, + deps, + mod_insp_id, + src = "//:needs_json", + link_fields = ["fulfils", "includes", "uses", "provides", "derived_from", "satisfies", "covers"], + extra_needs = [], + visibility = None): + """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 `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 + 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 + inspection record that reviewed it: + + component_architecture( + name = "bitmanipulation_comp_arch", + component = "bitmanipulation", + ) + + architecture_checklist( + name = "bitmanipulation_arch_checklist", + 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 inspection record once it has + been (re-)reviewed. + + Args: + name: Name of the generated target. The output file is `.sha256`. + 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. + 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 + 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] + extra_needs + deps, + outs = [name + ".sha256"], + cmd = """ + $(location {validate_tool}) \ + --needs-json $(location {src})/needs.json \ + --checklist-id '{mod_insp_id}' \ + --output $@ \ + {link_args} \ + {extra_args} \ + {dep_args} + """.format( + validate_tool = validate_tool, + mod_insp_id = mod_insp_id, + src = src, + link_args = link_args, + extra_args = extra_args, + dep_args = dep_args, + ), + tools = [validate_tool], + visibility = visibility, + ) + +def score_component( + name, + req_chklst = [], + arch_chklst = [], + visibility = None): + """Bundle the requirement and architecture checklists of a single component. + + 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. + 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. + """ + native.filegroup( + name = name, + srcs = req_chklst + arch_chklst, + visibility = visibility, + ) + +def score_module( + name, + req_chklst = [], + arch_chklst = [], + components = [], + visibility = None): + """Bundle the feature checklists of a module with all of its 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. + 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. + visibility: Standard Bazel visibility for the generated target. + """ + native.filegroup( + name = name, + srcs = req_chklst + arch_chklst + components, + 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) @@ -230,7 +764,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" @@ -240,7 +774,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( @@ -256,7 +790,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" @@ -266,7 +800,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" @@ -276,7 +810,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" @@ -286,7 +820,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/docs/internals/requirements/requirements.rst b/docs/internals/requirements/requirements.rst index edfa719a4..71550ce65 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,49 @@ 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 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`` + ๐Ÿงช Tool Verification Reports ############################ diff --git a/scripts_bazel/BUILD b/scripts_bazel/BUILD index e2d0402d2..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( @@ -45,3 +45,35 @@ 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 = [], +) + +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 new file mode 100644 index 000000000..c6b14541a --- /dev/null +++ b/scripts_bazel/README_needs_rules.md @@ -0,0 +1,377 @@ +# 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. +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. + +## 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. | +| `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 `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/`) + +- [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 + +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 + +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` and `names` 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`) 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. + +## Checklists + +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 +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 | `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 inspection record need + +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 (`mod_insp`): + +```rst +.. 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 (`mod_insp`): + +```rst +.. 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 +``` + +### 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") + +component_requirements( + name = "bitmanipulation_comp_reqs", + component = "bitmanipulation", +) + +requirements_checklist( + name = "bitmanipulation_req_checklist", + 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"], +) +``` + +Architecture `BUILD`: + +```starlark +load("@docs-as-code//:docs.bzl", "component_architecture", "architecture_checklist") + +component_architecture( + name = "bitmanipulation_comp_arch", + component = "bitmanipulation", +) + +architecture_checklist( + name = "bitmanipulation_arch_checklist", + 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"], +) +``` + +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 //: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 +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: transitively pins feature + stakeholder requirements. +requirements_checklist( + name = "bitmanipulation_req_checklist", + mod_insp_id = "mod_insp__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", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], +) +``` + +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. + +### Resolving external needs (`extra_needs`) + +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. + +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 +requirements_checklist( + name = "bitmanipulation_req_checklist", + mod_insp_id = "mod_insp__bitmanipulation__comp_req", + deps = [":bitmanipulation_comp_reqs"], + extra_needs = ["@score_platform//:needs_json"], +) + +architecture_checklist( + name = "bitmanipulation_arch_checklist", + mod_insp_id = "mod_insp__bitmanipulation__comp_arc", + deps = [":bitmanipulation_comp_arch"], + extra_needs = ["@score_platform//:needs_json"], +) +``` + +`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 (`[]`). + + + +### Gotcha: the feature/component filter matches on the need ID + +`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 `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: + +```text +comp_arc_sta__baselibs__filesystem +``` + +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 new file mode 100644 index 000000000..6964936c9 --- /dev/null +++ b/scripts_bazel/filter_needs_json.py @@ -0,0 +1,178 @@ +# ******************************************************************************* +# 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. +* ``--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. +""" + +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__) + + +# 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], + names: set[str], +) -> bool: + if types and need.get("type") not in types: + return False + if names: + segment = _id_name_segment(need_id, need.get("type")) + if segment is None or segment not in names: + return False + return True + + +def filter_needs( + data: dict[str, Any], + types: set[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(): + needs = version.get("needs", {}) + version["needs"] = { + need_id: need + for need_id, need in needs.items() + if _keep_need(need_id, need, types, names) + } + 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( + "--name", + dest="names", + action="append", + default=[], + metavar="NAME", + help=( + "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( + "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), + names=set(args.names), + ) + + kept = sum( + len(version.get("needs", {})) + for version in filtered.get("versions", {}).values() + ) + logger.info( + "Filtered '%s' -> '%s' (%d needs kept, types=%s, names=%s)", + args.input, + args.output, + kept, + sorted(args.types) or "ALL", + sorted(args.names) 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()) diff --git a/scripts_bazel/validate_checklist.py b/scripts_bazel/validate_checklist.py new file mode 100644 index 000000000..3f3b44427 --- /dev/null +++ b/scripts_bazel/validate_checklist.py @@ -0,0 +1,348 @@ +# ******************************************************************************* +# 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 an inspection record against the build output it was reviewed against. + +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 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 record. + +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. +""" + +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 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() + for path in sorted(paths, key=lambda p: p.name): + digest.update(path.read_bytes()) + 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=( + "Validate an inspection record (mod_insp) 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 mod_insp need to validate (e.g. 'mod_insp__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( + "--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="+", + 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 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( + "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 + + 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..1bfcdc2ea 100644 --- a/src/extensions/score_metamodel/metamodel.yaml +++ b/src/extensions/score_metamodel/metamodel.yaml @@ -919,6 +919,88 @@ 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])$ + 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: + # 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 + realizes: workproduct + tags: + - verification_report + 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_template: ^.*$ + 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}$ + # 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 + 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 + 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 @@ -993,6 +1075,11 @@ needs_extra_links: incoming: covered by outgoing: covers + # Inspection record / checklist -> checklist document (rendered .rst) + checklist: + incoming: is checklist for + outgoing: checklist document + # Architecture consists_of: incoming: forms part of @@ -1052,6 +1139,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/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..24728db89 --- /dev/null +++ b/src/extensions/score_metamodel/tests/rst/options/test_options_verification_evidence.rst @@ -0,0 +1,115 @@ +.. + # ******************************************************************************* + # 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_template: 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 + :checklist: doc__verification__filled_checklist + + +.. 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_template: gd_chklst__arch_inspection_checklist + :reviewers: reviewer_a + :belongs_to: mod__verification_module + :inspects: comp_req__verification__sample + :checklist: doc__verification__filled_checklist diff --git a/src/extensions/score_metamodel/tests/test_metamodel_load.py b/src/extensions/score_metamodel/tests/test_metamodel_load.py index e8aa0daa0..f70bf055d 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,13 @@ def test_load_metamodel_data(): assert defined_graph_check["check"] == { "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" + with open(schema_path, encoding="utf-8") as schema_file: + parsed = json.load(schema_file) + + assert isinstance(parsed, dict) + assert "$schema" in parsed