diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 08ff2788..d3656e84 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -76,6 +76,18 @@ py_binary( visibility = ["//visibility:public"], ) +# AoU forwarding filter: filters received AoU lobster entries for chain-forwarding +py_binary( + name = "aou_forwarding_to_lobster", + srcs = ["src/aou_forwarding_to_lobster.py"], + imports = ["src"], + main = "src/aou_forwarding_to_lobster.py", + visibility = ["//visibility:public"], + deps = [ + requirement("pyyaml"), + ], +) + # HTML merge tool py_binary( name = "sphinx_html_merge", diff --git a/bazel/rules/rules_score/docs/requirements/potential_errors.trlc b/bazel/rules/rules_score/docs/requirements/potential_errors.trlc index 4afe979e..22d4e592 100644 --- a/bazel/rules/rules_score/docs/requirements/potential_errors.trlc +++ b/bazel/rules/rules_score/docs/requirements/potential_errors.trlc @@ -97,4 +97,30 @@ section "Potential Errors" { } + section "AoU Forwarding" { + + ToolQualification.PotentialError AoU_Silently_Dropped { + description = ''' + An assumption of use defined by a dependency is not forwarded + to the dependee, causing the integrating project to be unaware + of a condition it must satisfy. + ''' + impacts = ["Safety-relevant assumption not communicated to integrator"] + affects = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] + impact_type = ToolQualification.Impact_Type.Safety + } + + ToolQualification.PotentialError Invalid_Forwarding_YAML_Accepted { + description = ''' + A chain-forwarding YAML file that references a non-existent + AoU identifier is accepted without error, causing the build to + succeed while the intended forwarding does not take effect. + ''' + impacts = ["Chain-forwarded AoU missing from traceability report"] + affects = [Forward_AoU_To_Dependees] + impact_type = ToolQualification.Impact_Type.Safety + } + + } + } diff --git a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc index 981646b2..0af48c4c 100644 --- a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc +++ b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc @@ -123,4 +123,57 @@ section "Tool Requirements" { } + section "AoU Forwarding" { + + ToolQualification.ToolRequirement Forward_Own_AoUs_To_Dependees { + description = ''' + 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. + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + + ToolQualification.ToolRequirement Chain_Forward_Received_AoUs { + description = ''' + The dependable_element rule shall support an aou_forwarding + attribute pointing to a YAML file that selects which received + AoUs are forwarded further to elements that depend on this + element. Each entry in the YAML shall require a mandatory + justification field. + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + + ToolQualification.ToolRequirement Reject_Unknown_AoU_In_Forwarding_YAML { + description = ''' + The AoU forwarding tool shall exit with a non-zero return code + when the forwarding YAML references an AoU identifier that does + not exist in the set of received AoUs, preventing silent + misconfiguration. + ''' + mitigates = [Invalid_Forwarding_YAML_Accepted] + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + + 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). + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees, Gate_Traceability_At_Test_Time] + satisfied_by = Tools.Lobster + } + + } + } diff --git a/bazel/rules/rules_score/docs/requirements/use_cases.trlc b/bazel/rules/rules_score/docs/requirements/use_cases.trlc index 3ba2951d..bb40b259 100644 --- a/bazel/rules/rules_score/docs/requirements/use_cases.trlc +++ b/bazel/rules/rules_score/docs/requirements/use_cases.trlc @@ -142,6 +142,17 @@ section "Use Cases" { affected_tools = [Tools.Docs, Tools.Bazel] } + ToolQualification.UseCase Forward_AoU_To_Dependees { + description = ''' + As a system integrator I want assumptions of use defined by a + dependable element to be automatically forwarded to the elements + that depend on it so that the integrating project is made aware + of all conditions it must satisfy — including those originating + from transitive dependencies. + ''' + affected_tools = [Tools.Bazel, Tools.Lobster] + } + ToolQualification.UseCase Validate_Architecture_Specification_Documents { description = ''' As a software architect I want the build to verify that architectural diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index ceca4bf1..0204d0cb 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -625,6 +625,10 @@ and scope checks at build/test time. - label list - no - Other ``dependable_element`` targets for cross-referencing and HTML merging (default ``[]``) + * - ``aou_forwarding`` + - label + - no + - A YAML file selecting which *received* AoUs to chain-forward to elements that depend on this one. Each entry requires an ``aou_id`` and a ``justification``. Own AoUs (from ``assumptions_of_use``) are always forwarded automatically. * - ``maturity`` - string - no diff --git a/bazel/rules/rules_score/docs/user_guide/general.md b/bazel/rules/rules_score/docs/user_guide/general.md index 21d9c177..6a662bed 100644 --- a/bazel/rules/rules_score/docs/user_guide/general.md +++ b/bazel/rules/rules_score/docs/user_guide/general.md @@ -26,6 +26,7 @@ A *dependable element* is the top-level unit of certification work. It bundles: | Assumed System Requirements | System-level requirements given as constraints from the surrounding context | | Feature Requirements | Functional and safety requirements for this element | | Assumptions of Use | Conditions the integrating project must satisfy | +| Forwarded AoUs | Assumptions of use received from dependencies that must be handled or forwarded further | | Architectural Design | Software Architectural Design in PlantUML | | Software Units and Components | Implementation targets linked to their design | | Dependability Analysis | FMEA, FTA diagrams and control measures | @@ -60,6 +61,7 @@ dependable_element( name = "safety_software_seooc_example", architectural_design = ["//bazel/rules/rules_score/examples/seooc/design:sample_seooc_design"], assumptions_of_use = [], + aou_forwarding = "aou_forwarding.yaml", # chain-forward selected received AoUs components = [":component_example"], dependability_analysis = [":sample_dependability_analysis"], integrity_level = "B", diff --git a/bazel/rules/rules_score/docs/user_guide/requirements.md b/bazel/rules/rules_score/docs/user_guide/requirements.md index 116044bf..efa0ccdf 100644 --- a/bazel/rules/rules_score/docs/user_guide/requirements.md +++ b/bazel/rules/rules_score/docs/user_guide/requirements.md @@ -129,6 +129,59 @@ ScoreReq.AoU AOU_001 { } ``` +### AoU Forwarding + +When a dependable element depends on another via `deps`, all **assumptions of use** defined by the dependency are automatically forwarded to the dependee. This ensures the integrating project is made aware of every condition it must satisfy — even those originating from transitive dependencies. + +There are two forwarding mechanisms: + +**Automatic forwarding (own AoUs)** +All AoUs declared in a dependable element's `assumptions_of_use` attribute are automatically forwarded to every element that lists it in `deps`. No configuration is needed. + +**Chain-forwarding (received AoUs)** +When a dependable element receives forwarded AoUs from its own dependencies, it can selectively forward them further by providing an `aou_forwarding` YAML file. Each entry requires a mandatory justification explaining *why* this AoU is forwarded rather than handled locally: + +```yaml +# aou_forwarding.yaml +forwarded_aous: + - aou_id: "OtherLibrary.TimingConstraint" + justification: > + This timing constraint originates from the underlying library and + must be satisfied by the final system integrator who controls scheduling. +``` + +**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: + +- 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 + +If a forwarded AoU is not handled, the `bazel test` traceability check will fail. + +**Example: three-level forwarding chain** + +``` +other_seooc → defines AoU: TimingConstraint + ↑ (deps) +middle_seooc → auto-forwards TimingConstraint + - also chain-forwards it via aou_forwarding.yaml + ↑ (deps) +integrator_seooc → receives TimingConstraint, must handle it +``` + +```{code-block} starlark +:caption: middle_seooc/BUILD + +dependable_element( + name = "middle_seooc", + assumptions_of_use = [":my_aous"], + aou_forwarding = "aou_forwarding.yaml", + deps = ["//other:other_seooc"], + ... +) +``` + ## Allocation of Requirements to Architectural Elements Requirements are allocated to architectural elements differently depending on their level: diff --git a/bazel/rules/rules_score/examples/integrator/BUILD b/bazel/rules/rules_score/examples/integrator/BUILD new file mode 100644 index 00000000..0a58c216 --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/BUILD @@ -0,0 +1,86 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# +# Example: System integrator SEooC that depends on safety_software_seooc_example. +# +# This illustrates the full AoU forwarding chain: +# +# other_seooc defines AoU: OtherLibrary.TimingConstraint +# ↑ (deps) +# safety_software_seooc_example +# - defines own AoU: SampleType.SampleAoU (auto-forwarded here) +# - 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 +# + +load( + "//bazel/rules/rules_score:rules_score.bzl", + "architectural_design", + "component", + "dependable_element", + "unit", +) + +cc_library( + name = "integrator_lib", + srcs = [], + visibility = ["//visibility:public"], + deps = [ + "//bazel/rules/rules_score/examples/seooc:sample_library", + ], +) + +architectural_design( + name = "integrator_design", + static = ["static_design.puml"], +) + +unit( + name = "integrator_unit", + scope = ["//bazel/rules/rules_score/examples/integrator:__pkg__"], + tests = [], + unit_design = [], + implementation = [":integrator_lib"], +) + +component( + name = "integrator_component", + components = [":integrator_unit"], + requirements = [ + "//bazel/rules/rules_score/examples/integrator/docs/requirements:component_requirements", + ], + tags = ["manual"], + tests = [], +) + +# The integrator depends on safety_software_seooc_example and therefore +# receives all forwarded AoUs: +# - SampleType.SampleAoU (auto-forwarded, own AoU of seooc) +# - OtherLibrary.TimingConstraint (chain-forwarded from other_seooc through seooc) +dependable_element( + name = "integrator_seooc", + architectural_design = [":integrator_design"], + assumptions_of_use = [], + components = [":integrator_component"], + dependability_analysis = [], + integrity_level = "B", + requirements = [ + "//bazel/rules/rules_score/examples/integrator/docs/requirements:feature_requirements", + ], + tests = [], + deps = ["//bazel/rules/rules_score/examples/seooc:safety_software_seooc_example"], +) diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD b/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD new file mode 100644 index 00000000..28341d1e --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/BUILD @@ -0,0 +1,44 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "assumed_system_requirements", "component_requirements", "feature_requirements") + +assumed_system_requirements( + name = "assumed_system_requirements", + srcs = [ + "assumed_system_requirements.trlc", + ], + visibility = ["//visibility:public"], +) + +feature_requirements( + name = "feature_requirements", + srcs = [ + "feature_requirements.trlc", + ], + visibility = ["//visibility:public"], + deps = [ + ":assumed_system_requirements", + ], +) + +component_requirements( + name = "component_requirements", + srcs = [ + "component_requirements.trlc", + ], + visibility = ["//visibility:public"], + deps = [ + ":feature_requirements", + ], +) diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/assumed_system_requirements.trlc b/bazel/rules/rules_score/examples/integrator/docs/requirements/assumed_system_requirements.trlc new file mode 100644 index 00000000..561890a2 --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/assumed_system_requirements.trlc @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +package Integrator + +import ScoreReq + +/////////////////////////////// +// Assumed System Requirements +// System level requirements for the integrator +/////////////////////////////// + +ScoreReq.AssumedSystemReq ASR_INT_001 { + description = "The system shall integrate the numeric value management SEooC and invoke its interfaces within real-time constraints" + safety = ScoreReq.Asil.B + version = 1 + rationale = "System-level requirement for integrating a safety-qualified SEooC into the target platform" +} + +ScoreReq.AssumedSystemReq ASR_INT_002 { + description = "The system shall detect and react to faults reported by integrated SEooC components within a bounded time" + safety = ScoreReq.Asil.B + version = 1 + rationale = "System-level requirement for fault handling in a safety-critical integration context" +} 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 new file mode 100644 index 00000000..b3becb90 --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/component_requirements.trlc @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +package IntegratorComponent + +import ScoreReq +import Integrator + +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] + version = 1 +} + +ScoreReq.CompReq COMP_INT_002 { + description = "The cyclic task shall invoke the validation interface every 10ms using a hardware timer interrupt" + safety = ScoreReq.Asil.B + derived_from = [Integrator.FEAT_INT_002@1] + version = 1 +} + +ScoreReq.CompReq COMP_INT_003 { + description = "The fault handler shall assert the safe-state output within 50ms of receiving a validation failure" + safety = ScoreReq.Asil.B + derived_from = [Integrator.FEAT_INT_003@1] + version = 1 +} + +ScoreReq.CompReq COMP_INT_004 { + description = "The diagnostic reporter shall provide a health status register readable via the diagnostic interface" + safety = ScoreReq.Asil.B + derived_from = [Integrator.FEAT_INT_004@1] + version = 1 +} diff --git a/bazel/rules/rules_score/examples/integrator/docs/requirements/feature_requirements.trlc b/bazel/rules/rules_score/examples/integrator/docs/requirements/feature_requirements.trlc new file mode 100644 index 00000000..25df47d2 --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/docs/requirements/feature_requirements.trlc @@ -0,0 +1,48 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +package Integrator + +import ScoreReq + +/////////////////////////////// +// Feature Requirements +// Requirements for the system integrator that incorporates safety_software_seooc +/////////////////////////////// + +ScoreReq.FeatReq FEAT_INT_001 { + description = "The integrator shall initialize the numeric value manager provided by the SEooC within the system startup sequence" + safety = ScoreReq.Asil.B + derived_from = [Integrator.ASR_INT_001@1] + version = 1 +} + +ScoreReq.FeatReq FEAT_INT_002 { + description = "The integrator shall invoke the validation interface cyclically with a period no greater than 10ms to ensure timely fault detection" + safety = ScoreReq.Asil.B + derived_from = [Integrator.ASR_INT_001@1] + version = 1 +} + +ScoreReq.FeatReq FEAT_INT_003 { + description = "The integrator shall transition to a safe state within 50ms if the validation interface returns a failure result" + safety = ScoreReq.Asil.B + derived_from = [Integrator.ASR_INT_002@1] + version = 1 +} + +ScoreReq.FeatReq FEAT_INT_004 { + description = "The integrator shall provide a diagnostic reporting interface that exposes the current health status of all managed SEooC components" + safety = ScoreReq.Asil.B + derived_from = [Integrator.ASR_INT_002@1] + version = 1 +} diff --git a/bazel/rules/rules_score/examples/integrator/static_design.puml b/bazel/rules/rules_score/examples/integrator/static_design.puml new file mode 100644 index 00000000..f7aa98a1 --- /dev/null +++ b/bazel/rules/rules_score/examples/integrator/static_design.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 + +package "Integrator SEooC" as integrator_seooc <> { + component "IntegratorComponent" as integrator_component <> { + component "IntegratorUnit" as integrator_unit <> + } +} + +@enduml diff --git a/bazel/rules/rules_score/examples/seooc/BUILD b/bazel/rules/rules_score/examples/seooc/BUILD index e0ef69cb..6c5d1b20 100644 --- a/bazel/rules/rules_score/examples/seooc/BUILD +++ b/bazel/rules/rules_score/examples/seooc/BUILD @@ -49,8 +49,11 @@ dependability_analysis( dependable_element( name = "safety_software_seooc_example", + # Chain-forward received AoUs that this element cannot handle itself. + # TimingConstraint from other_seooc is forwarded to dependees (integrator). + aou_forwarding = "aou_forwarding.yaml", architectural_design = ["//bazel/rules/rules_score/examples/seooc/design:sample_seooc_design"], - assumptions_of_use = [], + assumptions_of_use = ["//bazel/rules/rules_score/examples/seooc/docs:sample_aous"], components = [":component_example"], dependability_analysis = [ ":sample_dependability_analysis", @@ -60,5 +63,6 @@ dependable_element( "//bazel/rules/rules_score/examples/seooc/docs/requirements:feature_requirements", ], tests = [], + visibility = ["//:__subpackages__"], deps = ["//bazel/rules/rules_score/examples/some_other_library:other_seooc"], ) diff --git a/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml b/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml new file mode 100644 index 00000000..22ddbba9 --- /dev/null +++ b/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml @@ -0,0 +1,23 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# +# Chain-forwarding configuration for safety_software_seooc_example. +# +# This SEooC depends on other_seooc (some_other_library) and receives its AoU +# "OtherLibrary.TimingConstraint". Since this element cannot satisfy the timing +# constraint itself (it is a library, not a runtime scheduler), it forwards the +# AoU to its own dependees — the system integrator. +# +forwarded_aous: + - aou_id: "OtherLibrary.TimingConstraint" + justification: "This SEooC is a library component and has no control over the invocation cycle time. The system integrator must ensure that calls to the library do not exceed the 10ms cycle time constraint imposed by the underlying other_seooc dependency." diff --git a/bazel/rules/rules_score/examples/seooc/docs/BUILD b/bazel/rules/rules_score/examples/seooc/docs/BUILD index e2d391f4..07ec428e 100644 --- a/bazel/rules/rules_score/examples/seooc/docs/BUILD +++ b/bazel/rules/rules_score/examples/seooc/docs/BUILD @@ -11,6 +11,10 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* load("@trlc//:trlc.bzl", "trlc_requirements", "trlc_requirements_test") +load( + "//bazel/rules/rules_score:rules_score.bzl", + "assumptions_of_use", +) trlc_requirements( name = "aous", @@ -23,6 +27,12 @@ trlc_requirements( visibility = ["//visibility:public"], ) +assumptions_of_use( + name = "sample_aous", + srcs = [":aous"], + visibility = ["//visibility:public"], +) + trlc_requirements_test( name = "aous_test", reqs = [ diff --git a/bazel/rules/rules_score/examples/some_other_library/BUILD b/bazel/rules/rules_score/examples/some_other_library/BUILD index be639cc5..c53d97d3 100644 --- a/bazel/rules/rules_score/examples/some_other_library/BUILD +++ b/bazel/rules/rules_score/examples/some_other_library/BUILD @@ -10,9 +10,11 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@trlc//:trlc.bzl", "trlc_requirements") load( "//bazel/rules/rules_score:rules_score.bzl", "architectural_design", + "assumptions_of_use", "component", "dependable_element", "unit", @@ -25,6 +27,17 @@ architectural_design( ], ) +trlc_requirements( + name = "aous_trlc", + srcs = ["aous.trlc"], + spec = ["@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model"], +) + +assumptions_of_use( + name = "other_library_aous", + srcs = [":aous_trlc"], +) + unit( name = "abc", scope = ["//bazel/rules/rules_score/examples/some_other_library:__pkg__"], @@ -48,7 +61,7 @@ component( dependable_element( name = "other_seooc", architectural_design = [":sample_seooc_design"], - assumptions_of_use = [], + assumptions_of_use = [":other_library_aous"], components = [":component_example"], dependability_analysis = [], integrity_level = "D", diff --git a/bazel/rules/rules_score/examples/some_other_library/aous.trlc b/bazel/rules/rules_score/examples/some_other_library/aous.trlc new file mode 100644 index 00000000..8922285f --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/aous.trlc @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +package OtherLibrary + +import ScoreReq + +ScoreReq.AoU TimingConstraint { + description = "The caller shall ensure that invocations do not exceed 10ms cycle time" + safety = ScoreReq.Asil.B + version = 1 +} + +ScoreReq.AoU SpaceConstraint { + description = "The caller shall ensure that invocations do not exceed 10 MB" + safety = ScoreReq.Asil.B + version = 1 +} diff --git a/bazel/rules/rules_score/lobster/config/BUILD b/bazel/rules/rules_score/lobster/config/BUILD index 8efc55e0..466b5ef0 100644 --- a/bazel/rules/rules_score/lobster/config/BUILD +++ b/bazel/rules/rules_score/lobster/config/BUILD @@ -44,6 +44,12 @@ filegroup( visibility = ["//visibility:public"], ) +filegroup( + name = "aou_config", + srcs = ["lobster_aou.yaml"], + visibility = ["//visibility:public"], +) + # --------------------------------------------------------------------------- # Lobster report config templates: static structure with {PLACEHOLDER} markers # for source file lists. Used by rules_score to generate per-target configs diff --git a/bazel/rules/rules_score/lobster/config/lobster_aou.yaml b/bazel/rules/rules_score/lobster/config/lobster_aou.yaml new file mode 100644 index 00000000..88ffb22a --- /dev/null +++ b/bazel/rules/rules_score/lobster/config/lobster_aou.yaml @@ -0,0 +1,21 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# lobster-trlc configuration for Assumptions of Use records. +inputs: + - . +conversion-rules: + - package: ScoreReq + record-type: AoU + namespace: req + version-field: version + description-fields: description 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 d01bc5a7..b4ccfd6b 100644 --- a/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl +++ b/bazel/rules/rules_score/lobster/config/lobster_de.conf.tpl @@ -2,6 +2,10 @@ requirements "Feature Requirements" { {FEAT_REQ_SOURCES} } +requirements "Forwarded AoUs" { +{FORWARDED_AOU_SOURCES} +} + requirements "Component Requirements" { {COMP_REQ_SOURCES}{COMP_REQ_TRACE}} diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index f0e69914..f5a522cc 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -19,6 +19,7 @@ following S-CORE process guidelines. Assumptions of Use define the safety-releva operating conditions and constraints for a Safety Element out of Context (SEooC). """ +load("@lobster//:lobster.bzl", "subrule_lobster_trlc") load("@trlc//:trlc.bzl", "TrlcProviderInfo", "trlc_requirements_test") load("//bazel/rules/rules_score:providers.bzl", "AssumptionsOfUseInfo", "ComponentRequirementsInfo", "FeatureRequirementsInfo", "SphinxSourcesInfo") load("//bazel/rules/rules_score/private:rst_to_trlc.bzl", "rst_srcs_to_trlc") @@ -61,6 +62,14 @@ def _assumptions_of_use_impl(ctx): all_srcs = depset(rendered_files) + # Generate lobster file from AoU TRLC sources for traceability forwarding + all_trlc_files = [] + for src in ctx.attr.srcs: + all_trlc_files.extend(src[DefaultInfo].files.to_list()) + aou_lobster_file = None + if all_trlc_files and ctx.file.lobster_config: + aou_lobster_file, _ = subrule_lobster_trlc(all_trlc_files, ctx.file.lobster_config) + # Collect requirements providers and lobster files reqs = [] lobster_files = [] @@ -84,6 +93,7 @@ def _assumptions_of_use_impl(ctx): DefaultInfo(files = all_srcs), AssumptionsOfUseInfo( srcs = depset(transitive = lobster_files), + aou_lobster = depset([aou_lobster_file] if aou_lobster_file else []), name = ctx.label.name, ), SphinxSourcesInfo( @@ -111,6 +121,11 @@ _assumptions_of_use = rule( mandatory = False, doc = "List of feature or component requirements targets that these Assumptions of Use trace to", ), + "lobster_config": attr.label( + allow_single_file = True, + mandatory = True, + doc = "Lobster YAML configuration file for AoU traceability extraction.", + ), "_renderer": attr.label( default = Label("@trlc//tools/trlc_rst:trlc_rst"), executable = True, @@ -118,6 +133,7 @@ _assumptions_of_use = rule( cfg = "exec", ), }, + subrules = [subrule_lobster_trlc], ) # ============================================================================ @@ -129,6 +145,7 @@ def assumptions_of_use( srcs, requirements = [], ref_package = None, + lobster_config = Label("//bazel/rules/rules_score/lobster/config:aou_config"), **kwargs): """Define Assumptions of Use following S-CORE process guidelines. @@ -149,6 +166,8 @@ def assumptions_of_use( traceability as defined in the S-CORE process. ref_package: Optional TRLC package prefix used for ``derived_from`` cross-references when converting RST sources. + lobster_config: Lobster YAML configuration for AoU traceability + extraction. Defaults to the standard S-CORE AoU config. visibility: Bazel visibility specification for the generated targets. Generated Targets: @@ -179,6 +198,7 @@ def assumptions_of_use( name = name, srcs = trlc_srcs, requirements = requirements, + lobster_config = lobster_config, **kwargs ) trlc_requirements_test( diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 8d391749..98457645 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -29,12 +29,14 @@ load( "//bazel/rules/rules_score:providers.bzl", "ArchitecturalDesignInfo", "AssumedSystemRequirementsInfo", + "AssumptionsOfUseInfo", "CertifiedScope", "ComponentInfo", "DependabilityAnalysisInfo", "DependableElementInfo", "DependableElementLobsterInfo", "FeatureRequirementsInfo", + "ForwardedAoUInfo", "SphinxIndexFileInfo", "SphinxModuleInfo", "SphinxNeedsInfo", @@ -1011,19 +1013,70 @@ def _dependable_element_index_impl(ctx): cm_list = [sa_lobster_files["controlmeasures.lobster"]] if "controlmeasures.lobster" in sa_lobster_files else [] rc_list = [sa_lobster_files["root_causes.lobster"]] if "root_causes.lobster" in sa_lobster_files else [] + # ========================================================================= + # AoU Forwarding: collect own AoUs and received AoUs from deps + # ========================================================================= + + # Collect own AoU lobster 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) + + # Collect forwarded AoU lobster files from deps (received AoUs) + received_aou_lobster_files = [] + for dep in ctx.attr.processed_deps: + if ForwardedAoUInfo in dep: + fwd_info = dep[ForwardedAoUInfo] + received_aou_lobster_files.append(fwd_info.own_aou_lobster) + received_aou_lobster_files.append(fwd_info.chain_forwarded_lobster) + + 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_forwarded_lobster_depset = depset() + if ctx.file.aou_forwarding and received_aou_list: + chain_forwarded_lobster_file = ctx.actions.declare_file( + ctx.label.name + "/chain_forwarded_aous.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("--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], + 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]) + output_files.append(chain_forwarded_lobster_file) + lobster_report_file = None lobster_html_report = None lobster_rst_dir = None lobster_files = [] if feat_req_list: + # Build comp_req trace-to lines (Feature Requirements + optionally Forwarded AoUs) + comp_req_trace_lines = "" + if comp_req_list: + comp_req_trace_lines = " trace to: \"Feature Requirements\";\n" + lobster_config = ctx.actions.declare_file(ctx.label.name + "/de_traceability_config") ctx.actions.expand_template( template = ctx.file._lobster_de_template, output = lobster_config, substitutions = { "{FEAT_REQ_SOURCES}": format_lobster_sources(feat_req_list), + "{FORWARDED_AOU_SOURCES}": format_lobster_sources(received_aou_list), "{COMP_REQ_SOURCES}": format_lobster_sources(comp_req_list), - "{COMP_REQ_TRACE}": (" trace to: \"Feature Requirements\";\n") if comp_req_list else "", + "{COMP_REQ_TRACE}": comp_req_trace_lines, "{ARCH_SOURCES}": format_lobster_sources(comp_arch_list), "{UNIT_TEST_SOURCES}": format_lobster_sources(comp_test_list), "{PUBLIC_API_SOURCES}": format_lobster_sources(interface_req_list), @@ -1033,7 +1086,7 @@ def _dependable_element_index_impl(ctx): }, ) - all_lobster_inputs = feat_req_list + comp_req_list + comp_arch_list + comp_test_list + interface_req_list + fm_list + cm_list + rc_list + 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 lobster_report_file = subrule_lobster_report(all_lobster_inputs, lobster_config) lobster_html_report = subrule_lobster_html_report(lobster_report_file) @@ -1077,6 +1130,10 @@ def _dependable_element_index_impl(ctx): lobster_html_report = lobster_html_report, lobster_rst_dir = lobster_rst_dir, ), + ForwardedAoUInfo( + own_aou_lobster = own_aou_lobster_depset, + chain_forwarded_lobster = chain_forwarded_lobster_depset, + ), OutputGroupInfo(debug = depset([validation_log])), ] @@ -1132,6 +1189,11 @@ _dependable_element_index = rule( default = [], doc = "Dependencies on other dependable element modules (submodules).", ), + "aou_forwarding": attr.label( + allow_single_file = [".yaml", ".yml"], + default = None, + doc = "Optional YAML file listing received AoU IDs to further-forward to this element's own dependees. Only needed for chain-forwarding.", + ), "integrity_level": attr.string( mandatory = True, values = _INTEGRITY_LEVELS, @@ -1159,6 +1221,12 @@ _dependable_element_index = rule( cfg = "exec", doc = "Lobster RST report tool for generating the multi-page Sphinx traceability report.", ), + "_aou_forwarding_tool": attr.label( + default = Label("//bazel/rules/rules_score:aou_forwarding_to_lobster"), + executable = True, + cfg = "exec", + doc = "Tool for filtering received AoU lobster entries based on chain-forwarding YAML.", + ), }, **VERBOSITY_ATTR ), @@ -1272,6 +1340,7 @@ def dependable_element( integrity_level, checklists = [], deps = [], + aou_forwarding = None, maturity = "release", sphinx = Label("//bazel/rules/rules_score:score_build"), testonly = True, @@ -1308,6 +1377,9 @@ def dependable_element( safety checklists and verification documents. deps: Optional list of other module targets this element depends on. Cross-references will work automatically. + aou_forwarding: Optional label to a YAML file listing received AoU IDs + to further-forward to this element's own dependees. Only needed for + chain-forwarding received AoUs that this element cannot handle. sphinx: Label to sphinx build binary. Default: //bazel/rules/rules_score:score_build testonly: If True, only testonly targets can depend on this target. @@ -1337,6 +1409,7 @@ def dependable_element( tests = tests, deps = deps, processed_deps = processed_deps, + aou_forwarding = aou_forwarding, integrity_level = integrity_level, maturity = maturity, testonly = testonly, diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 7c952e9f..5dfc7f27 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -122,11 +122,25 @@ AssumptionsOfUseInfo = provider( doc = "Provider for assumptions of use artifacts.", fields = { "srcs": "Depset of .lobster traceability files collected from all linked requirements targets.", + "aou_lobster": "Depset of .lobster traceability files generated from the AoU TRLC sources themselves (used for forwarding to dependees).", "requirements": "List of FeatureRequirementsInfo or ComponentRequirementsInfo providers this AoU traces to.", "name": "Name of the assumptions of use target.", }, ) +ForwardedAoUInfo = provider( + doc = """Carries AoU lobster 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 + lobster traceability report or further-forward them. + """, + 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).", + }, +) + ComponentInfo = provider( doc = "Provider for component artifacts.", fields = { diff --git a/bazel/rules/rules_score/requirements.in b/bazel/rules/rules_score/requirements.in index c09147dc..b22a28a7 100644 --- a/bazel/rules/rules_score/requirements.in +++ b/bazel/rules/rules_score/requirements.in @@ -1,4 +1,5 @@ myst-parser +pyyaml readthedocs-sphinx-ext rst2pdf sphinx diff --git a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py new file mode 100644 index 00000000..5d19702a --- /dev/null +++ b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py @@ -0,0 +1,202 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Filter received AoU lobster entries for chain-forwarding. + +Reads a chain-forwarding YAML file and one or more received AoU .lobster +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. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +import yaml + + +def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: + """Parse the AoU forwarding YAML file. + + Args: + yaml_path: Path to the YAML file. + + Returns: + List of dicts with 'aou_id' and 'justification' keys. + + Raises: + SystemExit: If YAML is malformed or missing required fields. + """ + try: + with open(yaml_path, encoding="utf-8") as f: + data = yaml.safe_load(f) + except (OSError, yaml.YAMLError) as e: + raise SystemExit(f"Failed to parse YAML {yaml_path}: {e}") from e + + if not isinstance(data, dict) or "forwarded_aous" not in data: + raise SystemExit( + f"YAML {yaml_path} must contain a 'forwarded_aous' key with a list of entries." + ) + + entries = data["forwarded_aous"] + if not isinstance(entries, list): + raise SystemExit(f"YAML {yaml_path}: 'forwarded_aous' must be a list.") + + result = [] + for i, entry in enumerate(entries): + if not isinstance(entry, dict): + raise SystemExit( + f"YAML {yaml_path}: entry {i} must be a mapping with 'aou_id' and 'justification'." + ) + aou_id = entry.get("aou_id") + justification = entry.get("justification") + if not aou_id: + raise SystemExit( + f"YAML {yaml_path}: entry {i} is missing required field 'aou_id'." + ) + if not justification: + raise SystemExit( + f"YAML {yaml_path}: entry {i} (aou_id='{aou_id}') is missing required field 'justification'." + ) + result.append({"aou_id": aou_id, "justification": justification}) + + return result + + +def load_lobster_items(lobster_paths: list[str]) -> list[dict]: + """Load all 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. + """ + all_items = [] + for path in lobster_paths: + try: + with open(path, encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError) as e: + raise SystemExit(f"Failed to parse lobster file {path}: {e}") from e + all_items.extend(data.get("data", [])) + return all_items + + +def filter_forwarded_aous( + forwarding_entries: list[dict[str, str]], + lobster_items: list[dict], +) -> list[dict]: + """Filter lobster items to only those listed in the forwarding YAML. + + Matches by checking if the AoU ID appears in the lobster item's tag. + Lobster-trlc generates tags like "req PackageName.RecordName@version". + The YAML can reference either the full versioned ID or the base name + (without @version suffix). + + 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. + """ + # 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] = {} + 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 = [] + 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)" + raise SystemExit( + f"AoU ID '{aou_id}' listed in forwarding YAML not found in received AoUs. " + f"Available IDs: {available}" + ) + filtered.append(item_by_id[aou_id]) + + return filtered + + +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 main() -> None: + """Entry point for the AoU forwarding filter tool.""" + parser = argparse.ArgumentParser( + description="Filter received AoU lobster entries for chain-forwarding." + ) + parser.add_argument( + "--yaml", + required=True, + help="Path to the aou_forwarding.yaml file listing AoU IDs to further-forward.", + ) + parser.add_argument( + "--input-lobster", + nargs="+", + required=True, + help="One or more .lobster files received from deps containing AoU entries.", + ) + parser.add_argument( + "--output", + required=True, + help="Output .lobster file path for the filtered entries.", + ) + + args = parser.parse_args() + + # Parse YAML + forwarding_entries = parse_forwarding_yaml(args.yaml) + + # Load received lobster items + lobster_items = load_lobster_items(args.input_lobster) + + # Filter + 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") + + +if __name__ == "__main__": + main() diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 3b3402b8..8ae23dcd 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -751,6 +751,13 @@ py_test( deps = ["//bazel/rules/rules_score:safety_analysis_tools"], ) +py_test( + name = "test_aou_forwarding_to_lobster", + size = "small", + srcs = ["test_aou_forwarding_to_lobster.py"], + deps = ["//bazel/rules/rules_score:aou_forwarding_to_lobster"], +) + py_test( name = "test_rst_to_trlc", size = "small", @@ -779,6 +786,7 @@ test_suite( ":requirements_rst_tests", ":seooc_tests", ":sphinx_module_tests", + ":test_aou_forwarding_to_lobster", ":test_rst_to_trlc", ":test_safety_analysis_tools", ":test_trlc_rst_image_rendering", 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 new file mode 100644 index 00000000..49daead2 --- /dev/null +++ b/bazel/rules/rules_score/test/test_aou_forwarding_to_lobster.py @@ -0,0 +1,184 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Tests for aou_forwarding_to_lobster.""" + +import json +import tempfile +import unittest + +import yaml + +from aou_forwarding_to_lobster import ( + create_lobster_output, + filter_forwarded_aous, + load_lobster_items, + parse_forwarding_yaml, +) + + +class TestParseForwardingYaml(unittest.TestCase): + """Tests for parse_forwarding_yaml.""" + + def _write_yaml(self, data: dict) -> str: + f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) + yaml.dump(data, f) + f.close() + return f.name + + def test_valid_yaml(self) -> None: + path = self._write_yaml( + { + "forwarded_aous": [ + {"aou_id": "Pkg.AoU1", "justification": "reason"}, + ] + } + ) + result = parse_forwarding_yaml(path) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["aou_id"], "Pkg.AoU1") + self.assertEqual(result[0]["justification"], "reason") + + def test_missing_forwarded_aous_key(self) -> None: + path = self._write_yaml({"wrong_key": []}) + with self.assertRaises(SystemExit): + parse_forwarding_yaml(path) + + def test_missing_aou_id(self) -> None: + path = self._write_yaml({"forwarded_aous": [{"justification": "r"}]}) + with self.assertRaises(SystemExit): + parse_forwarding_yaml(path) + + def test_missing_justification(self) -> None: + path = self._write_yaml({"forwarded_aous": [{"aou_id": "Foo.Bar"}]}) + with self.assertRaises(SystemExit): + parse_forwarding_yaml(path) + + def test_multiple_entries(self) -> None: + path = self._write_yaml( + { + "forwarded_aous": [ + {"aou_id": "A.B", "justification": "r1"}, + {"aou_id": "C.D", "justification": "r2"}, + ] + } + ) + result = parse_forwarding_yaml(path) + self.assertEqual(len(result), 2) + + +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, + } + f = tempfile.NamedTemporaryFile(mode="w", suffix=".lobster", delete=False) + json.dump(data, f) + 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) + loaded = load_lobster_items([path]) + self.assertEqual(len(loaded), 2) + self.assertEqual(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"}]) + loaded = load_lobster_items([path1, path2]) + self.assertEqual(len(loaded), 2) + + def test_empty_data(self) -> None: + path = self._write_lobster([]) + loaded = load_lobster_items([path]) + self.assertEqual(loaded, []) + + +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"}, + ] + 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") + + 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"}, + ] + entries = [ + {"aou_id": "A.B", "justification": "r1"}, + {"aou_id": "E.F", "justification": "r2"}, + ] + filtered = filter_forwarded_aous(entries, items) + self.assertEqual(len(filtered), 2) + + def test_nonexistent_aou_id_raises(self) -> None: + items = [{"tag": "req Pkg.AoU1", "name": "AoU1"}] + entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] + with self.assertRaises(SystemExit): + filter_forwarded_aous(entries, items) + + 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"}, + ] + 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") + + def test_versioned_tag_matches_full_id(self) -> None: + """Full versioned ID should also work.""" + items = [{"tag": "req Pkg.AoU1@2", "name": "AoU1"}] + entries = [{"aou_id": "Pkg.AoU1@2", "justification": "reason"}] + filtered = filter_forwarded_aous(entries, items) + self.assertEqual(len(filtered), 1) + + +class TestCreateLobsterOutput(unittest.TestCase): + """Tests for create_lobster_output.""" + + 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_empty_items(self) -> None: + output = create_lobster_output([]) + self.assertEqual(output["data"], []) + + +if __name__ == "__main__": + unittest.main()