diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 8a2ff0c0..47792d8e 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -109,6 +109,7 @@ py_binary( main = "src/aou_forwarding_to_lobster.py", visibility = ["//visibility:public"], deps = [ + "@lobster//lobster/common", requirement("pyyaml"), ], ) diff --git a/bazel/rules/rules_score/docs/_assets/aou_forwarding_one_seooc.puml b/bazel/rules/rules_score/docs/_assets/aou_forwarding_one_seooc.puml new file mode 100644 index 00000000..e560e242 --- /dev/null +++ b/bazel/rules/rules_score/docs/_assets/aou_forwarding_one_seooc.puml @@ -0,0 +1,56 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +@startuml aou_forwarding_one_seooc + +' One SEooC receiving AoUs from its own deps, handling one locally, chain- +' forwarding another, and still contributing its own AoU untouched. + +skinparam linetype ortho +skinparam ArrowFontSize 10 +skinparam ArrowColor #555555 +skinparam defaultTextAlignment center + +skinparam rectangle { + BackgroundColor<> #E3F2FD + BorderColor<> #1565C0 + BackgroundColor<> #FFF8E1 + BorderColor<> #F9A825 + BackgroundColor<> #ECEFF1 + BorderColor<> #455A64 + BackgroundColor<> #E8F5E9 + BorderColor<> #2E7D32 + BackgroundColor<> #FCE4EC + BorderColor<> #AD1457 +} + +rectangle "dependency_a\ndefines AoU_1" <> as dep_a +rectangle "dependency_b\ndefines AoU_2" <> as dep_b + +rectangle "one_seooc" <> as seooc { + rectangle "received AoUs\n(each dep's own_aou_trlc +\nall_received_trlc, merged)" <> as received + rectangle "AoU_1 -- handled locally\n(CompReq.derived_from)" <> as handled + rectangle "AoU_2 -- chain-forwarded\n(aou_forwarding.yaml)" <> as forwarded + rectangle "own AoU_3\n(auto-forwarded)" <> as own + + received --> handled + received --> forwarded +} + +rectangle "dependee" <> as dependee + +dep_a --> seooc : deps (defines AoU_1) +dep_b --> seooc : deps (defines AoU_2) +seooc --> dependee : deps\n(forwards AoU_2 + AoU_3) + +@enduml diff --git a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc index a47952f4..e47927e4 100644 --- a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc +++ b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc @@ -130,7 +130,7 @@ section "Tool Requirements" { The dependable_element rule shall automatically include lobster traceability entries for all assumptions of use defined by its direct dependencies in the dependee's traceability report as - a "Forwarded AoUs" tier. + a "Received AoUs" tier. ''' mitigates = [AoU_Silently_Dropped] derived_from = [Forward_AoU_To_Dependees] @@ -162,12 +162,35 @@ section "Tool Requirements" { satisfied_by = Tools.Bazel } + ToolQualification.ToolRequirement Cover_Received_AoU_Via_Component_Requirement { + description = ''' + The CompReq requirement type's derived_from field + (CompReqSourceId) shall accept versioned, typed cross-module + TRLC references to AoU records alongside its usual FeatReq/ + AssumedSystemReq references. Resolving an AoU reference shall + require the enclosing dependable_element's auto-generated + "_received_aous" target to be listed in the CompReq + target's deps, exposing exactly the AoUs this element owns or + receives without creating a dependency cycle back through the + element's own requirements. The dependable_element rule shall + convert such references to lobster trace references resolved + against the "Received AoUs" tier, so a component requirement + can cover a received AoU it handles locally without requiring + that AoU to be chain-forwarded. + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] + satisfied_by = Tools.Lobster + } + ToolQualification.ToolRequirement Include_Forwarded_AoUs_In_Traceability { description = ''' - The lobster traceability report of a dependee shall include - forwarded AoUs as traceable items so that the existing - lobster-ci-report test fails when forwarded AoUs are not - handled (linked to a requirement, test, or justification). + The lobster traceability report of a dependable element shall + require every item in its "Received AoUs" tier to be covered + by at least one of: a component requirement (via + derived_from) or a chain-forwarding entry (via + aou_forwarding), so that the existing lobster-ci-report test + fails when a received AoU is neither handled nor forwarded. ''' mitigates = [AoU_Silently_Dropped] derived_from = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index c3d4fd44..0fe11d83 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -274,6 +274,14 @@ testable within that component. **Generated targets:** ```` (documentation), ``_test`` (TRLC validation) +A ``CompReq`` record's ``derived_from`` field (``CompReqSourceId``) may also +reference AoUs received by the enclosing ``dependable_element`` that this +requirement implements, alongside its usual ``FeatReq``/``AssumedSystemReq`` +references (e.g. ``[SampleType.SampleAoU@1]``). This requires importing the +AoU's package and listing the enclosing ``dependable_element``'s +auto-generated ``_received_aous`` target in this target's ``deps`` — +see :doc:`user_guide/assumptions_of_use`. + .. _rule-assumptions-of-use: assumptions_of_use @@ -815,3 +823,5 @@ and scope checks at build/test time. - Internal ``sphinx_module`` target; usable as ``deps`` in other Sphinx builds * - ``_index`` - Internal artefact-collection and architecture-validation target + * - ``_received_aous`` + - Exposes this element's own + received AoUs as typed TRLC references; add to a ``component_requirements`` target's ``deps`` to reference them from ``derived_from`` (see :doc:`user_guide/assumptions_of_use`) diff --git a/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst b/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst index f7da12c6..050b9da5 100644 --- a/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst +++ b/bazel/rules/rules_score/docs/user_guide/assumptions_of_use.rst @@ -21,7 +21,9 @@ or risk that is mitigated when this assumption is fulfilled. Traceability to requirements is established at the Bazel level via the ``deps`` attribute on the ``assumptions_of_use`` rule — there is no TRLC ``derived_from`` -or ``satisfies`` field on ``AoU``. +or ``satisfies`` field on ``AoU`` itself. A dependent component requirement can, +however, declare that it implements a received AoU by referencing it from its own +``derived_from`` field (see `AoU Forwarding`_ below). .. code-block:: text :caption: examples/seooc/docs/aous.trlc @@ -83,15 +85,80 @@ is forwarded rather than handled locally: calls to the library do not exceed the 10ms cycle time constraint imposed by the underlying other_seooc dependency. -**Handling forwarded AoUs in the dependee** -Forwarded AoUs appear as a "Forwarded AoUs" tier in the dependee's lobster -traceability report. The dependee must handle each forwarded AoU by one of: +**Handling AoUs received in the dependee** +Every AoU a dependable element receives appears as +an item in a "Received AoUs" tier in the dependee's lobster traceability +report. Each received AoU must be covered by exactly one of: + +- **Handling it locally**: a component requirement's ``derived_from`` field + references the AoU it implements (see below). This shows up as "Component + Requirements" coverage in the report. +- **Chain-forwarding it further** (with justification) via ``aou_forwarding``, + to be handled by this element's own dependees instead. This shows up as + "Forwarded AoUs" coverage in the report. + +If a received AoU is neither handled nor forwarded, the ``bazel test`` +traceability check fails. + +A single dependable element can do all three at once — receive AoUs from its +own dependencies, handle some of them locally, chain-forward the rest, and +still contribute its own AoUs to the mix: + +.. uml:: ../_assets/aou_forwarding_one_seooc.puml + +**Handling a received AoU with a component requirement** +Add a typed, versioned reference to the AoU (``Package.RecordName@version``, +matching the upstream ``AoU`` TRLC record) to the ``derived_from`` field of +the ``CompReq`` that implements it, alongside any ``FeatReq``/ +``AssumedSystemReq`` references — all three item kinds share the same field. +Two things are required for the reference to resolve: + +1. ``import`` the AoU's package, same as any other TRLC cross-reference. +2. List the enclosing dependable element's ``_received_aous`` target + (auto-generated by every ``dependable_element``) in the + ``component_requirements`` target's ``deps``. This target exposes exactly + the AoUs this element owns or receives -- it deliberately has no + ``requirements`` input, since a ``CompReq`` cannot depend on its own + dependable element's ``_index`` target without creating a cycle. -- Linking it to a component requirement that addresses the assumption -- Linking it to a test that verifies the assumption is met -- Chain-forwarding it further (with justification) to its own dependees +.. code-block:: text + :caption: examples/integrator/docs/requirements/component_requirements.trlc + + package IntegratorComponent + + import ScoreReq + import Integrator + import SampleType + + ScoreReq.CompReq COMP_INT_001 { + description = "The startup module shall call the SEooC initialization routine before entering the main loop" + safety = ScoreReq.Asil.B + derived_from = [Integrator.FEAT_INT_001@1, SampleType.SampleAoU@1] + version = 1 + } + +.. code-block:: starlark + :caption: examples/integrator/docs/requirements/BUILD + + component_requirements( + name = "component_requirements", + srcs = ["component_requirements.trlc"], + # A dependable_element defaults to testonly = True, so its + # auto-generated "..._received_aous" target is testonly too -- + # any component_requirements depending on it needs testonly = True + # as well, or Bazel rejects the dependency at analysis time. + testonly = True, + deps = [ + ":feature_requirements", + "@seooc//:safety_software_seooc_example_received_aous", + ], + ) -If a forwarded AoU is not handled, the ``bazel test`` traceability check will fail. +Being a real TRLC reference, an AoU entry in ``derived_from`` is resolved (and +a typo or an AoU this element does not actually receive is rejected) by the +TRLC parser itself at build time, not by a later lobster-report matching step +-- while the resulting lobster item is still tagged and traced exactly as +before, so the coverage report is unaffected. **Example: three-level forwarding chain** (the real working code for this example lives in ``examples/some_other_library``, ``examples/seooc``, and @@ -105,7 +172,8 @@ example lives in ``examples/some_other_library``, ``examples/seooc``, and → chain-forwards received TimingConstraint via aou_forwarding.yaml ↑ (deps) integrator_seooc → receives SampleType.SampleAoU (auto-forwarded) - and OtherLibrary.TimingConstraint (chain-forwarded), must handle both + and OtherLibrary.TimingConstraint (chain-forwarded) + → handles both locally via derived_from (no further dependees) .. code-block:: starlark :caption: examples/seooc/BUILD diff --git a/bazel/rules/rules_score/examples/integrator/BUILD b/bazel/rules/rules_score/examples/integrator/BUILD index c963f8ad..16e9055b 100644 --- a/bazel/rules/rules_score/examples/integrator/BUILD +++ b/bazel/rules/rules_score/examples/integrator/BUILD @@ -13,7 +13,8 @@ # # Example: System integrator SEooC that depends on safety_software_seooc_example. # -# This illustrates the full AoU forwarding chain: +# This illustrates the full AoU forwarding chain, and the two ways a +# dependable element can cover an AoU it receives: # # other_seooc defines AoU: OtherLibrary.TimingConstraint # ↑ (deps) @@ -22,9 +23,9 @@ # - chain-forwards OtherLibrary.TimingConstraint via aou_forwarding.yaml # ↑ (deps) # integrator_seooc (this target) -# - receives SampleType.SampleAoU (auto-forwarded from seooc) -# - receives OtherLibrary.TimingConstraint (chain-forwarded through seooc) -# - must handle both in its lobster traceability report +# - receives SampleType.SampleAoU (auto-forwarded from seooc) +# - receives OtherLibrary.TimingConstraint (chain-forwarded through seooc) +# - HANDLES both locally: COMP_INT_001 and COMP_INT_002 # load( diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD b/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD index 50d8c6e9..8d11e37b 100644 --- a/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD @@ -34,11 +34,13 @@ feature_requirements( component_requirements( name = "component_requirements", + testonly = True, srcs = [ "component_requirements.trlc", ], visibility = ["//visibility:public"], deps = [ ":feature_requirements", + "@seooc//:safety_software_seooc_example_received_aous", ], ) diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc b/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc index b3becb90..d9fb84bb 100644 --- a/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc @@ -14,18 +14,20 @@ package IntegratorComponent import ScoreReq import Integrator +import SampleType +import OtherLibrary ScoreReq.CompReq COMP_INT_001 { description = "The startup module shall call the SEooC initialization routine before entering the main loop" safety = ScoreReq.Asil.B - derived_from = [Integrator.FEAT_INT_001@1] + derived_from = [Integrator.FEAT_INT_001@1, SampleType.SampleAoU@1] version = 1 } ScoreReq.CompReq COMP_INT_002 { - description = "The cyclic task shall invoke the validation interface every 10ms using a hardware timer interrupt" + description = "The cyclic task shall invoke the validation interface every 10ms using a hardware timer interrupt, satisfying the underlying library's timing constraint" safety = ScoreReq.Asil.B - derived_from = [Integrator.FEAT_INT_002@1] + derived_from = [Integrator.FEAT_INT_002@1, OtherLibrary.TimingConstraint@1] version = 1 } diff --git a/bazel/rules/rules_score/examples/seooc/BUILD b/bazel/rules/rules_score/examples/seooc/BUILD index c42ad63b..de798362 100644 --- a/bazel/rules/rules_score/examples/seooc/BUILD +++ b/bazel/rules/rules_score/examples/seooc/BUILD @@ -45,6 +45,7 @@ component( "//docs/requirements:component_requirements", "//docs/requirements:component_requirements_extra", "//docs/requirements:component_requirements_sub", + "//docs/requirements:feature_requirements", ], tags = ["manual"], test_case_coverage_lock = "test_case_coverage.lock.yaml", diff --git a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl index df39df77..57e56b26 100644 --- a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl +++ b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl @@ -1,5 +1,7 @@ {FEAT_REQ_BLOCK} +{RECEIVED_AOU_BLOCK} + {FORWARDED_AOU_BLOCK} {COMP_REQ_BLOCK} diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index 650e2dd1..d9d9ff46 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -181,19 +181,31 @@ def _component_impl(ctx): # ------------------------------------------------------------------------- # Lobster Tracing: collect .lobster files from component_requirements targets - # and feature_requirements targets (needed to resolve derived_from references) + # and feature_requirements targets (needed to resolve derived_from references). # ------------------------------------------------------------------------- req_lobster_files = [] + req_lobster_transitive_files = [] feat_req_lobster_files = [] for req_target in ctx.attr.requirements: if ComponentRequirementsInfo in req_target: req_lobster_files.append(req_target[ComponentRequirementsInfo].srcs) + req_lobster_transitive_files.append(req_target[ComponentRequirementsInfo].srcs) if FeatureRequirementsInfo in req_target: feat_req_lobster_files.append(req_target[FeatureRequirementsInfo].srcs) if AssumedSystemRequirementsInfo in req_target: feat_req_lobster_files.append(req_target[AssumedSystemRequirementsInfo].srcs) + # Bubble up requirement lobster files from nested components so + # dependable_element sees the full tree, not just this component's own + # requirements (ComponentInfo.requirements itself is intentionally scoped + # to only this component's own explicitly-listed requirements). + for component in ctx.attr.components: + if ComponentInfo in component: + if component[ComponentInfo].requirements_transitive: + req_lobster_transitive_files.append(component[ComponentInfo].requirements_transitive) + req_lobster_depset = depset(transitive = req_lobster_files) + req_lobster_transitive_depset = depset(transitive = req_lobster_transitive_files) feat_req_lobster_depset = depset(transitive = feat_req_lobster_files) # Collect nested components @@ -314,6 +326,7 @@ def _component_impl(ctx): ComponentInfo( name = ctx.label.name, requirements = req_lobster_depset, + requirements_transitive = req_lobster_transitive_depset, components = components_depset, tests = depset( [gtest_lobster_file], diff --git a/bazel/rules/rules_score/private/component_requirements.bzl b/bazel/rules/rules_score/private/component_requirements.bzl index 79a86b63..e92f17af 100644 --- a/bazel/rules/rules_score/private/component_requirements.bzl +++ b/bazel/rules/rules_score/private/component_requirements.bzl @@ -49,13 +49,18 @@ def component_requirements( in the S-CORE requirements model. deps: Optional list of requirement targets (e.g. assumed_system_requirements, feature_requirements) whose TRLC records are needed for cross-reference - parsing. These targets must provide TrlcProviderInfo. + parsing. Also list the enclosing dependable_element's auto-generated + "_received_aous" target here to resolve AoU references in + `derived_from` (CompReqSourceId). These targets must provide TrlcProviderInfo. spec: TRLC specification target(s) providing RSL type definitions. Accepts a single label or a list of labels; all are merged into the spec passed to TRLC. Defaults to the S-CORE requirements model (``@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model``). lobster_config: Optional Lobster extraction config label. Defaults to the - S-CORE component requirement config. + S-CORE component requirement config. Its conversion rules always + include `derived_from` as a tracing target, including any AoU + entries within it (resolved only at the dependable_element level, + which has a "Received AoUs" level). visibility: Bazel visibility specification for the generated targets. Generated Targets: diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 35e917be..a6f2a777 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -27,6 +27,7 @@ load( "subrule_lobster_report", ) load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") +load("@trlc//:trlc.bzl", "TrlcProviderInfo") load( "//bazel/rules/rules_score:providers.bzl", "ArchitecturalDesignInfo", @@ -749,10 +750,94 @@ def _symlink_validation_log(ctx, validation_log): return output_log +# ============================================================================ +# Received-AoUs Rule +# ============================================================================ +# + +def _collect_aou_trlc_sources(assumptions_of_use, processed_deps): + """Collect this element's own + deps' forwarded AoU TRLC/spec files. + + Shared by `_dependable_element_aou_reqs_impl` and + `_dependable_element_index_impl` so the two never disagree on what an + element's own vs. received AoUs are. + + Returns: + (own_trlc_files, own_spec_files, dep_trlc_files, dep_spec_files) -- + four lists of depsets, kept separate (not pre-merged) so callers can + decide how to combine "own" vs "received from deps" (ForwardedAoUInfo + keeps them distinct; _dependable_element_aou_reqs unions them). + """ + own_trlc_files = [] + own_spec_files = [] + for aou_target in assumptions_of_use: + if TrlcProviderInfo in aou_target: + own_trlc_files.append(aou_target[TrlcProviderInfo].reqs) + own_spec_files.append(aou_target[TrlcProviderInfo].spec) + + dep_trlc_files = [] + dep_spec_files = [] + for dep in processed_deps: + if ForwardedAoUInfo in dep: + fwd_info = dep[ForwardedAoUInfo] + dep_trlc_files.append(fwd_info.own_aou_trlc) + dep_trlc_files.append(fwd_info.all_received_trlc) + dep_spec_files.append(fwd_info.spec) + + return own_trlc_files, own_spec_files, dep_trlc_files, dep_spec_files + +def _dependable_element_aou_reqs_impl(ctx): + own_trlc_files, own_spec_files, dep_trlc_files, dep_spec_files = _collect_aou_trlc_sources( + ctx.attr.assumptions_of_use, + ctx.attr.processed_deps, + ) + + all_received_trlc_depset = depset(transitive = own_trlc_files + dep_trlc_files) + + return [ + DefaultInfo(files = all_received_trlc_depset), + TrlcProviderInfo( + spec = depset(transitive = own_spec_files + dep_spec_files), + reqs = all_received_trlc_depset, + deps = depset(), + ), + ] + +_dependable_element_aou_reqs = rule( + implementation = _dependable_element_aou_reqs_impl, + doc = """Exposes this dependable element's own + received AoUs as TrlcProviderInfo. + + Intended to be added to a component_requirements target's `deps` so that + its CompReq records can type-reference a received AoU from their + `derived_from` field (CompReqSourceId's item type includes AoU) without + creating a dependency cycle through the enclosing dependable_element's + `_index` target. + """, + attrs = { + "assumptions_of_use": attr.label_list( + default = [], + doc = "This element's own Assumptions of Use targets or files.", + ), + "processed_deps": attr.label_list( + default = [], + doc = "This element's deps, mapped to their '_index' targets.", + ), + }, +) + # ============================================================================ # Index Generation Rule Implementation # ============================================================================ +def _cond_names(*conditioned_names): + """Flatten (condition, level_name) pairs into a name list, dropping false conditions. + + Reduces the `(["X"] if cond else []) + (["Y"] if cond2 else [])` + repetition used to build `trace_to`/`requires` level-name lists below to + a flat, readable argument list. + """ + return [name for condition, name in conditioned_names if condition] + def _dependable_element_index_impl(ctx): """Generate index.rst file with references to all dependable element artifacts. @@ -1059,15 +1144,14 @@ def _dependable_element_index_impl(ctx): feat_req_lobster_depset = depset(transitive = feat_req_lobster_files) - # Collect component requirement and test .lobster files from ComponentInfo comp_req_lobster_files = [] comp_test_lobster_files = [] comp_arch_lobster_files = [] for comp_target in ctx.attr.components: if ComponentInfo in comp_target: comp_info = comp_target[ComponentInfo] - if comp_info.requirements: - comp_req_lobster_files.append(comp_info.requirements) + if comp_info.requirements_transitive: + comp_req_lobster_files.append(comp_info.requirements_transitive) if comp_info.tests: comp_test_lobster_files.append(comp_info.tests) if comp_info.architecture: @@ -1160,6 +1244,7 @@ def _dependable_element_index_impl(ctx): # Build the DE-level lobster report if feature and component traces exist feat_req_list = feat_req_lobster_depset.to_list() + comp_req_list = comp_req_lobster_depset.to_list() comp_test_list = comp_test_lobster_depset.to_list() comp_arch_list = comp_arch_lobster_depset.to_list() @@ -1172,19 +1257,20 @@ def _dependable_element_index_impl(ctx): # AoU Forwarding: collect own AoUs and received AoUs from deps # ========================================================================= - # Collect own AoU lobster files from assumptions_of_use targets + # Collect own AoU lobster + trlc files from assumptions_of_use targets. own_aou_lobster_files = [] for aou_target in ctx.attr.assumptions_of_use: if AssumptionsOfUseInfo in aou_target: own_aou_lobster_files.append(aou_target[AssumptionsOfUseInfo].aou_lobster) - own_aou_lobster_depset = depset(transitive = own_aou_lobster_files) - - own_assumptions_of_use_info = AssumptionsOfUseInfo( - aou_lobster = own_aou_lobster_depset, - name = ctx.label.name, + own_trlc_files, own_spec_files, dep_trlc_files, dep_spec_files = _collect_aou_trlc_sources( + ctx.attr.assumptions_of_use, + ctx.attr.processed_deps, ) + own_aou_lobster_depset = depset(transitive = own_aou_lobster_files) + own_aou_trlc_depset = depset(transitive = own_trlc_files) + # Collect forwarded AoU lobster files from deps (received AoUs) received_aou_lobster_files = [] for dep in ctx.attr.processed_deps: @@ -1196,27 +1282,36 @@ def _dependable_element_index_impl(ctx): received_aou_lobster_depset = depset(transitive = received_aou_lobster_files) received_aou_list = received_aou_lobster_depset.to_list() - # Chain-forwarding: if aou_forwarding YAML is provided, filter received AoUs + # Chain-forwarding: if aou_forwarding YAML is provided, filter received AoUs. chain_forwarded_lobster_depset = depset() + forwarded_aou_markers_list = [] if ctx.file.aou_forwarding and received_aou_list: chain_forwarded_lobster_file = ctx.actions.declare_file( ctx.label.name + "/chain_forwarded_aous.lobster", ) + forwarded_aou_markers_file = ctx.actions.declare_file( + ctx.label.name + "/forwarded_aou_markers.lobster", + ) fwd_args = ctx.actions.args() fwd_args.add("--yaml", ctx.file.aou_forwarding) fwd_args.add("--output", chain_forwarded_lobster_file) + fwd_args.add("--markers-output", forwarded_aou_markers_file) fwd_args.add("--input-lobster") fwd_args.add_all(received_aou_list) ctx.actions.run( inputs = [ctx.file.aou_forwarding] + received_aou_list, - outputs = [chain_forwarded_lobster_file], + outputs = [chain_forwarded_lobster_file, forwarded_aou_markers_file], executable = ctx.executable._aou_forwarding_tool, arguments = [fwd_args], progress_message = "Filtering chain-forwarded AoUs for %s" % ctx.label.name, mnemonic = "AoUForwarding", ) chain_forwarded_lobster_depset = depset([chain_forwarded_lobster_file]) + forwarded_aou_markers_list = [forwarded_aou_markers_file] output_files.append(chain_forwarded_lobster_file) + output_files.append(forwarded_aou_markers_file) + + all_received_trlc_depset = depset(transitive = dep_trlc_files) lobster_report_file = None lobster_html_report = None @@ -1272,6 +1367,7 @@ def _dependable_element_index_impl(ctx): has_feat_req = bool(feat_req_list) has_comp_req = bool(comp_req_list) + has_received_aou = bool(received_aou_list) has_public_api = bool(interface_req_list) or strict has_fm = bool(fm_list) or strict has_cm = bool(cm_list) or strict @@ -1283,32 +1379,65 @@ def _dependable_element_index_impl(ctx): output = lobster_config, substitutions = { "{FEAT_REQ_BLOCK}": format_lobster_block("requirements", "Feature Requirements", feat_req_list), - "{FORWARDED_AOU_BLOCK}": format_lobster_block("requirements", "Forwarded AoUs", received_aou_list), + # Target level: every item here must be covered, either by a + # Component Requirement (`derived_from`) or by being + # further chain-forwarded (Forwarded AoUs, below). Without the + # `requires` override, LOBSTER would instead require BOTH + # sources to independently cover every item (its default + # AND-across-sources behaviour for multiple `trace to:` + # declarations), which no single AoU could ever satisfy. + "{RECEIVED_AOU_BLOCK}": format_lobster_block( + "requirements", + "Received AoUs", + received_aou_list, + requires = [_cond_names( + (has_comp_req, "Component Requirements"), + (forwarded_aou_markers_list or strict, "Forwarded AoUs"), + )], + ), + # Checking level: markers for received AoUs this element chain- + # forwards onward instead of handling locally. Force-emitted + # empty in release mode (when there are received AoUs) so the + # "trace to: Received AoUs" edge stays active even without an + # aou_forwarding.yaml -- otherwise an element that receives + # AoUs but neither forwards nor handles any of them would have + # no checking level at all, and the missing coverage would + # silently pass instead of failing the build. + "{FORWARDED_AOU_BLOCK}": format_lobster_block( + "requirements", + "Forwarded AoUs", + forwarded_aou_markers_list, + trace_to = _cond_names((has_received_aou, "Received AoUs")), + emit_empty = strict and has_received_aou, + ), "{COMP_REQ_BLOCK}": format_lobster_block( "requirements", "Component Requirements", comp_req_list, - trace_to = ["Feature Requirements"] if has_feat_req else [], + trace_to = _cond_names( + (has_feat_req, "Feature Requirements"), + (has_received_aou, "Received AoUs"), + ), ), "{UNIT_TEST_BLOCK}": format_lobster_block( "activity", "Unit Test", comp_test_list, - trace_to = ["Component Requirements"] if has_comp_req else [], + trace_to = _cond_names((has_comp_req, "Component Requirements")), emit_empty = strict, ), "{COVERAGE_BLOCK}": format_lobster_block( "activity", "Test Case Coverage", coverage_lobster_files, - trace_to = ["Component Requirements"] if has_comp_req else [], + trace_to = _cond_names((has_comp_req, "Component Requirements")), emit_empty = strict, ), "{ARCH_BLOCK}": format_lobster_block( "implementation", "Architecture", comp_arch_list, - trace_to = ["Component Requirements"] if has_comp_req else [], + trace_to = _cond_names((has_comp_req, "Component Requirements")), emit_empty = strict, ), "{PUBLIC_API_BLOCK}": format_lobster_block( @@ -1321,7 +1450,7 @@ def _dependable_element_index_impl(ctx): "requirements", "Failure Modes", fm_list, - trace_to = ["Public API"] if has_public_api else [], + trace_to = _cond_names((has_public_api, "Public API")), emit_empty = strict, ), "{CM_BLOCK}": format_lobster_block( @@ -1334,13 +1463,13 @@ def _dependable_element_index_impl(ctx): "activity", "Root Causes", rc_list, - trace_to = (["Failure Modes"] if has_fm else []) + (["Control Measures"] if has_cm else []), + trace_to = _cond_names((has_fm, "Failure Modes"), (has_cm, "Control Measures")), emit_empty = strict, ), }, ) - all_lobster_inputs = feat_req_list + comp_req_list + comp_arch_list + comp_test_list + interface_req_list + fm_list + cm_list + rc_list + received_aou_list + coverage_lobster_files + all_lobster_inputs = feat_req_list + comp_req_list + comp_arch_list + comp_test_list + interface_req_list + fm_list + cm_list + rc_list + received_aou_list + forwarded_aou_markers_list + coverage_lobster_files lobster_report_file = subrule_lobster_report(all_lobster_inputs, lobster_config) lobster_files = [lobster_config, lobster_report_file] @@ -1435,8 +1564,10 @@ def _dependable_element_index_impl(ctx): ForwardedAoUInfo( own_aou_lobster = own_aou_lobster_depset, chain_forwarded_lobster = chain_forwarded_lobster_depset, + own_aou_trlc = own_aou_trlc_depset, + all_received_trlc = all_received_trlc_depset, + spec = depset(transitive = own_spec_files + dep_spec_files), ), - own_assumptions_of_use_info, OutputGroupInfo(debug = depset(validation_output_files + unit_validation_output_files)), ] @@ -1696,7 +1827,7 @@ def dependable_element( aou_forwarding = None, maturity = "release", generate_html_report = False, - testonly = True, + testonly = False, docs_prefix = "docs/sphinx/", **kwargs): """Define a dependable element (Safety Element out of Context - SEooC) following S-CORE process guidelines. @@ -1764,6 +1895,9 @@ def dependable_element( (see `processed_deps`), not against `` itself. : Main dependable element target (sphinx_module) with HTML documentation _needs: Sphinx-needs JSON target (created by sphinx_module for cross-referencing) + _received_aous: TrlcProviderInfo target exposing this element's own + + received AoUs; add to a component_requirements target's `deps` to + type-reference a received AoU from `derived_from`. """ @@ -1771,6 +1905,18 @@ def dependable_element( for dep in deps: processed_deps.append("{}_index".format(dep)) + # Step 0: Expose this element's own + received AoUs as TrlcProviderInfo, + # for component_requirements targets to reference via `derived_from`. Kept + # deliberately separate from _dependable_element_index (which depends on + # `requirements`): a CompReq target listing this in its own `deps` must + # never create a cycle back through `requirements` -> `_index`. + _dependable_element_aou_reqs( + name = name + "_received_aous", + assumptions_of_use = assumptions_of_use, + processed_deps = processed_deps, + **kwargs + ) + # Step 1: Generate index.rst and collect all artifacts # Note: validation runs as a subrule within the index generation _dependable_element_index( diff --git a/bazel/rules/rules_score/private/lobster_config.bzl b/bazel/rules/rules_score/private/lobster_config.bzl index d201e084..7e8188fb 100644 --- a/bazel/rules/rules_score/private/lobster_config.bzl +++ b/bazel/rules/rules_score/private/lobster_config.bzl @@ -32,7 +32,7 @@ def format_lobster_sources(files): """ return "\n".join([' source: "{}";'.format(f.path) for f in files]) -def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): +def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False, requires = []): """Build a complete, optional LOBSTER config block. By default returns "" (omitting the level entirely) when `files` is empty, @@ -48,6 +48,17 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): a missing verification level (e.g. no unit tests, no root causes) fail the traceability check instead of silently disappearing from the policy. + By default, when *multiple* levels each declare `trace to: `, + LOBSTER requires ALL of them to independently cover every item here (an + AND across sources) -- see the `requires` config directive in LOBSTER's + documentation. Pass `requires` to relax this to an OR: each element is a + list of level names, rendered as a single `requires: "A" or "B";` line + (multiple elements become separate, independently-AND'd `requires:` + lines). Only include level names that are themselves present (non-empty + or emit_empty) in the same config, and that already declare a `trace to:` + line pointing at this level (`requires` narrows an existing mandatory + `trace to:` edge into an alternative, it does not create one). + Args: kind: LOBSTER block kind, e.g. "requirements", "activity", "implementation". name: Level name, used as the quoted block header. @@ -60,6 +71,10 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): emit_empty: When True, emit the block header and `trace to:` lines even if `files` is empty (no `source:` lines are produced). Defaults to False. + requires: List of OR-groups (each a list of level names) that jointly + replace the default AND-of-sources coverage check for this level + with "any one of these is sufficient". Defaults to [] (no + override; the default AND-of-sources behaviour applies). Returns: The full block text (kind, header, sources, trace-to lines), or "" @@ -68,9 +83,15 @@ def format_lobster_block(kind, name, files, trace_to = [], emit_empty = False): if not files and not emit_empty: return "" trace_lines = "".join([' trace to: "{}";\n'.format(t) for t in trace_to]) - return '{kind} "{name}" {{\n{sources}\n{trace}}}'.format( + requires_lines = "".join([ + " requires: {};\n".format(" or ".join(['"{}"'.format(n) for n in group])) + for group in requires + if group + ]) + return '{kind} "{name}" {{\n{sources}\n{trace}{requires}}}'.format( kind = kind, name = name, sources = format_lobster_sources(files), trace = trace_lines, + requires = requires_lines, ) diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index b94d1692..f7bcecab 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -99,7 +99,7 @@ FeatureRequirementsInfo = provider( ComponentRequirementsInfo = provider( doc = "Provider for component requirements artifacts.", fields = { - "srcs": "Depset of .lobster traceability files generated from TRLC requirement sources.", + "srcs": "Depset of .lobster traceability files generated from TRLC requirement sources. Includes `derived_from` as a tracing target, including any AoU references within it.", "name": "Name of the requirements target.", }, ) @@ -130,7 +130,7 @@ AssumptionsOfUseInfo = provider( ) ForwardedAoUInfo = provider( - doc = """Carries AoU lobster files that dependees must satisfy. + doc = """Carries AoU lobster/trlc files that dependees must satisfy. When a dependable element is listed in another element's `deps`, the dependee receives this element's AoUs and must either link them in its @@ -139,6 +139,9 @@ ForwardedAoUInfo = provider( fields = { "own_aou_lobster": "Depset of .lobster files from this element's own assumptions_of_use (always forwarded to dependees).", "chain_forwarded_lobster": "Depset of .lobster files for received AoUs being further-forwarded (selected via aou_forwarding YAML).", + "own_aou_trlc": "Depset of .trlc files from this element's own assumptions_of_use (always forwarded to dependees; mirrors own_aou_lobster). Consumed only by the sibling '_received_aous' target, not lobster tooling.", + "all_received_trlc": "Depset of .trlc files for every AoU this element has received from its deps, forwarded unfiltered -- deliberately NOT filtered by aou_forwarding YAML like chain_forwarded_lobster is, so any received AoU stays type-referenceable downstream regardless of whether it is chain-forwarded at the lobster level. Consumed only by the sibling '_received_aous' target.", + "spec": "Depset of RSL spec files needed to parse own_aou_trlc and all_received_trlc (union of this element's own assumptions_of_use spec and any spec received from deps). Lets downstream elements parse forwarded AoU records without assuming a fixed RSL model.", }, ) @@ -146,7 +149,8 @@ ComponentInfo = provider( doc = "Provider for component artifacts.", fields = { "name": "Name of the component target.", - "requirements": "Depset of component requirement traceability files (.lobster) collected from ComponentRequirementsInfo targets only (CompReq kind). Does not include feature or assumed-system requirement files.", + "requirements": "Depset of component requirement traceability files (.lobster) collected from this component's own ComponentRequirementsInfo targets only (CompReq kind); not rolled up from nested components. Does not include feature or assumed-system requirement files. Consumed only by dependable_element's test-case-coverage-lock check. Includes `derived_from` tracing targets, including any AoU references within it (resolved only at the dependable_element level).", + "requirements_transitive": "Depset of component requirement traceability files (.lobster), rolled up transitively from this component and all nested components. Consumed by dependable_element to build the aggregated 'Component Requirements' traceability report tier.", "components": "Depset of nested component and/or unit Targets that comprise this component.", "tests": "Depset of test traceability files (.lobster) generated from unit test results, collected transitively from all nested components and units.", "architecture": "Depset of architecture traceability files (.lobster) generated from unit architectural designs, collected transitively from all nested components and units.", diff --git a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py index 8c9e50c9..042da0d6 100644 --- a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py +++ b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py @@ -16,15 +16,42 @@ files, then outputs a new .lobster file containing only the entries listed in the YAML. This enables dependable elements to further-forward AoUs they cannot handle to their own dependees. + +Optionally also emits a second "markers" .lobster file: one synthetic item +per forwarded entry, distinct from (but referencing) the original received +AoU item. This is what lets the dependable_element's own traceability report +show a "Forwarded AoUs" level with a `trace to: "Received AoUs"` edge — +using the original (identity-preserved) items directly would create a tag +collision with the "Received AoUs" level in the same report. + +Reuses ``Requirement``, ``Tracing_Tag``, ``File_Reference``, ``lobster_read``, +and ``lobster_write`` from the lobster library (no manual JSON construction +or envelope/schema handling) — only the YAML parsing and the AoU-ID-to-tag +matching (which lobster itself has no concept of) are specific to this tool. """ from __future__ import annotations import argparse -import json +import logging from pathlib import Path import yaml +from lobster.common.errors import LOBSTER_Error, Message_Handler +from lobster.common.io import lobster_read, lobster_write +from lobster.common.items import Requirement, Tracing_Tag +from lobster.common.location import File_Reference + +GENERATOR = "aou_forwarding_to_lobster" + +logger = logging.getLogger(__name__) + +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: @@ -66,34 +93,40 @@ def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: ) result.append({"aou_id": aou_id, "justification": justification}) + logger.info("Parsed %d forwarding entr%s from %s", len(result), "y" if len(result) == 1 else "ies", yaml_path) return result -def load_lobster_items(lobster_paths: list[str]) -> list[dict]: - """Load all items from one or more .lobster JSON files. +def load_lobster_items(lobster_paths: list[str]) -> list[Requirement]: + """Load all Requirement items from one or more .lobster JSON files. Args: lobster_paths: Paths to .lobster files. Returns: - List of all lobster item dicts from all files. + List of all Requirement items from all files. + + Raises: + SystemExit: If a file cannot be read, or is not valid lobster-req-trace JSON. """ - all_items = [] + mh = Message_Handler() + all_items: list[Requirement] = [] for path in lobster_paths: + items: dict = {} try: - with open(path, encoding="utf-8") as f: - data = json.load(f) - except (json.JSONDecodeError, OSError) as e: + lobster_read(mh, path, "aou", items) + except (OSError, LOBSTER_Error) as e: raise SystemExit(f"Failed to parse lobster file {path}: {e}") from e - all_items.extend(data.get("data", [])) + all_items.extend(items.values()) + logger.info("Loaded %d lobster item(s) from %d file(s)", len(all_items), len(lobster_paths)) return all_items -def filter_forwarded_aous( +def _match_forwarded_entries( forwarding_entries: list[dict[str, str]], - lobster_items: list[dict], -) -> list[dict]: - """Filter lobster items to only those listed in the forwarding YAML. + lobster_items: list[Requirement], +) -> list[tuple[dict[str, str], Requirement]]: + """Match each forwarding YAML entry to its received AoU lobster item. Matches by checking if the AoU ID appears in the lobster item's tag. Lobster-trlc generates tags like "req PackageName.RecordName@version". @@ -105,48 +138,113 @@ def filter_forwarded_aous( lobster_items: All lobster items from received AoU files. Returns: - Filtered list of lobster items matching the forwarding entries. + List of (entry, matched item) pairs, in forwarding YAML order. Raises: SystemExit: If any aou_id from YAML doesn't match a received item. """ # Build lookup: tag suffix -> item # Lobster-trlc may generate versioned tags like "req Pkg.Name@1". - # We index by both the full ID and the base ID (without @version). - item_by_id: dict[str, dict] = {} + # We index by both the full ID and the base ID (without @version), but + # only register the bare base ID when it is unambiguous: if two received + # items share the same base ID at different versions (e.g. the same AoU + # forwarded at different versions via different deps), a bare YAML + # reference must not silently resolve to whichever one loaded last. + item_by_id: dict[str, Requirement] = {} + base_id_versions: dict[str, dict[object, Requirement]] = {} for item in lobster_items: - tag = item.get("tag", "") - # Tags are formatted as "req PackageName.RecordName[@version]" - parts = tag.split(" ", 1) - if len(parts) == 2: - full_id = parts[1] - item_by_id[full_id] = item - # Also index by base name (strip @version suffix) - base_id = full_id.split("@")[0] - if base_id != full_id: - item_by_id[base_id] = item - - filtered = [] + full_id = item.tag.tag + if item.tag.version: + full_id += f"@{item.tag.version}" + item_by_id[full_id] = item + base_id_versions.setdefault(item.tag.tag, {})[item.tag.version] = item + + for base_id, by_version in base_id_versions.items(): + if len(by_version) == 1: + item_by_id[base_id] = next(iter(by_version.values())) + + matched = [] for entry in forwarding_entries: aou_id = entry["aou_id"] if aou_id not in item_by_id: available = ", ".join(sorted(item_by_id.keys())) if item_by_id else "(none)" + hint = "" + if aou_id in base_id_versions and len(base_id_versions[aou_id]) > 1: + hint = " (ambiguous: multiple versions received -- specify '@version' explicitly)" raise SystemExit( - f"AoU ID '{aou_id}' listed in forwarding YAML not found in received AoUs. Available IDs: {available}" + f"AoU ID '{aou_id}' listed in forwarding YAML not found in received AoUs.{hint} Available IDs: {available}" ) - filtered.append(item_by_id[aou_id]) + matched.append((entry, item_by_id[aou_id])) - return filtered + logger.info("Matched %d/%d forwarding entries to received AoU items", len(matched), len(forwarding_entries)) + return matched -def create_lobster_output(items: list[dict]) -> dict: - """Wrap items in the standard lobster JSON envelope.""" - return { - "schema": "lobster-req-trace", - "version": 3, - "generator": "aou_forwarding_to_lobster", - "data": items, - } +def filter_forwarded_aous( + forwarding_entries: list[dict[str, str]], + lobster_items: list[Requirement], +) -> list[Requirement]: + """Filter lobster items to only those listed in the forwarding YAML. + + The returned items are identical (same tag) to the originals: this + output is handed on, unmodified, to this element's own dependees so + that further chain-forwarding and eventual handling still resolves + against the AoU's original tag. + + Args: + forwarding_entries: Parsed YAML entries with 'aou_id' fields. + lobster_items: All lobster items from received AoU files. + + Returns: + Filtered list of lobster items matching the forwarding entries. + + Raises: + SystemExit: If any aou_id from YAML doesn't match a received item. + """ + return [item for _, item in _match_forwarded_entries(forwarding_entries, lobster_items)] + + +def build_forwarded_markers( + forwarding_entries: list[dict[str, str]], + lobster_items: list[Requirement], + yaml_path: str, +) -> list[Requirement]: + """Build synthetic "Forwarded AoUs" marker items for the DE's own report. + + Each marker is a distinct lobster item (its own tag, so it does not + collide with the "Received AoUs" level in the same report) carrying a + `refs` entry pointing at the original received AoU tag. This gives + LOBSTER a `trace to: "Received AoUs"` edge for AoUs that are being + chain-forwarded rather than handled locally. The forwarding + justification becomes the marker's descriptive text. + + Args: + forwarding_entries: Parsed YAML entries with 'aou_id' and + 'justification' fields. + lobster_items: All lobster items from received AoU files. + yaml_path: Path to the aou_forwarding.yaml file (used as the + marker's source location). + + Returns: + List of marker Requirement items, one per forwarding entry. + + Raises: + SystemExit: If any aou_id from YAML doesn't match a received item. + """ + markers = [] + for entry, item in _match_forwarded_entries(forwarding_entries, lobster_items): + aou_id = entry["aou_id"] + marker = Requirement( + tag=Tracing_Tag("req", f"{aou_id}__forwarded"), + location=File_Reference(yaml_path, line=1), + framework="AoUForwarding", + kind="ForwardedAoU", + name=aou_id, + text=entry["justification"], + ) + marker.add_tracing_target(item.tag) + markers.append(marker) + return markers def main() -> None: @@ -168,8 +266,23 @@ def main() -> None: required=True, help="Output .lobster file path for the filtered entries.", ) + parser.add_argument( + "--markers-output", + required=False, + help="Optional output .lobster file path for synthetic 'Forwarded AoUs' " + "marker items (distinct tags, refs pointing at the original received " + "AoU items). Used by the dependable_element's own traceability report.", + ) + parser.add_argument( + "--log-level", + choices=["error", "warn", "info", "debug"], + default="warn", + dest="log_level", + help="Log level for tool output (default: warn).", + ) args = parser.parse_args() + logging.basicConfig(level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s") # Parse YAML forwarding_entries = parse_forwarding_yaml(args.yaml) @@ -177,16 +290,23 @@ def main() -> None: # Load received lobster items lobster_items = load_lobster_items(args.input_lobster) - # Filter + # Filter (identity-preserved copies, forwarded on to this element's own dependees) filtered_items = filter_forwarded_aous(forwarding_entries, lobster_items) # Write output - output = create_lobster_output(filtered_items) output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: - json.dump(output, f, indent=2) - f.write("\n") + lobster_write(f, Requirement, GENERATOR, filtered_items) + logger.info("Wrote %d item(s) to %s", len(filtered_items), output_path) + + if args.markers_output: + markers = build_forwarded_markers(forwarding_entries, lobster_items, args.yaml) + markers_output_path = Path(args.markers_output) + markers_output_path.parent.mkdir(parents=True, exist_ok=True) + with open(markers_output_path, "w", encoding="utf-8") as f: + lobster_write(f, Requirement, GENERATOR, markers) + logger.info("Wrote %d marker item(s) to %s", len(markers), markers_output_path) if __name__ == "__main__": diff --git a/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml b/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml index 0bb54fbe..ee2dc8fa 100644 --- a/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml +++ b/bazel/rules/rules_score/src/test_case_coverage/assets/component_diagram.puml @@ -24,7 +24,7 @@ package "Inputs" { package "component rule\n(component.bzl)" { [subrule_lobster_gtest] as subrule - [_component_test\n(test_case_coverage_lock attr)] as rule_test + [_component_rule\n(test_case_coverage_lock attr)] as rule_test [_component_update\n(.update target)] as rule_update } diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 8f70606e..48536343 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -102,8 +102,10 @@ load( ":unit_component_test.bzl", "component_excludes_feature_req_docs_test", "component_provider_test", + "component_requirements_transitive_test", "component_sphinx_sources_test", "component_test_case_coverage_lock_test", + "dependable_element_received_aous_union_test", "dependable_element_test_case_coverage_lock_check_action_test", "unit_component_test_suite", "unit_provider_test", @@ -695,6 +697,26 @@ dependable_element( deps = [":dep_seooc_lib"], # dependency to other seoocs/score_components ) +# Regression fixture for the `_received_aous` sibling target: its own +# AoUs (`:aous_rst`) are deliberately a DIFFERENT assumptions_of_use target +# than its dep's (`dep_seooc_lib` uses `:aous`), so the two are genuinely +# distinguishable content-wise. Used by +# dependable_element_received_aous_union_test to verify the sibling target's +# TrlcProviderInfo unions BOTH this element's own AoU .trlc files AND the +# ones received (forwarded) from `dep_seooc_lib`, rather than only one side. +dependable_element( + name = "test_dependable_element_received_aous_union", + architectural_design = [":arch_design_elem"], + assumptions_of_use = [":aous_rst"], + components = [], + dependability_analysis = [":dependability_analysis_target"], + integrity_level = "B", + maturity = "development", + requirements = [":feat_req"], + tests = [], + deps = [":dep_seooc_lib"], +) + # ============================================================================ # Test Fixtures - Unit, Component, and Dependable Element # ============================================================================ @@ -870,13 +892,22 @@ unit( ) # Nested component containing unit2 +# Deliberately uses a DIFFERENT component_requirements target (:comp_req_rst) +# than the parent's own (:comp_req) below, so the two are genuinely +# distinguishable content-wise -- this makes ComponentInfo.requirements_transitive +# rollup observable: if the nested component's own requirements failed to +# propagate up, only :comp_req's .lobster file would show up at the parent, +# not :comp_req_rst's. See component_requirements_transitive_test. +# component( name = "test_nested_component", testonly = True, components = [":test_unit2"], requirements = [ ":comp_req", + ":comp_req_rst", ":feat_req", + ":feat_req_rst", ], tests = [], ) @@ -1316,6 +1347,15 @@ component_test_case_coverage_lock_test( target_under_test = ":test_component_with_test_case_coverage_lock", ) +# Verifies ComponentInfo.requirements_transitive rolls up both the parent's +# own component_requirements AND the nested component's (which use two +# distinct fixtures, :comp_req and :comp_req_rst -- see test/BUILD comments +# on test_nested_component/test_parent_component above). +component_requirements_transitive_test( + name = "component_requirements_transitive_test", + target_under_test = ":test_parent_component", +) + # Verifies the ComponentTestCaseCoverageLockCheck action is wired up end-to-end through # dependable_element (not just component() in isolation), and that # --allow-check-failures is only passed for maturity == "development". @@ -1333,6 +1373,13 @@ dependable_element_test_case_coverage_lock_check_action_test( target_under_test = ":test_dependable_element_coverage_development_index", ) +# Verifies the `_received_aous` sibling target unions this element's own +# AoU trlc with the AoU trlc it received (forwarded) from its deps. +dependable_element_received_aous_union_test( + name = "dependable_element_received_aous_union_test", + target_under_test = ":test_dependable_element_received_aous_union_received_aous", +) + # Unit, Component, and Dependable Element test suite unit_component_test_suite(name = "unit_component_tests") @@ -1521,7 +1568,10 @@ py_test( name = "test_aou_forwarding_to_lobster", size = "small", srcs = ["test_aou_forwarding_to_lobster.py"], - deps = ["@score_tooling//bazel/rules/rules_score:aou_forwarding_to_lobster"], + deps = [ + "@lobster//lobster/common", + "@score_tooling//bazel/rules/rules_score:aou_forwarding_to_lobster", + ], ) py_test( diff --git a/bazel/rules/rules_score/test/lobster_config_test.bzl b/bazel/rules/rules_score/test/lobster_config_test.bzl index c9cae8be..8a2ae1d3 100644 --- a/bazel/rules/rules_score/test/lobster_config_test.bzl +++ b/bazel/rules/rules_score/test/lobster_config_test.bzl @@ -150,3 +150,60 @@ def lobster_config_test_suite(name): trace_to = ["Failure Modes", "Control Measures"], ), ) + + # --- format_lobster_block: requires (OR-of-sources override) -------------- + # By default, when multiple different levels each `trace to:` the same + # target, LOBSTER requires ALL of them to independently cover every item + # (AND across sources). `requires` overrides this to "any one of these is + # sufficient" for a given target level -- see e.g. Received AoUs, which + # must be covered by EITHER Component Requirements OR Forwarded AoUs, not + # both at once. + + loadingtest.equals( + env, + "block_with_requires_or_group", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n requires: "Component Requirements" or "Forwarded AoUs";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [["Component Requirements", "Forwarded AoUs"]], + ), + ) + + loadingtest.equals( + env, + "block_with_requires_single_name", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n requires: "Component Requirements";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [["Component Requirements"]], + ), + ) + + loadingtest.equals( + env, + "block_with_requires_empty_group_omitted", + 'requirements "Received AoUs" {\n source: "aou.lobster";\n}', + format_lobster_block( + "requirements", + "Received AoUs", + [_fake_file("aou.lobster")], + requires = [[]], + ), + ) + + loadingtest.equals( + env, + "block_with_trace_to_and_requires_combined", + 'requirements "Component Requirements" {\n source: "comp.lobster";\n trace to: "Feature Requirements";\n requires: "Feature Requirements" or "Received AoUs";\n}', + format_lobster_block( + "requirements", + "Component Requirements", + [_fake_file("comp.lobster")], + trace_to = ["Feature Requirements"], + requires = [["Feature Requirements", "Received AoUs"]], + ), + ) diff --git a/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py b/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py index 49daead2..324ad7d2 100644 --- a/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py +++ b/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py @@ -12,20 +12,33 @@ # ******************************************************************************* """Tests for aou_forwarding_to_lobster.""" -import json import tempfile import unittest import yaml +from lobster.common.items import Requirement, Tracing_Tag +from lobster.common.location import Void_Reference from aou_forwarding_to_lobster import ( - create_lobster_output, + build_forwarded_markers, filter_forwarded_aous, load_lobster_items, parse_forwarding_yaml, ) +def _req(tag: str, name: str) -> Requirement: + """Build a minimal Requirement item with the given 'req Pkg.Name[@ver]' tag.""" + namespace, rest = tag.split(" ", 1) + return Requirement( + tag=Tracing_Tag.from_text(namespace, rest), + location=Void_Reference(), + framework="TRLC", + kind="AoU", + name=name, + ) + + class TestParseForwardingYaml(unittest.TestCase): """Tests for parse_forwarding_yaml.""" @@ -79,31 +92,24 @@ def test_multiple_entries(self) -> None: class TestLoadLobsterItems(unittest.TestCase): """Tests for load_lobster_items.""" - def _write_lobster(self, items: list) -> str: - data = { - "schema": "lobster-req-trace", - "version": 3, - "generator": "test", - "data": items, - } + def _write_lobster(self, tags: list[str]) -> str: + from lobster.common.io import lobster_write + + items = [_req(tag, tag.split(" ", 1)[1].split("@")[0]) for tag in tags] f = tempfile.NamedTemporaryFile(mode="w", suffix=".lobster", delete=False) - json.dump(data, f) + lobster_write(f, Requirement, "test", items) f.close() return f.name def test_loads_items(self) -> None: - items = [ - {"tag": "req Pkg.AoU1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2", "name": "AoU2"}, - ] - path = self._write_lobster(items) + path = self._write_lobster(["req Pkg.AoU1", "req Pkg.AoU2"]) loaded = load_lobster_items([path]) self.assertEqual(len(loaded), 2) - self.assertEqual(loaded[0]["tag"], "req Pkg.AoU1") + self.assertEqual(str(loaded[0].tag), "req Pkg.AoU1") def test_multiple_files(self) -> None: - path1 = self._write_lobster([{"tag": "req A.B", "name": "B"}]) - path2 = self._write_lobster([{"tag": "req C.D", "name": "D"}]) + path1 = self._write_lobster(["req A.B"]) + path2 = self._write_lobster(["req C.D"]) loaded = load_lobster_items([path1, path2]) self.assertEqual(len(loaded), 2) @@ -117,20 +123,17 @@ class TestFilterForwardedAous(unittest.TestCase): """Tests for filter_forwarded_aous.""" def test_filters_correctly(self) -> None: - items = [ - {"tag": "req Pkg.AoU1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2", "name": "AoU2"}, - ] + items = [_req("req Pkg.AoU1", "AoU1"), _req("req Pkg.AoU2", "AoU2")] entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) - self.assertEqual(filtered[0]["tag"], "req Pkg.AoU1") + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1") def test_multiple_filters(self) -> None: items = [ - {"tag": "req A.B", "name": "B"}, - {"tag": "req C.D", "name": "D"}, - {"tag": "req E.F", "name": "F"}, + _req("req A.B", "B"), + _req("req C.D", "D"), + _req("req E.F", "F"), ] entries = [ {"aou_id": "A.B", "justification": "r1"}, @@ -140,7 +143,7 @@ def test_multiple_filters(self) -> None: self.assertEqual(len(filtered), 2) def test_nonexistent_aou_id_raises(self) -> None: - items = [{"tag": "req Pkg.AoU1", "name": "AoU1"}] + items = [_req("req Pkg.AoU1", "AoU1")] entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] with self.assertRaises(SystemExit): filter_forwarded_aous(entries, items) @@ -148,36 +151,108 @@ def test_nonexistent_aou_id_raises(self) -> None: def test_versioned_tag_matches_base_id(self) -> None: """lobster-trlc generates versioned tags like 'req Pkg.Name@1'.""" items = [ - {"tag": "req Pkg.AoU1@1", "name": "AoU1"}, - {"tag": "req Pkg.AoU2@3", "name": "AoU2"}, + _req("req Pkg.AoU1@1", "AoU1"), + _req("req Pkg.AoU2@3", "AoU2"), ] entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) - self.assertEqual(filtered[0]["tag"], "req Pkg.AoU1@1") + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1@1") def test_versioned_tag_matches_full_id(self) -> None: """Full versioned ID should also work.""" - items = [{"tag": "req Pkg.AoU1@2", "name": "AoU1"}] + items = [_req("req Pkg.AoU1@2", "AoU1")] entries = [{"aou_id": "Pkg.AoU1@2", "justification": "reason"}] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) + def test_ambiguous_base_id_requires_version(self) -> None: + """Two different versions sharing a base ID must not silently pick one.""" + items = [ + _req("req Pkg.AoU1@1", "AoU1"), + _req("req Pkg.AoU1@2", "AoU1"), + ] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + with self.assertRaises(SystemExit): + filter_forwarded_aous(entries, items) -class TestCreateLobsterOutput(unittest.TestCase): - """Tests for create_lobster_output.""" + def test_ambiguous_base_id_still_resolves_with_explicit_version(self) -> None: + items = [ + _req("req Pkg.AoU1@1", "AoU1"), + _req("req Pkg.AoU1@2", "AoU1"), + ] + entries = [{"aou_id": "Pkg.AoU1@2", "justification": "reason"}] + filtered = filter_forwarded_aous(entries, items) + self.assertEqual(len(filtered), 1) + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1@2") - def test_wraps_items(self) -> None: - items = [{"tag": "req Foo.Bar", "name": "Bar"}] - output = create_lobster_output(items) - self.assertEqual(output["schema"], "lobster-req-trace") - self.assertEqual(output["version"], 3) - self.assertEqual(output["generator"], "aou_forwarding_to_lobster") - self.assertEqual(output["data"], items) + def test_duplicate_identical_item_is_not_ambiguous(self) -> None: + """The same AoU loaded twice (e.g. via two overlapping input files) is not a conflict.""" + items = [ + _req("req Pkg.AoU1@1", "AoU1"), + _req("req Pkg.AoU1@1", "AoU1"), + ] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + filtered = filter_forwarded_aous(entries, items) + self.assertEqual(len(filtered), 1) + + +class TestBuildForwardedMarkers(unittest.TestCase): + """Tests for build_forwarded_markers.""" + + def test_builds_one_marker_per_entry(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1"), _req("req Pkg.AoU2@1", "AoU2")] + entries = [ + {"aou_id": "Pkg.AoU1", "justification": "reason 1"}, + {"aou_id": "Pkg.AoU2", "justification": "reason 2"}, + ] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(len(markers), 2) + + def test_marker_has_distinct_tag_and_refs_original(self) -> None: + """The marker's tag must not collide with the original item's tag + (so it can coexist with the "Received AoUs" level in the same + report), but its refs must point at the original tag.""" + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + marker = markers[0] + self.assertNotEqual(str(marker.tag), "req Pkg.AoU1@1") + self.assertEqual(str(marker.tag), "req Pkg.AoU1__forwarded") + self.assertEqual( + [str(ref) for ref in marker.unresolved_references], + ["req Pkg.AoU1@1"], + ) + + def test_marker_uses_justification_as_text(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "must be handled downstream"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(markers[0].text, "must be handled downstream") + + def test_marker_kind_and_framework(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") + self.assertEqual(markers[0].kind, "ForwardedAoU") + self.assertEqual(markers[0].framework, "AoUForwarding") + + def test_marker_location_uses_yaml_path(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + markers = build_forwarded_markers(entries, items, "some/path/aou_forwarding.yaml") + self.assertEqual(markers[0].location.filename, "some/path/aou_forwarding.yaml") + + def test_nonexistent_aou_id_raises(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] + with self.assertRaises(SystemExit): + build_forwarded_markers(entries, items, "aou_forwarding.yaml") - def test_empty_items(self) -> None: - output = create_lobster_output([]) - self.assertEqual(output["data"], []) + def test_empty_entries_produces_no_markers(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + markers = build_forwarded_markers([], items, "aou_forwarding.yaml") + self.assertEqual(markers, []) if __name__ == "__main__": diff --git a/bazel/rules/rules_score/test/unit_component_test.bzl b/bazel/rules/rules_score/test/unit_component_test.bzl index 7ad2b46a..71efcf74 100644 --- a/bazel/rules/rules_score/test/unit_component_test.bzl +++ b/bazel/rules/rules_score/test/unit_component_test.bzl @@ -21,6 +21,7 @@ Tests the new hierarchical structure for S-CORE process compliance: load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("@score_tooling//bazel/rules/rules_score:providers.bzl", "ComponentInfo", "SphinxSourcesInfo", "UnitInfo") +load("@trlc//:trlc.bzl", "TrlcProviderInfo") # ============================================================================ # Unit Tests @@ -132,6 +133,39 @@ def _component_provider_test_impl(ctx): component_provider_test = analysistest.make(_component_provider_test_impl) +def _component_requirements_transitive_test_impl(ctx): + """Test that ComponentInfo.requirements_transitive rolls up nested components. + + :test_parent_component nests :test_nested_component, and the two use + DIFFERENT component_requirements targets (:comp_req vs. :comp_req_rst, + see test/BUILD). Both must be present in the parent's + requirements_transitive, or the nested component's own requirements + silently stopped being rolled up (and thus stopped being covered by any + CI-visible traceability check at the dependable_element level). + """ + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + comp_info = target_under_test[ComponentInfo] + basenames = [f.basename for f in comp_info.requirements_transitive.to_list()] + + asserts.true( + env, + "comp_req.lobster" in basenames, + "Parent component's own requirements (:comp_req) missing from " + + "requirements_transitive; got: %s" % basenames, + ) + asserts.true( + env, + "comp_req_rst.lobster" in basenames, + "Nested component's requirements (:comp_req_rst) did not roll up into " + + "the parent's requirements_transitive; got: %s" % basenames, + ) + + return analysistest.end(env) + +component_requirements_transitive_test = analysistest.make(_component_requirements_transitive_test_impl) + def _component_sphinx_sources_test_impl(ctx): """Test that component rule provides SphinxSourcesInfo.""" env = analysistest.begin(ctx) @@ -287,6 +321,46 @@ dependable_element_test_case_coverage_lock_check_action_test = analysistest.make # Note: Provider tests removed as dependable_element no longer creates a # separate provider target. The main target is now a sphinx_module. +def _dependable_element_received_aous_union_test_impl(ctx): + """Test that the `_received_aous` sibling target unions own + received AoU trlc. + + :test_dependable_element_received_aous_union owns :aous_rst and depends on + :dep_seooc_lib (which owns :aous) -- two genuinely distinct + assumptions_of_use fixtures, so the union is observable: if either side + were dropped, one of the two basenames below would be missing. + """ + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + asserts.true( + env, + TrlcProviderInfo in target_under_test, + "_received_aous target should provide TrlcProviderInfo", + ) + + trlc_info = target_under_test[TrlcProviderInfo] + short_paths = [f.short_path for f in trlc_info.reqs.to_list()] + + # Both fixtures' rst_to_trlc output happens to share the same basename + # ("assumptions_of_use.trlc", sibling-declared next to their differently + # named source directories) -- so distinguish them by directory instead. + asserts.true( + env, + any(["rst_requirements/assumptions_of_use.trlc" in p for p in short_paths]), + "This element's own AoU (:aous_rst) missing from received_aous " + + "TrlcProviderInfo.reqs; got: %s" % short_paths, + ) + asserts.true( + env, + any(["seooc_test/assumptions_of_use.trlc" in p for p in short_paths]), + "AoU received (forwarded) from :dep_seooc_lib (:aous) missing from " + + "received_aous TrlcProviderInfo.reqs; got: %s" % short_paths, + ) + + return analysistest.end(env) + +dependable_element_received_aous_union_test = analysistest.make(_dependable_element_received_aous_union_test_impl) + # ============================================================================ # Test Suite Definition # ============================================================================ @@ -306,7 +380,9 @@ def unit_component_test_suite(name): ":component_sphinx_sources_test", ":component_excludes_feature_req_docs_test", ":component_test_case_coverage_lock_test", + ":component_requirements_transitive_test", ":test_case_coverage_lock_check_action_release_test", ":test_case_coverage_lock_check_action_development_test", + ":dependable_element_received_aous_union_test", ], ) diff --git a/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl b/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl index 3a2d44ce..fb96f64c 100644 --- a/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl +++ b/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl @@ -70,14 +70,22 @@ tuple FeatReqId { version Integer } +type ControlMeasure "A design or operational measure that detects, prevents, or limits a failure mode." extends RequirementSafety { + mitigates "Reference to the FailureMode or safety concern that this requirement mitigates." + optional String +} + +type AoU "Assumption of Use — a safety-relevant condition that a user must fulfil." extends ControlMeasure { +} + tuple CompReqSourceId { - item [FeatReq, AssumedSystemReq] + item [FeatReq, AssumedSystemReq, AoU] separator @ version Integer } type CompReq "Component-level requirement allocated to a specific software component." extends RequirementSafety { - derived_from "Versioned references to the FeatReq or AssumedSystemReq items this component requirement is derived from. Omit only for component-internal requirements with no feature-level parent." + derived_from "Versioned references to the FeatReq, AssumedSystemReq, or received AoU items this component requirement is derived from. An AoU reference must come from a target in this dependable_element's own assumptions_of_use or from one of its deps' forwarded/received AoUs (see the '_received_aous' target). Omit only for component-internal requirements with no feature-level parent." CompReqSourceId[1 .. *] } @@ -154,11 +162,6 @@ type FailureMode "A potential failure of a system function, described using one optional String } -type ControlMeasure "A design or operational measure that detects, prevents, or limits a failure mode." extends RequirementSafety { - mitigates "Reference to the FailureMode or safety concern that this requirement mitigates." - optional String -} - type PreventiveMeasure "A measure that prevents a failure mode from occurring in the first place." { } @@ -167,9 +170,6 @@ type Mitigation "A measure that reduces the severity or probability of a failure optional String } -type AoU "Assumption of Use — a safety-relevant condition that a user must fulfil." extends ControlMeasure { -} - tuple Measure { item [ControlMeasure, PreventiveMeasure, Mitigation] separator @