diff --git a/.github/skills/rules-score/SKILL.md b/.github/skills/rules-score/SKILL.md index f164647c..443dac05 100644 --- a/.github/skills/rules-score/SKILL.md +++ b/.github/skills/rules-score/SKILL.md @@ -59,7 +59,7 @@ with a traceability report. | Architectural Design | `architectural_design` | score-architecture | | Units & Components | `unit`, `unit_design`, `component` | score-architecture | | Tests & Coverage | `tests` attr, `test_case_coverage_lock` | score-testing | -| Dependability Analysis | `safety_analysis`, `dependability_analysis` | score-safety-analysis | +| Dependability Analysis | `failure_modes`, `fault_trees`, `safety_analysis`, `dependability_analysis` | score-safety-analysis | | SEooC assembly | `dependable_element` | this skill | ### Hierarchy @@ -151,7 +151,6 @@ component( dependable_element( name = "my_element", architectural_design = [":my_arch"], - assumptions_of_use = [], components = [":MyComponent"], dependability_analysis = [], integrity_level = "B", @@ -182,11 +181,14 @@ FMEA, cross-module `deps`, and test-case coverage — see 3. **Implementation & tests** → back each `unit` with a `cc_library` + `cc_test`; annotate tests with `lobster-tracing` + Given-When-Then; add `test_case_coverage_lock` on components. *(score-testing)* -4. **Safety analysis** → add `safety_analysis` (FailureMode + SafetyMeasure + FTA) and wrap it in a - `dependability_analysis`. *(score-safety-analysis)* +4. **Safety analysis** → add `failure_modes`, `fault_trees` (FTA, `deps = [failure_modes]`) and + `safety_analysis` (`safety_measures`: Mitigation files + AoU / `component_requirements` + targets) and wrap it in a `dependability_analysis`. AoU targets and + `component_requirements` list `fault_trees` in `deps`; AoU targets passed as `safety_measures` + are the element's own AoUs (derived, deduplicated, forwarded to dependees). *(score-safety-analysis)* 5. **Assemble** → allocate `CompReq` to `component(requirements=…)` and `FeatReq` to `dependable_element(requirements=…)`; wire `architectural_design`, `components`, - `assumptions_of_use`, `dependability_analysis`, `glossary`, and `integrity_level`. + `dependability_analysis`, `glossary`, and `integrity_level`. --- diff --git a/.github/skills/score-architecture/SKILL.md b/.github/skills/score-architecture/SKILL.md index 384d06d8..4387cd79 100644 --- a/.github/skills/score-architecture/SKILL.md +++ b/.github/skills/score-architecture/SKILL.md @@ -531,7 +531,6 @@ dependable_element( name = "safety_software_seooc_example", architectural_design = ["//design:sample_seooc_design"], requirements = ["//docs/requirements:feature_requirements"], # FeatReq targets - assumptions_of_use = ["//docs:sample_aous"], dependability_analysis = [":sample_dependability_analysis"], components = [":component_example"], tests = [], diff --git a/.github/skills/score-requirements/SKILL.md b/.github/skills/score-requirements/SKILL.md index c7b00b20..3c01dec6 100644 --- a/.github/skills/score-requirements/SKILL.md +++ b/.github/skills/score-requirements/SKILL.md @@ -243,10 +243,14 @@ component-internal requirements with no feature-level parent. ### Assumptions of Use (AoU) -`AoU` extends `RequirementSafety` (not `SafetyMeasure`) and captures conditions the integrating -project must satisfy, via its own independent, optional `root_causes` field. -The `assumptions_of_use` rule accepts raw `.trlc` **or** `.rst` files carrying `aou_req` -directives (converted to TRLC automatically). +`AoU` extends `SafetyMeasure` and captures conditions the integrating project must satisfy. +`root_causes` is mandatory: every AoU references the fault-tree root causes it addresses +(`.`), so the `assumptions_of_use` target lists the `fault_trees` target +in `deps`. The `assumptions_of_use` rule accepts raw `.trlc` **or** `.rst` files carrying `aou_req` +directives (converted to TRLC automatically; RST directives take `:root_causes: ., ...`). +Pass the target as `safety_measures` to `safety_analysis`: that makes it an own AoU of the +`dependable_element` (there is no AoU attribute on the element). Forwarding YAML +entries name received AoUs with their version (`Package.Name@1`). --- diff --git a/.github/skills/score-safety-analysis/SKILL.md b/.github/skills/score-safety-analysis/SKILL.md index 4faa0be8..5de688ab 100644 --- a/.github/skills/score-safety-analysis/SKILL.md +++ b/.github/skills/score-safety-analysis/SKILL.md @@ -32,11 +32,11 @@ argument-hint: "interface or component name to analyse" score//dependability/ ├── safety_analysis/ │ ├── failure_modes.trlc # FailureMode records (one per unique root-cause cluster) -│ ├── safetymeasures.trlc # Mitigation / AoU / CompReq records +│ ├── safetymeasures.trlc # Mitigation records │ ├── fta_.puml # One FTA diagram per FailureMode -│ └── BUILD # safety_analysis() rule — must list all .puml in fta_files filegroup +│ └── BUILD # failure_modes() + fault_trees() + safety_analysis() rules — must list all .puml in fta_files filegroup ├── assumed_system/ -│ └── aous.trlc # AoU records (caller obligations) +│ └── aous.trlc # AoU records (caller obligations, each with root_causes) └── requirements/ └── component_requirements.trlc ``` @@ -93,7 +93,7 @@ until each `$RootCause` is something you can place a measure on. |------|--------------|------------| | `Mitigation` | the SEooC itself provides a dedicated safety measure (with a mandatory `justification`) | `root_causes` | | `CompReq` | a normal, implemented-and-tested component requirement already closes it | `derived_from` | -| `AoU` | can only be guaranteed by the **integrator/caller** | `root_causes` (optional) | +| `AoU` | can only be guaranteed by the **integrator/caller** | `root_causes` (mandatory) | Use an `AoU` to **push an obligation outward** when the SEooC cannot close a cause itself; it must be forwarded to the integrating project. Record *why* a measure is sufficient (AND-gate argument, @@ -159,7 +159,7 @@ $RootCause("", "", "OG1") | `$TransferInGate(name, alias, connection)` | Link to sub-tree | parent alias | **Rules:** -- `$RootCause` alias is a plain identifier; the `puml_cli` FTA parser auto-generates a `fta_events.trlc` stub (imported as `_fta`) containing a `RootCause` record named after that alias (its `failure_modes` lists the `FailureMode`s of the tree's `$FailureMode` root) — `Mitigation`/`CompReq`/`AoU` records reference it explicitly (e.g. `root_causes = [sample_safety_analysis_fta.JustBadLuck]`); there is no implicit name-matching. +- `$RootCause` alias is a plain identifier; the `puml_cli` FTA parser auto-generates a `fta_events.trlc` stub (imported as ``, default derived from the target label) containing a `RootCause` record named after that alias (its `failure_modes` lists the `FailureMode`s of the tree's `$FailureMode` root) — `Mitigation`/`CompReq`/`AoU` records reference it explicitly (e.g. `root_causes = [sample_safety_analysis_fta.JustBadLuck]`); there is no implicit name-matching. - Build top-down in the file: `$FailureMode` first, then gates, then `$RootCause` leaves. - The same `$RootCause` alias may appear in multiple FTAs (shared root cause). - `$OrGate` is the default for independent root causes; use `$AndGate` only when all causes must co-occur. @@ -182,13 +182,16 @@ ScoreReq.Mitigation { Other ways to close a root cause: - `ScoreReq.CompReq` — closes it via `derived_from`, referencing the `RootCause` stub alongside any `FeatReq`/`AssumedSystemReq` it also derives from. An `AoU` in `derived_from` is only for an AoU **received** from another dependable element; the element's own AoUs are never a `derived_from` source -- `ScoreReq.AoU` — own assumption the caller must satisfy; does **not** extend `SafetyMeasure` and has its own independent, optional `root_causes` field (no `justification` required) +- `ScoreReq.AoU` — own assumption the caller must satisfy; extends `SafetyMeasure`, so `root_causes` is **mandatory** (no `justification` required) -**Rule:** every `$RootCause` alias must be referenced by at least one `Mitigation.root_causes`, `CompReq.derived_from`, or `AoU.root_causes`. This is checked by lobster in the traceability report of the enclosing `dependable_element` (Root Causes are covered via the Safety Measures level **or** the Component Requirements level; uncovered ones are reported as `missing reference to Component Requirements or Safety Measures`, and fail `bazel test ` with `maturity = "release"`). Only files passed as `safety_analysis.safetymeasures` form the Safety Measures level, so an AoU file with `root_causes` must be listed there **and** the `assumptions_of_use` target must list the `safety_analysis` target in `deps` (the generated `` package resolves only through it). Every AoU in a `safetymeasures` file must reference a root cause, otherwise it is reported as `missing up reference`; keep AoUs without `root_causes` in a separate file passed only to `assumptions_of_use`: +**Rule:** every `$RootCause` alias must be referenced by at least one `Mitigation.root_causes`, `CompReq.derived_from`, or `AoU.root_causes`. This is checked by lobster in the traceability report of the enclosing `dependable_element` (Root Causes are covered via the Safety Measures level **or** the Component Requirements level; uncovered ones are reported as `missing reference to Component Requirements or Safety Measures`, and fail `bazel test ` with `maturity = "release"`). The safety analysis is three rules: `failure_modes` (the `FailureMode` records), `fault_trees` (`srcs` = FTA diagrams, `deps` = the `failure_modes` targets; generates the `` root causes) and `safety_analysis` (`failure_modes`, `fault_trees` and `safety_measures`: `Mitigation` `.trlc` files plus `assumptions_of_use` / `component_requirements` targets; only their own records are rendered, targets in their `deps` just resolve references; raw `.trlc` files may only hold `Mitigation` records, and every failure mode a root cause references must be in `failure_modes`). Any target whose records reference `.` (`assumptions_of_use`, `component_requirements`) must list the `fault_trees` target in `deps`. Every `assumptions_of_use` target passed as `safety_measures` is an own AoU of the enclosing `dependable_element` (derived, deduplicated by label, forwarded to dependees); one no `safety_analysis` lists is neither rendered nor forwarded: ```python -safety_analysis(name = "sa", safetymeasures = ["aou.trlc"], ...) -assumptions_of_use(name = "aous", srcs = ["aou.trlc"], deps = [":sa"]) +failure_modes(name = "fm", srcs = ["failure_modes.trlc"]) +fault_trees(name = "ft", srcs = [":fta_files"], deps = [":fm"]) +assumptions_of_use(name = "aous", srcs = ["aous.trlc"], deps = [":ft"]) +safety_analysis(name = "sa", arch_design = ":arch", failure_modes = [":fm"], fault_trees = ":ft", + safety_measures = [":aous", "mitigations.trlc"]) ``` ## Step 4 — Update BUILD @@ -228,7 +231,8 @@ $RootCause alias → Mitigation.root_causes / CompReq.derived_from / AoU.r | `$FailureMode` fm1..fm8 argument does not match any TRLC record | Ensure `.` is spelled identically in both places | | A root cause (`$RootCause`) is not referenced by any `Mitigation`/`CompReq`/`AoU` | Add an explicit reference to the generated `.` `RootCause` stub | | New `.puml` not in BUILD `fta_files` | Add the file path to the `srcs` list | -| AoU added to `safetymeasures.trlc` | AoUs belong in `aous.trlc`; `AoU` does not extend `SafetyMeasure`, it has its own independent `root_causes` field. If the AoU closes a root cause, pass `aous.trlc` as `safety_analysis.safetymeasures` (coverage) and add the `safety_analysis` target to `assumptions_of_use.deps` | +| AoU added to `safetymeasures.trlc` | AoUs belong in `aous.trlc` and are passed to `safety_analysis` via `safety_measures` (the `assumptions_of_use` target), not inside the `Mitigation` `.trlc` file (the assembler rejects non-`Mitigation` records there). Each AoU needs `root_causes`. | | `CompReq.derived_from` references an own `AoU` | Only AoUs received from another dependable element belong there; own AoUs close root causes via `AoU.root_causes` | -| Unknown symbol `` when parsing `aous.trlc` | Add the `safety_analysis` target to the `assumptions_of_use` `deps` | +| Unknown symbol `` when parsing `aous.trlc` or component requirements | Add the `fault_trees` target to the `deps` of the `assumptions_of_use` / `component_requirements` | +| `safety_analysis` fails: root cause references a failure mode not in `failure_modes` | Add the `failure_modes` target holding it to `safety_analysis.failure_modes` | | Wrong RSL used for trlc validation | Always pass the tooling RSL as the first directory argument | diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 9b08f4ec..7a3b4832 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -114,6 +114,16 @@ py_binary( ], ) +# Merges lobster files, collapsing the same item arriving via several paths +py_binary( + name = "merge_lobster_items", + srcs = ["src/merge_lobster_items.py"], + imports = ["src"], + main = "src/merge_lobster_items.py", + visibility = ["//visibility:public"], + deps = ["@lobster//lobster/common"], +) + # HTML merge tool py_library( name = "sphinx_html_merge_lib", diff --git a/bazel/rules/rules_score/README.md b/bazel/rules/rules_score/README.md index 5af1d8d7..02be8822 100644 --- a/bazel/rules/rules_score/README.md +++ b/bazel/rules/rules_score/README.md @@ -28,7 +28,9 @@ for safety related automotive software. | `architectural_design` | `ArchitecturalDesignInfo` | | `unit` | `UnitInfo`, `CertifiedScope` | | `component` | `ComponentInfo` | -| `safety_analysis` | `AnalysisInfo` | +| `failure_modes` | `FailureModesInfo`, `TrlcProviderInfo` | +| `fault_trees` | `FaultTreesInfo`, `TrlcProviderInfo` | +| `safety_analysis` | `AnalysisInfo`, `SafetyAnalysisProviderInfo` | | `glossary` | `SphinxSourcesInfo` | | `dependability_analysis` | `DependabilityAnalysisInfo` | | `dependable_element` | HTML documentation zip (Sphinx) | @@ -71,9 +73,11 @@ assumptions_of_use( ``` **`bazel build`** — renders the AoU TRLC/RST sources and exposes their -LOBSTER traceability file via `AssumptionsOfUseInfo.aou_lobster`. Tracing to -feature/assumed-system requirements is established at the `dependable_element` -level (via its own `requirements` attribute), not here. +LOBSTER traceability file via `AssumptionsOfUseInfo.aou_lobster`. Every `AoU` +references the fault-tree root cause(s) it addresses (`root_causes`, mandatory); +list the `fault_trees` target in `deps` so `.` resolves. +Tracing to feature/assumed-system requirements is established at the +`dependable_element` level (via its own `requirements` attribute), not here. --- @@ -137,23 +141,53 @@ and collects requirement + architecture + test lobster sources. --- -## `safety_analysis` +## `failure_modes` / `fault_trees` / `safety_analysis` + +Three stages avoid a dependency cycle: the fault trees reference the failure +modes, the measures (AoUs, component requirements) reference root causes, and +the safety analysis lists the failure modes and the measures. ```starlark +failure_modes( + name = "my_failure_modes", + srcs = ["failure_modes.trlc"], +) + +fault_trees( + name = "my_fault_trees", + srcs = ["fta.puml"], + deps = [":my_failure_modes"], +) + +assumptions_of_use( + name = "my_aou", + srcs = ["aous.trlc"], + deps = [":my_fault_trees"], +) + safety_analysis( name = "my_safety_analysis", - failuremodes = [":failure_modes"], - root_causes = ["fta.puml"], - safetymeasures = [":safetymeasures"], arch_design = ":my_design", + failure_modes = [":my_failure_modes"], + fault_trees = ":my_fault_trees", + safety_measures = [":my_aou", "mitigations.trlc"], ) ``` -**`bazel build`** — generates `safety_analysis.rst` (failure modes with their root causes and measures, fault trees, safety measures), -runs `lobster-trlc` on TRLC inputs, and extracts FTA events from `.puml` -diagrams into `fta_events.trlc` (generated `RootCause` records, usable via the -target's `TrlcProviderInfo`). Build-only; traceability validation is done -by the wrapping `dependability_analysis` test. +**`bazel build` (`failure_modes`)** — runs `lobster-trlc` on the `FailureMode` +records. + +**`bazel build` (`fault_trees`)** — extracts FTA events from `.puml` diagrams +into `fta_events.trlc` (generated `RootCause` records, usable via the target's +`TrlcProviderInfo`) and runs `lobster-trlc` on the root causes. + +**`bazel build` (`safety_analysis`)** — generates `safety_analysis.rst` (failure +modes with their root causes and measures, fault trees) and runs `lobster-trlc` +on the measures. Raw `.trlc` files in `safety_measures` may only hold +`Mitigation` records; a root cause whose failure mode is not in `failure_modes` +fails the build. Build-only; traceability validation is done by the wrapping +`dependability_analysis` test. The `dependable_element` derives its own AoUs +from the `assumptions_of_use` targets passed as `safety_measures`. --- @@ -244,7 +278,6 @@ dependable_element( dependability_analysis = [":my_da"], components = [":my_component"], glossary = [":project_glossary"], - assumptions_of_use = [], tests = [], ) ``` 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 index e560e242..76a0baca 100644 --- a/bazel/rules/rules_score/docs/_assets/aou_forwarding_one_seooc.puml +++ b/bazel/rules/rules_score/docs/_assets/aou_forwarding_one_seooc.puml @@ -38,7 +38,7 @@ 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 "received AoUs\n(each dep's own_aou_lobster +\nchain_forwarded_lobster, 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 diff --git a/bazel/rules/rules_score/docs/_assets/rules_score_overview.puml b/bazel/rules/rules_score/docs/_assets/rules_score_overview.puml index ae3d48b9..1a7b54ac 100644 --- a/bazel/rules/rules_score/docs/_assets/rules_score_overview.puml +++ b/bazel/rules/rules_score/docs/_assets/rules_score_overview.puml @@ -45,6 +45,8 @@ component "unit" <> as unit component "component" <> as comp ' ── Safety Analysis rules ───────────────────────────────────────────────────── +component "failure_modes" <> as failure_modes +component "fault_trees" <> as fault_trees component "safety_analysis" <> as safety_analysis component "dependability_analysis" <> as da @@ -65,10 +67,15 @@ feat_req --> de : <> comp_req --> comp : <> comp_req --> aou : <> -safety_analysis --> da : <> +failure_modes --> fault_trees : <> +failure_modes --> safety_analysis : <> +fault_trees --> safety_analysis : <> +fault_trees --> aou : <> +fault_trees --> comp_req : <> +safety_analysis --> da : <>\n<> da --> de : <> -aou --> de : <> +aou --> safety_analysis : <> unit --> comp : <> comp --> de : <> diff --git a/bazel/rules/rules_score/docs/_assets/safety_analysis_doc_pipeline.puml b/bazel/rules/rules_score/docs/_assets/safety_analysis_doc_pipeline.puml index 6f6725a8..f6c13c95 100644 --- a/bazel/rules/rules_score/docs/_assets/safety_analysis_doc_pipeline.puml +++ b/bazel/rules/rules_score/docs/_assets/safety_analysis_doc_pipeline.puml @@ -14,8 +14,9 @@ @startuml safety_analysis_doc_pipeline ' Component view of the FMEA build: input artifacts (authored + tooling -' defaults) flow through three in-process tool actions of the ``safety_analysis`` rule into -' the generated files, the providers, and finally the Sphinx staging tree. +' defaults) flow through the in-process tool actions of the ``failure_modes``, +' ``fault_trees`` and ``safety_analysis`` rules into the generated files, the +' providers, and finally the Sphinx staging tree. skinparam linetype ortho skinparam ArrowFontSize 10 @@ -42,9 +43,10 @@ skinparam component { ' ── Input artifacts ────────────────────────────────────────────────────────── package "Authored by the component team" { - rectangle "failuremodes.trlc\n(FailureMode records)" <> as fm_trlc - rectangle "safetymeasures.trlc\n(Mitigation/AoU/CompReq)" <> as sm_trlc - rectangle "fta_*.puml\n(root_causes attr)" <> as fta_puml + rectangle "failure_modes srcs\n(FailureMode records)" <> as fm_trlc + rectangle "mitigations.trlc\n(Mitigation records)" <> as sm_trlc + rectangle "aous.trlc\n(AoU records, root_causes)" <> as aou_trlc + rectangle "fta_*.puml\n(fault_trees srcs)" <> as fta_puml } package "Tooling defaults (rules_score / ScoreReq)" { rectangle "ScoreReq *.rsl\n(spec attr)" <> as rsl @@ -53,11 +55,21 @@ package "Tooling defaults (rules_score / ScoreReq)" { rectangle "fm/safetymeasures lobster\nconfigs" <> as lcfg } -' ── safety_analysis rule: three tool actions ──────────────────────────────────────────── -package "safety_analysis rule actions" { +' ── failure_modes rule: one tool action ───────────────────────────────────── +package "failure_modes rule action" { + component "lobster-trlc" <> as ltrlc_fm +} + +' ── fault_trees rule: two tool actions ────────────────────────────────────── +package "fault_trees rule actions" { component "puml_cli (FTA mode)\n--fta-output-dir\n[crate: puml_fta]" <> as puml + component "lobster-trlc" <> as ltrlc_ft +} + +' ── safety_analysis rule: two tool actions ────────────────────────────────── +package "safety_analysis rule actions" { component "safety_analysis_assembler\n[lib: TRLCRST]" <> as asm - component "lobster-trlc x3" <> as ltrlc + component "lobster-trlc" <> as ltrlc } ' ── Generated files ────────────────────────────────────────────────────────── @@ -65,33 +77,45 @@ package "Generated files" { rectangle "fta_*.puml\n(authored, symlinked)" <> as puml_inl rectangle "fta_events.trlc\n(RootCause stubs)" <> as fta_events rectangle "safety_analysis.rst" <> as safety_analysis_rst - rectangle "failuremodes.lobster\nsafetymeasures.lobster\nfta_root_causes.lobster" <> as fmcm_lob + rectangle "failure_modes lobster" <> as fm_lob + rectangle "fta_root_causes.lobster" <> as ft_lob + rectangle "safetymeasures.lobster" <> as sm_lob } ' ── Providers ──────────────────────────────────────────────────────────────── +rectangle "TrlcProviderInfo + FaultTreesInfo\n────────────────\nfta_events.trlc, failure modes (deps)\nlobster_files, diagrams" <> as ftpi rectangle "SphinxSourcesInfo\n────────────────\nsrcs: safety_analysis.rst\ndeps: safety_analysis.rst\naux_srcs: fta_*.puml" <> as ssi rectangle "AnalysisInfo.lobster_files\n────────────────\nfailuremodes.lobster\nsafetymeasures.lobster\nfta_root_causes.lobster" <> as ai -rectangle "TrlcProviderInfo\n────────────────\nreqs: fta_events.trlc, failuremodes\n(safetymeasures excluded: they\nreference fta_events)" <> as trlcpi +rectangle "SafetyAnalysisProviderInfo\n────────────────\naou_targets (safety_measures)" <> as sapi ' ── Downstream rules + staging ─────────────────────────────────────────────── component "dependability_analysis" <> as da -component "dependable_element" <> as de -component "assumptions_of_use\n(deps = [safety_analysis])" <> as aou +component "dependable_element\n(own AoUs from aou_targets)" <> as de +component "assumptions_of_use\n(deps = [fault_trees])" <> as aou rectangle "dependability_analysis/\n dfa.rst <- toctree\n safety_analysis.rst <- toctree\n fta_*.puml (.. uml::)" <> as stage ' ── Edges: inputs -> tools ─────────────────────────────────────────────────── fta_puml --> puml : parse macro calls +fm_trlc --> ltrlc_fm +rsl --> ltrlc_fm +lcfg --> ltrlc_fm +fm_trlc --> ltrlc_ft : import context +fta_events --> ltrlc_ft +rsl --> ltrlc_ft +lcfg --> ltrlc_ft fta_events --> asm fta_puml --> asm : diagram names\n(one .. uml:: each) fm_trlc --> asm sm_trlc --> asm +aou_trlc --> asm : safety_measures rsl --> asm : import resolution tmpl --> asm -fm_trlc --> ltrlc sm_trlc --> ltrlc +aou_trlc --> ltrlc fta_events --> ltrlc +fm_trlc --> ltrlc rsl --> ltrlc lcfg --> ltrlc @@ -100,19 +124,25 @@ fta_puml --> puml_inl : symlinked beside safety_analysis.rst meta --> stage : on PlantUML include path puml --> fta_events asm --> safety_analysis_rst -ltrlc --> fmcm_lob +ltrlc_fm --> fm_lob +ltrlc_ft --> ft_lob +ltrlc --> sm_lob ' ── Edges: generated -> providers ──────────────────────────────────────────── safety_analysis_rst --> ssi puml_inl --> ssi -fmcm_lob --> ai -fta_events --> trlcpi -fm_trlc --> trlcpi +ft_lob --> ai +fm_lob --> ai +sm_lob --> ai +fta_events --> ftpi +fm_trlc --> ftpi +aou_trlc --> sapi ' ── Edges: providers -> downstream -> staging ──────────────────────────────── ssi --> da : merge SphinxSourcesInfo ai --> da : merge lobster_files -trlcpi --> aou : resolve AoU.root_causes\n(.) +sapi --> da : aou_targets +ftpi --> aou : resolve AoU.root_causes\n(.) da --> de de --> stage : symlink srcs + aux_srcs\n(aux not indexed in toctree) diff --git a/bazel/rules/rules_score/docs/_assets/safety_analysis_linkage.puml b/bazel/rules/rules_score/docs/_assets/safety_analysis_linkage.puml index 9e09c872..6c2f1ec6 100644 --- a/bazel/rules/rules_score/docs/_assets/safety_analysis_linkage.puml +++ b/bazel/rules/rules_score/docs/_assets/safety_analysis_linkage.puml @@ -62,7 +62,7 @@ RootCause "1..*" --> FailureMode : failure_modes ' ── Safety measures: every RootCause needs at least one ───────────────────── Mitigation "1..*" --> RootCause : root_causes\n(+ justification) -OwnAoU "0..*" --> RootCause : root_causes\n(preventive measure) +OwnAoU "1..*" --> RootCause : root_causes\n(preventive measure) CompReq "0..*" --> RootCause : derived_from\n(control measure) ' ── Requirement chain ─────────────────────────────────────────────────────── diff --git a/bazel/rules/rules_score/docs/_assets/seooc_flow.puml b/bazel/rules/rules_score/docs/_assets/seooc_flow.puml index cbeecc2b..4ac643d7 100644 --- a/bazel/rules/rules_score/docs/_assets/seooc_flow.puml +++ b/bazel/rules/rules_score/docs/_assets/seooc_flow.puml @@ -80,7 +80,7 @@ unit_r --> comp_r2 : components safety_analysis_r --> da_r : safety_analysis arch_r --> de_r : architectural_design -aou_r --> de_r : assumptions_of_use +aou_r --> safety_analysis_r : measures comp_r2 --> de_r : components da_r --> de_r : dependability_analysis diff --git a/bazel/rules/rules_score/docs/_assets/traceability_dataflow.puml b/bazel/rules/rules_score/docs/_assets/traceability_dataflow.puml index 2dbfe2a6..607797e7 100644 --- a/bazel/rules/rules_score/docs/_assets/traceability_dataflow.puml +++ b/bazel/rules/rules_score/docs/_assets/traceability_dataflow.puml @@ -33,7 +33,7 @@ skinparam rectangle { ' ── Sources ─────────────────────────────────────────────────────────────────── rectangle "Assumed System Req\nFeature Req\nComponent Req\n(.trlc)" <> as reqs_in rectangle "public_api.puml\n(architectural_design)" <> as api_in -rectangle "failuremodes.trlc\nsafetymeasures.trlc" <> as fmcm_in +rectangle "failuremodes.trlc\nmitigations.trlc\naous.trlc" <> as fmcm_in rectangle "fta.puml" <> as fta_in rectangle "Unit tests\n(cc_test / gtest)" <> as test_in diff --git a/bazel/rules/rules_score/docs/integration_guide.rst b/bazel/rules/rules_score/docs/integration_guide.rst index 7fb5b766..30811d37 100644 --- a/bazel/rules/rules_score/docs/integration_guide.rst +++ b/bazel/rules/rules_score/docs/integration_guide.rst @@ -189,8 +189,8 @@ Complete Example load("@score_tooling//bazel/rules/rules_score:rules_score.bzl", "architectural_design", "assumed_system_requirements", "assumptions_of_use", "component", "component_requirements", - "dependability_analysis", "dependable_element", - "feature_requirements", "safety_analysis", "unit") + "dependability_analysis", "dependable_element", "failure_modes", + "fault_trees", "feature_requirements", "safety_analysis", "unit") # Requirements assumed_system_requirements(name = "sys_req", srcs = ["docs/sys_req.trlc"]) @@ -199,7 +199,7 @@ Complete Example component_requirements(name = "reqs", srcs = ["docs/reqs.trlc"], deps = [":features"]) assumptions_of_use(name = "aous", srcs = ["docs/aous.trlc"], - requirements = [":features"]) + deps = [":my_fault_trees"]) # Architecture architectural_design(name = "arch", @@ -208,10 +208,14 @@ Complete Example public_api = ["docs/public_api.puml"]) # Safety analysis + failure_modes(name = "my_failure_modes", srcs = ["docs/failures.trlc"]) + fault_trees(name = "my_fault_trees", + srcs = ["docs/fta.puml"], + deps = [":my_failure_modes"]) safety_analysis(name = "my_safety_analysis", arch_design = ":arch", - safetymeasures = ["docs/safetymeasures.trlc"], - failuremodes = ["docs/failures.trlc"], - root_causes = ["docs/fta.puml"]) + failure_modes = [":my_failure_modes"], + fault_trees = ":my_fault_trees", + safety_measures = [":aous", "docs/mitigations.trlc"]) dependability_analysis(name = "analysis", safety_analysis = [":my_safety_analysis"]) # Implementation @@ -228,7 +232,6 @@ Complete Example dependable_element( name = "persistency_kvs", integrity_level = "B", - assumptions_of_use = [":aous"], requirements = [":reqs"], architectural_design = [":arch"], dependability_analysis = [":analysis"], diff --git a/bazel/rules/rules_score/docs/requirements/potential_errors.trlc b/bazel/rules/rules_score/docs/requirements/potential_errors.trlc index 22d4e592..36ce55ab 100644 --- a/bazel/rules/rules_score/docs/requirements/potential_errors.trlc +++ b/bazel/rules/rules_score/docs/requirements/potential_errors.trlc @@ -121,6 +121,17 @@ section "Potential Errors" { impact_type = ToolQualification.Impact_Type.Safety } + ToolQualification.PotentialError Conflicting_AoU_Definitions_Merged { + description = ''' + An assumption of use reaches a dependable element along + several paths with different content, for example in two + versions, and one definition silently replaces the other. + ''' + impacts = ["Integrator is shown an outdated or wrong assumption of use"] + 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 122f3c00..e3ac9d18 100644 --- a/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc +++ b/bazel/rules/rules_score/docs/requirements/tool_requirements.trlc @@ -82,8 +82,8 @@ section "Tool Requirements" { ToolQualification.ToolRequirement Render_All_Failure_Modes { description = ''' - The safety_analysis rule shall render every FailureMode record from the - input TRLC sources into the documentation output. + The safety_analysis rule shall render every FailureMode record of + its failure_modes into the documentation output. ''' mitigates = [Missing_Failure_Mode_In_Render] derived_from = [Render_Safety_Analyses] @@ -123,6 +123,22 @@ section "Tool Requirements" { } + section "RST Requirements" { + + ToolQualification.ToolRequirement Reject_Incomplete_RST_AoU { + description = ''' + The RST requirements converter shall exit with a non-zero + return code, name the offending assumption of use, and write + no output when an assumption of use directive has no + root_causes field or a root cause that is not of the form + .. + ''' + derived_from = [Author_Requirements_In_Rst_Or_Trlc] + satisfied_by = Tools.Bazel + } + + } + section "AoU Forwarding" { ToolQualification.ToolRequirement Forward_Own_AoUs_To_Dependees { @@ -143,7 +159,9 @@ section "Tool Requirements" { 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. + justification field and the version of the AoU it forwards, + so that a justification is reviewed again when the AoU + changes. ''' mitigates = [AoU_Silently_Dropped] derived_from = [Forward_AoU_To_Dependees] @@ -153,8 +171,9 @@ section "Tool Requirements" { 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 + when the forwarding YAML references an AoU identifier, or an + AoU version, that does not exist in the set of received AoUs, + or lists an AoU identifier more than once, preventing silent misconfiguration. ''' mitigates = [Invalid_Forwarding_YAML_Accepted] @@ -162,6 +181,45 @@ section "Tool Requirements" { satisfied_by = Tools.Bazel } + ToolQualification.ToolRequirement Derive_Own_AoUs_From_Safety_Analysis { + description = ''' + The dependable_element rule shall derive the assumptions of + use it exposes to its dependees from the assumptions_of_use + targets that the safety_analysis targets of its + dependability_analysis consume as safety_measures, and shall provide + no other source of own assumptions of use. An assumption of + use shall be exposed once, however many safety analyses + consume it. + ''' + mitigates = [AoU_Silently_Dropped] + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + + ToolQualification.ToolRequirement Receive_Duplicate_AoU_Once { + description = ''' + The dependable_element rule shall list an assumption of use in + its "Received AoUs" tier once, when it reaches the element + along several paths with identical content, such as through a + direct dependency and through a dependency that forwards it. + ''' + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + + ToolQualification.ToolRequirement Reject_Conflicting_AoU_Definitions { + description = ''' + The dependable_element rule shall fail the build when an + assumption of use reaches the element along several paths + with different content, such as in different versions, and + the error message shall name the assumption of use and the + differing fields. + ''' + mitigates = [Conflicting_AoU_Definitions_Merged] + derived_from = [Forward_AoU_To_Dependees] + satisfied_by = Tools.Bazel + } + ToolQualification.ToolRequirement Cover_Received_AoU_Via_Component_Requirement { description = ''' The CompReq requirement type's derived_from field diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index 5694f7d3..0bff5ec3 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -262,7 +262,7 @@ testable within that component. * - ``deps`` - label list - no - - ``feature_requirements`` or ``assumed_system_requirements`` targets for ``derived_from`` resolution (default ``[]``) + - ``feature_requirements`` or ``assumed_system_requirements`` targets for ``derived_from`` resolution. Also list the ``assumptions_of_use`` target of a *received* AoU and the ``fault_trees`` target of referenced ``.`` records (default ``[]``) * - ``image_srcs`` - label list - no @@ -311,11 +311,11 @@ Conditions that the *integrating project* must fulfil when using this SEooC. * - ``srcs`` - label list - yes - - ``.trlc`` files (containing ``AoU`` records), ``.rst`` files, or labels to existing ``TrlcProviderInfo``-providing targets + - ``.trlc`` files (containing ``AoU`` records), ``.rst`` files, or labels to existing ``TrlcProviderInfo``-providing targets. Every ``aou_req`` directive of an ``.rst`` source needs ``:root_causes: ., ...``; a missing or unqualified value fails the conversion * - ``deps`` - label list - no - - Other requirement targets (``TrlcProviderInfo``) needed for cross-reference parsing (default ``[]``). List the ``safety_analysis`` target when an ``AoU`` references its generated ``.`` records. + - Other requirement targets (``TrlcProviderInfo``) needed for cross-reference parsing (default ``[]``). List the ``fault_trees`` target: every ``AoU`` references the generated ``.`` records it addresses (``root_causes``, mandatory). Other ``assumptions_of_use`` targets are rejected here, as their AoUs would be extracted as own AoUs. * - ``ref_package`` - string - no @@ -529,22 +529,20 @@ implementation. **Generated targets:** ```` (no standalone test; diagrams are consumed by the parent ``unit``) -.. _rule-safety-analysis: +.. _rule-failure-modes: -safety_analysis -~~~~~~~~~~~~~~~ +failure_modes +~~~~~~~~~~~~~ -Bundles failure modes, safety measures, and FTA diagrams into a single FMEA -documentation target. +Stage 1 of the safety analysis: the ``FailureMode`` records. The +``fault_trees`` diagrams reference them as tree top events (list the target in +``fault_trees.deps``); ``safety_analysis`` renders and traces them. .. code-block:: python - safety_analysis( - name = "my_safety_analysis", - failuremodes = ["docs/failuremodes.trlc"], - root_causes = ["docs/fta.puml"], - safetymeasures = ["docs/safetymeasures.trlc"], - arch_design = ":arch", + failure_modes( + name = "my_failure_modes", + srcs = ["docs/failure_modes.trlc"], ) .. list-table:: @@ -559,30 +557,131 @@ documentation target. - string - yes - Target name - * - ``failuremodes`` + * - ``srcs`` - label list - - no - - ``.trlc`` files containing ``FailureMode`` records (default ``[]``) - * - ``safetymeasures`` + - yes + - ``.trlc`` files containing ``FailureMode`` records + * - ``deps`` - label list - no - - ``.trlc`` files containing ``Mitigation``/``AoU``/``CompReq`` records (default ``[]``) - * - ``root_causes`` + - Targets providing ``TrlcProviderInfo`` needed to parse the records (default ``[]``) + * - ``lobster_config`` + - label + - no + - lobster-trlc configuration for ``FailureMode`` extraction (default: the S-CORE failure mode config) + * - ``visibility`` + - — + - no + - Bazel visibility + +**Generated targets:** ````, ``_test`` (TRLC validation) + +.. _rule-fault-trees: + +fault_trees +~~~~~~~~~~~ + +Stage 2 of the safety analysis: the fault-tree root causes. Requirement +targets (``assumptions_of_use``, ``component_requirements``) list it in +``deps`` to reference the generated ``.`` records, +which breaks the dependency cycle between measures and analysis. + +.. code-block:: python + + fault_trees( + name = "my_fault_trees", + srcs = ["docs/fta.puml"], + deps = [":my_failure_modes"], + ) + +.. list-table:: + :header-rows: 1 + :widths: 18 12 10 60 + + * - Attribute + - Type + - Required + - Description + * - ``name`` + - string + - yes + - Target name + * - ``srcs`` - label list - no - FTA PlantUML diagram files (``.puml`` / ``.plantuml``) (default ``[]``) + * - ``deps`` + - label list + - no + - ``failure_modes`` targets holding the failure modes the diagrams reference (default ``[]``) * - ``fta_package`` - string - no - TRLC package name for the generated ``fta_events.trlc`` stub (the - ``RootCause`` records derived from ``root_causes``). The target - exports it (plus ``failuremodes``) as ``TrlcProviderInfo``, so other - targets such as ``assumptions_of_use`` can list it in ``deps``. - Defaults to a sanitized form of ``name`` with a ``_fta`` suffix. - * - ``arch_design`` + ``RootCause`` records derived from ``srcs``). The target + exports it (with the failure modes as context) as ``TrlcProviderInfo``. + Defaults to a sanitized form of the target's package and name with a + ``_fta`` suffix (e.g. ``pkg_sub__name_fta``), so equally named targets + of different packages do not collide. The repository is not part of + it; set ``fta_package`` explicitly when equally named targets of + different repositories meet in one build, when package names differ + only in case or underscores (TRLC rejects those as too similar, e.g. + ``pkg/fta:y`` and ``pkg:fta_y``), and to get a stable, readable + name for ``.`` references. + * - ``visibility`` + - — + - no + - Bazel visibility + +**Generated targets:** ```` (generated ``fta_events.trlc`` and the root-cause lobster file; no standalone test) + +.. _rule-safety-analysis: + +safety_analysis +~~~~~~~~~~~~~~~ + +Stage 3 of the safety analysis: renders the FMEA page from the +``failure_modes``, a ``fault_trees`` target and the measures that address the +root causes. + +.. code-block:: python + + safety_analysis( + name = "my_safety_analysis", + arch_design = ":arch", + failure_modes = [":my_failure_modes"], + fault_trees = ":my_fault_trees", + safety_measures = [":aous", "docs/mitigations.trlc"], + ) + +.. list-table:: + :header-rows: 1 + :widths: 18 12 10 60 + + * - Attribute + - Type + - Required + - Description + * - ``name`` + - string + - yes + - Target name + * - ``failure_modes`` + - label list + - no + - ``failure_modes`` targets whose records are rendered and traced (default ``[]``). Every failure mode a root cause references must be listed; the assembler fails otherwise. + * - ``fault_trees`` - label + - yes + - ``fault_trees`` target providing the root causes + * - ``safety_measures`` + - label list - no - - ``architectural_design`` target for interface traceability (default ``None``) + - Measures addressing the root causes (default ``[]``): ``assumptions_of_use`` / ``component_requirements`` targets and ``.trlc`` files containing ``Mitigation`` records (any other record type in such a file is rejected). Only the records of these entries are rendered and traced; the targets in their ``deps`` are used to resolve references only. The ``assumptions_of_use`` targets listed here are the *own* AoUs of the ``dependable_element`` whose ``dependability_analysis`` includes this target: they are rendered in the safety analysis, forwarded to dependees, and shown once however many safety analyses list them. An ``assumptions_of_use`` target that no ``safety_analysis`` lists here is neither rendered nor forwarded. + * - ``arch_design`` + - label + - yes + - ``architectural_design`` target * - ``visibility`` - — - no @@ -621,7 +720,7 @@ Running ``bazel test`` validates the full FMEA traceability chain. * - ``safety_analysis`` - label list - no - - ``safety_analysis`` targets to include in this analysis (default ``[]``) + - ``safety_analysis`` targets to include in this analysis (default ``[]``). A ``dependable_element`` stages the pages of each ``dependability_analysis`` under the analysis name as ``safety_analysis.rst`` / ``fta.puml``, so list one ``safety_analysis`` per ``dependability_analysis`` and use several ``dependability_analysis`` targets for several safety analyses. * - ``arch_design`` - label - no @@ -775,7 +874,6 @@ and scope checks at build/test time. dependable_element( name = "my_seooc", integrity_level = "B", - assumptions_of_use = [":aous"], requirements = [":features"], architectural_design = [":arch"], dependability_analysis = [":analysis"], @@ -799,10 +897,6 @@ and scope checks at build/test time. - string - yes - ``"A"``, ``"B"``, ``"C"``, or ``"D"`` (D is highest: D > C > B > A) - * - ``assumptions_of_use`` - - label list - - yes - - ``assumptions_of_use`` targets * - ``requirements`` - label list - yes @@ -838,12 +932,12 @@ and scope checks at build/test time. * - ``aou_forwarding`` - label - no - - Optional YAML file that selects which *received* AoUs to chain-forward to elements that depend on this one. Own AoUs (declared in ``assumptions_of_use``) are always forwarded automatically — no file needed. Schema: + - Optional YAML file that selects which *received* AoUs to chain-forward to elements that depend on this one. Own AoUs (the ``assumptions_of_use`` targets consumed as ``safety_measures`` by the element's safety analyses) are always forwarded automatically — no file needed. Every entry names the AoU with its version (``Package.AOU_ID@``), so a justification is reviewed again when the AoU changes; unknown IDs, mismatching versions and duplicate entries fail the build. An AoU that reaches the element along several paths is received once; differing definitions fail the build. Schema: .. code-block:: yaml forwarded_aous: - - aou_id: "Package.AOU_ID" + - aou_id: "Package.AOU_ID@1" justification: > Reason why this element cannot handle the AoU itself and must forward it to its own integrators. diff --git a/bazel/rules/rules_score/docs/tooling_architecture.rst b/bazel/rules/rules_score/docs/tooling_architecture.rst index 1c81aa9c..93482820 100644 --- a/bazel/rules/rules_score/docs/tooling_architecture.rst +++ b/bazel/rules/rules_score/docs/tooling_architecture.rst @@ -28,11 +28,13 @@ Three layers build wires layers 2 and 3 automatically. #. **Macros / rules** (Starlark, ``private/*.bzl``) — the public work-product - declarations (``feature_requirements``, ``architectural_design``, ``safety_analysis``, + declarations (``feature_requirements``, ``architectural_design``, ``failure_modes``, + ``fault_trees``, ``safety_analysis``, ``unit``, ``component``, ``dependability_analysis``, ``dependable_element``, …). Each one declares actions and emits **providers**. #. **Providers** (``providers.bzl``) — the typed contracts that carry data between rules (e.g. ``ArchitecturalDesignInfo``, ``AnalysisInfo``, + ``FailureModesInfo``, ``FaultTreesInfo``, ``SphinxSourcesInfo``). The provider graph is the "API" between rules; see :doc:`overview` for the provider-flow diagram. #. **Tools** — the executables each action runs. Some are vendored third-party @@ -79,7 +81,7 @@ are rendered under :doc:`tool_reference/index`. - ``@trlc//tools/trlc_rst:trlc_rst`` + TRLC parser; ``trlc_requirements_test`` - ``feature_requirements``, ``component_requirements``, - ``assumed_system_requirements``, ``safety_analysis`` + ``assumed_system_requirements``, ``failure_modes``, ``safety_analysis`` - Parses and type-checks requirement / FMEA records against the ``.rsl`` metamodel and renders them to ``.rst``. ``trlc_rst`` also ships a reusable ``TRLCRST`` library that ``safety_analysis_assembler`` links directly to @@ -102,10 +104,10 @@ are rendered under :doc:`tool_reference/index`. * - **puml_cli (FTA mode)** - ``//plantuml/parser/puml_cli`` ``--fta-output-dir`` (Rust; FTA model in the ``puml_fta`` crate) - - ``safety_analysis`` + - ``fault_trees`` - Analysis only: parses the ``$FailureMode`` / ``$RootCause`` / gate macro calls of each root-cause FTA diagram and emits ``fta_events.trlc``, a - generated stub file (imported as the ``_fta`` package) containing + generated stub file (imported as the ```` package) containing one ``RootCause`` record per reachable ``$RootCause`` alias; each record lists the failure modes of the ``$FailureMode`` root it hangs under. The authored @@ -130,7 +132,7 @@ are rendered under :doc:`tool_reference/index`. - ``@lobster//`` : ``lobster-trlc``, ``lobster-report``, ``lobster-ci-report``, ``lobster-html-report``, ``gtest_report``, ``lobster-rst-report`` - - ``*_requirements``, ``safety_analysis``, ``unit``, ``dependability_analysis``, + - ``*_requirements``, ``failure_modes``, ``fault_trees``, ``safety_analysis``, ``unit``, ``dependability_analysis``, ``dependable_element`` - The traceability backbone. ``lobster-trlc`` extracts ``.lobster`` items from TRLC; ``gtest_report`` turns test results into ``.lobster``; @@ -200,7 +202,7 @@ feed that pipeline: * **Requirements** (``.trlc``) → ``lobster-trlc`` → ``requirements.lobster``. * **Public API diagrams** (``public_api.puml``) → PlantUML parser → ``public_api.lobster`` (enables failure-mode-to-interface tracing). -* **FMEA** (``failuremodes.trlc`` / ``safetymeasures.trlc``) → ``lobster-trlc``; +* **FMEA** (``failure_modes`` / ``mitigations.trlc``) → ``lobster-trlc``; **FTA** (``fta.puml``) → ``puml_cli`` (FTA mode) → ``fta_events.trlc`` → ``lobster-trlc`` → ``fta_root_causes.lobster``. * **Unit tests** (gtest) → ``gtest_report`` → ``.lobster``. @@ -325,8 +327,8 @@ Safety analysis document pipeline The component diagram below shows how the FMEA **input artifacts** — authored ``.trlc`` records and ``fta_*.puml`` diagrams plus the tooling defaults (``ScoreReq`` ``.rsl`` spec, ``fta_metamodel.puml``, ``safety_analysis.template.rst`` and -the lobster configs) — flow through the three in-process tool actions of the -``safety_analysis`` rule into the generated files, the providers, and finally the Sphinx +the lobster configs) — flow through the in-process tool actions of the +``fault_trees`` and ``safety_analysis`` rules (with the ``failure_modes`` target as input) into the generated files, the providers, and finally the Sphinx staging tree. Blue boxes are authored sources, light-blue are tooling defaults, green components are the tool actions, orange boxes are generated files, yellow boxes are the provider payloads, and the purple box is the staging directory @@ -337,24 +339,37 @@ consumed by Sphinx. :alt: Safety analysis document pipeline :width: 100% -The ``safety_analysis`` rule drives three actions, all reading the input artifacts above: +The analysis is split in three rules so the measures that reference root causes +(``assumptions_of_use``, ``component_requirements``) can depend on the generated +``RootCause`` records without a dependency cycle: ``failure_modes`` holds the +``FailureMode`` records, ``fault_trees`` (``deps = [failure_modes]``) produces the +root causes, the measures list it in ``deps``, and ``safety_analysis`` consumes the +failure modes, the fault trees and the measures (``safety_measures``). + +``failure_modes`` runs ``lobster-trlc`` over its records into +``.lobster``; it provides ``FailureModesInfo`` and ``TrlcProviderInfo``. + +The ``fault_trees`` rule drives two actions: #. **puml_cli (FTA mode)** parses each ``fta_*.puml`` directly (no rewriting) and writes ``fta_events.trlc`` (the generated ``RootCause`` stubs). The diagrams keep their ``!include fta_metamodel.puml``; the metamodel is on PlantUML's global include path (shipped in the docs toolchain runfiles), so it resolves at render time. +#. **lobster-trlc** turns the generated ``RootCause`` records into + ``fta_root_causes.lobster``. The ``failure_modes`` files travel as inputs, so the + ``import``s of ``fta_events.trlc`` resolve. + +The ``safety_analysis`` rule drives two actions: + #. **safety_analysis_assembler** consumes ``fta_events.trlc`` and parses the FailureMode / Mitigation / AoU / CompReq ``.trlc`` records (with the ``.rsl`` spec for import resolution) in a single in-process ``TRLCRST`` pass, expanding ``safety_analysis.template.rst`` into ``safety_analysis.rst``. -#. **lobster-trlc** (run three times) turns the FailureMode, Mitigation/AoU/CompReq, - and generated ``RootCause`` records - into ``failuremodes.lobster`` / ``safetymeasures.lobster`` / - ``fta_root_causes.lobster`` for the - traceability report. +#. **lobster-trlc** turns the Mitigation and AoU records into + ``safetymeasures.lobster``; a ``CompReq`` measure is traced at its own level. -``SphinxSourcesInfo`` carries three depsets: +``SphinxSourcesInfo`` of ``safety_analysis`` carries three depsets: - **srcs** — files that become top-level toctree entries in the enclosing document section. ``safety_analysis`` emits exactly one: ``safety_analysis.rst``. @@ -363,16 +378,24 @@ The ``safety_analysis`` rule drives three actions, all reading the input artifac (failure modes and control measures are rendered inline, not pulled in via ``.. include::``). - **aux_srcs** — files to symlink alongside ``srcs``/``deps`` but **not** added - to any toctree. ``safety_analysis`` uses this for the authored ``fta_*.puml`` diagrams, + to any toctree. ``safety_analysis`` uses this for the authored ``fta_*.puml`` diagrams + (staged next to ``safety_analysis.rst`` from the ``fault_trees`` target), which ``safety_analysis.rst`` references inline via ``.. uml::`` and which must therefore sit beside it in the staging tree without being indexed as documents. (The metamodel is not staged here — it resolves via PlantUML's global include path.) -The lobster outputs travel separately on ``AnalysisInfo.lobster_files`` -(``failuremodes.lobster``, ``safetymeasures.lobster``, -``fta_root_causes.lobster``) -into the ``dependability_analysis`` traceability report. +The lobster outputs travel separately on ``AnalysisInfo.lobster_files`` (one +``AnalysisInfo`` per ``safety_analysis``: ``failuremodes.lobster``, +``safetymeasures.lobster``, ``fta_root_causes.lobster``) +into the ``dependability_analysis`` traceability report. The AoU targets passed +as ``safety_measures`` travel on ``SafetyAnalysisProviderInfo.aou_targets``; the +``dependable_element`` derives its own AoUs from them (deduplicated by label), +forwards them to dependees, and merges them with the received AoUs. +``merge_lobster_items`` collapses an item defined by several lobster files +(the same AoU or measure reached via several safety analyses, dependability +analyses or dependencies): identical definitions collapse, conflicting +definitions fail the build. .. _hermetic-tool-path-resolution: 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 e4d9007c..dc9ef865 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 @@ -16,9 +16,9 @@ Assumptions of Use =================== Conditions that the *integrating project* must satisfy when using your SEooC. -The optional ``root_causes`` field lets an ``AoU`` close one or more FTA -``RootCause`` root causes directly, pushing the obligation to prevent them -out to the integrator. +Every own ``AoU`` references the FTA ``RootCause`` records it closes in its +mandatory ``root_causes`` field, pushing the obligation to prevent them out to +the integrator. 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`` @@ -32,26 +32,27 @@ however, declare that it implements a received AoU by referencing it from its ow package SampleType import ScoreReq + import sample_safety_analysis_fta ScoreReq.AoU SampleAoU { description = "It shall be made sure that shared memory segments are never created with the wrong name (ShmemCreatedWrongName)" safety = ScoreReq.Asil.B version = 1 + root_causes = [sample_safety_analysis_fta.ShmemCreatedWrongName] } .. code-block:: starlark - :caption: examples/seooc/docs/BUILD and examples/seooc/BUILD + :caption: examples/seooc/docs/BUILD assumptions_of_use( name = "sample_aous", srcs = ["aous.trlc"], + deps = ["//safety_analysis:sample_fault_trees"], ) - dependable_element( - name = "safety_software_seooc_example", - assumptions_of_use = ["//docs:sample_aous"], - ... - ) +The ``dependable_element`` has no AoU attribute: its own AoUs are the +``assumptions_of_use`` targets that the ``safety_analysis`` targets of its +``dependability_analysis`` list in ``safety_measures`` (see below). Preventing a root cause ----------------------- @@ -75,22 +76,37 @@ referencing the generated ``RootCause`` record in ``root_causes``: } The generated ```` package only exists as output of the -``safety_analysis`` target, so two attributes must name that target: +``fault_trees`` target, so the ``assumptions_of_use`` target lists it in +``deps``. The ``safety_analysis`` target lists the same ``assumptions_of_use`` +target in ``safety_measures``, which counts the AoU as a measure of the root cause: .. code-block:: starlark - safety_analysis( - name = "sample_safety_analysis", - safetymeasures = ["aou.trlc"], # counts the AoU as a measure of the root cause + fault_trees( + name = "sample_fault_trees", + fta_package = "sample_safety_analysis_fta", # ... ) assumptions_of_use( name = "sample_aous", srcs = ["aou.trlc"], - deps = [":sample_safety_analysis"], # resolves sample_safety_analysis_fta + deps = [":sample_fault_trees"], # resolves sample_safety_analysis_fta ) + safety_analysis( + name = "sample_safety_analysis", + fault_trees = ":sample_fault_trees", + safety_measures = [":sample_aous"], # counts the AoU as a measure of the root cause + # ... + ) + +The ``dependable_element`` derives its own AoUs from the ``safety_measures`` of its +safety analyses: an AoU is exposed to dependees once, however many safety +analyses list it. An ``assumptions_of_use`` target that no ``safety_analysis`` +lists in ``safety_measures`` is neither rendered nor forwarded, and a +``dependable_element`` without a safety analysis has no own AoUs. + An own AoU is never a ``derived_from`` source of a ``CompReq``; only AoUs received from another dependable element are (see `AoU Forwarding`_). @@ -105,7 +121,7 @@ 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 +All AoUs that the element's safety analyses list in ``safety_measures`` are automatically forwarded to every element that lists it in ``deps``. No configuration is needed. @@ -119,17 +135,24 @@ is forwarded rather than handled locally: :caption: examples/seooc/aou_forwarding.yaml forwarded_aous: - - aou_id: "OtherLibrary.TimingConstraint" + - aou_id: "OtherLibrary.TimingConstraint@1" 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. +``aou_id`` carries the version of the received AoU (``Package.Name@version``), +so a justification is reviewed again when the AoU changes. An unknown ID, a +mismatching version or a duplicate entry fails the build. + **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: +report. An AoU that reaches the element along several paths (for example +directly and through a dependency that forwards it) appears once; if the +paths carry different definitions, the build fails. Each received AoU must be +covered by at least one of: - **Handling it locally**: a component requirement's ``derived_from`` field references the AoU it implements (see below). This shows up as "Component @@ -192,10 +215,10 @@ Two things are required for the reference to resolve: ) 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. +a typo or an AoU whose ``assumptions_of_use`` target is missing from ``deps`` 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 @@ -203,9 +226,10 @@ example lives in ``examples/some_other_library``, ``examples/seooc``, and :: - other_seooc → defines AoU: OtherLibrary.TimingConstraint + other_seooc → defines AoUs: OtherLibrary.TimingConstraint, OtherLibrary.SpaceConstraint ↑ (deps) safety_software_seooc_example → defines own AoU: SampleType.SampleAoU (auto-forwarded) + → handles received SpaceConstraint locally via derived_from → chain-forwards received TimingConstraint via aou_forwarding.yaml ↑ (deps) integrator_seooc → receives SampleType.SampleAoU (auto-forwarded) @@ -217,7 +241,6 @@ example lives in ``examples/some_other_library``, ``examples/seooc``, and dependable_element( name = "safety_software_seooc_example", - assumptions_of_use = ["//docs:sample_aous"], aou_forwarding = "aou_forwarding.yaml", deps = ["@some_other_library//:other_seooc"], ... diff --git a/bazel/rules/rules_score/docs/user_guide/dependability_analysis.rst b/bazel/rules/rules_score/docs/user_guide/dependability_analysis.rst index 1a7f4fe7..aa406e48 100644 --- a/bazel/rules/rules_score/docs/user_guide/dependability_analysis.rst +++ b/bazel/rules/rules_score/docs/user_guide/dependability_analysis.rst @@ -16,7 +16,7 @@ Safety Analysis ================ .. note:: - A complete working example covering ``safety_analysis`` and ``dependability_analysis`` is + A complete working example covering ``failure_modes``, ``fault_trees``, ``safety_analysis`` and ``dependability_analysis`` is available in `bazel/rules/rules_score/examples/seooc/safety_analysis/ `_. @@ -85,8 +85,8 @@ Artifacts and traceability ~~~~~~~~~~~~~~~~~~~~~~~~~~ As mentioned above, the safety analysis method used by ``dependability_analysis`` is a combination of -both an FMEA and a FTA. Each ``safety_analysis`` target bundles four types of artifacts that must be -linked together: +both an FMEA and a FTA. The ``failure_modes``, ``fault_trees`` and ``safety_analysis`` targets together bundle four types of +artifacts that must be linked together: .. list-table:: :header-rows: 1 @@ -184,7 +184,7 @@ Each identified failure mode is recorded as a ``ScoreReq.FailureMode`` record in a ``.trlc`` file (here ``sample_safety_analysis_failure_modes.trlc``). The TRLC ``package`` name (``SampleLibrary`` below) is chosen freely by the author — it is not derived from the Bazel target name and is not enforced by the rule -(only the *generated* ``_fta`` package, see `Modeling the fault tree +(only the *generated* ```` package, see `Modeling the fault tree (PlantUML)`_, is fixed): .. code-block:: text @@ -309,8 +309,8 @@ Gate and intermediate-event aliases (e.g. ``OG_1``) are only local identifiers within the diagram; they do not refer to TRLC records. A dedicated Rust tool (``puml_cli``'s FTA parser) reads every ``$FailureMode``/ -``$RootCause`` in the ``safety_analysis``'s ``root_causes`` diagrams and generates a -TRLC stub package (``fta_events.trlc``, imported as the ``_fta`` +``$RootCause`` in the ``fault_trees``'s ``srcs`` diagrams and generates a +TRLC stub package (``fta_events.trlc``, imported as the ```` package) with one ``RootCause`` record per reachable ``$RootCause`` alias; each record's ``failure_modes`` lists the ``FailureMode`` record(s) of the ``$FailureMode`` root it hangs under. No manual linking @@ -385,12 +385,12 @@ Pick the path by *who* closes the root cause: - ``derived_from`` referencing the ``RootCause`` * - ``AoU`` (Assumption of Use) - an obligation only the **integrator/caller** can guarantee, not the SEooC. - - optional ``root_causes`` + - mandatory ``root_causes`` Recording measures (TRLC) ^^^^^^^^^^^^^^^^^^^^^^^^^ -Each measure imports the generated ``_fta`` package and references the +Each measure imports the generated ```` package and references the ``RootCause`` stub(s) it addresses explicitly from its ``root_causes``/``derived_from`` field. For example, the following ``Mitigation`` records (from ``sample_safety_analysis_safetymeasures.trlc``) @@ -439,7 +439,7 @@ A ``CompReq`` can reference the generated ``RootCause`` stub directly in its ``derived_from`` list (closing it via a normal, implemented-and-tested component requirement instead of a dedicated safety measure), or an ``AoU`` -can reference it via its own optional ``root_causes`` field (pushing the +can reference it via its mandatory ``root_causes`` field (pushing the obligation out to the integrator). The traceability report of the enclosing ``dependable_element`` flags a root cause that is covered by none of ``Mitigation``, ``CompReq``, or ``AoU``; with ``maturity = "release"`` this @@ -463,6 +463,8 @@ Step 4 — Wire the analysis into Bazel load( "@score_tooling//bazel/rules/rules_score:rules_score.bzl", + "failure_modes", + "fault_trees", "safety_analysis", ) @@ -475,39 +477,61 @@ Step 4 — Wire the analysis into Bazel visibility = ["//visibility:public"], ) + failure_modes( + name = "sample_failure_modes", + srcs = ["sample_safety_analysis_failure_modes.trlc"], + ) + + fault_trees( + name = "sample_fault_trees", + srcs = [":sample_fta"], + fta_package = "sample_safety_analysis_fta", + visibility = ["//visibility:public"], + deps = [":sample_failure_modes"], + ) + safety_analysis( name = "sample_safety_analysis", arch_design = "//design:sample_seooc_design", - failuremodes = ["sample_safety_analysis_failure_modes.trlc"], - root_causes = [":sample_fta"], - safetymeasures = ["sample_safety_analysis_safetymeasures.trlc"], + failure_modes = [":sample_failure_modes"], + fault_trees = ":sample_fault_trees", + safety_measures = [ + "//docs:sample_aous", + "sample_safety_analysis_safetymeasures.trlc", + ], visibility = ["//visibility:public"], ) -An ``AoU`` whose ``root_causes`` reference the generated ``_fta`` package -is wired twice. Pass its ``.trlc`` file in ``safetymeasures`` so it counts as a -measure of the root cause, and list the ``safety_analysis`` target in the -``deps`` of the ``assumptions_of_use`` target so the generated package resolves -when the AoU file is parsed. Every ``AoU`` in a ``safetymeasures`` file is -expected to reference a root cause; keep AoUs without ``root_causes`` in a -separate file that is only passed to ``assumptions_of_use``: +The analysis is split in three targets to avoid a dependency cycle: the +``failure_modes`` target holds the ``FailureMode`` records, the ``fault_trees`` +target generates the ```` package with the ``RootCause`` records +referencing them, the measures reference those records, and the +``safety_analysis`` target lists the failure modes and the measures. Every +``AoU`` references a root cause, so each ``assumptions_of_use`` target lists the +``fault_trees`` target in its ``deps`` (so the generated package resolves when +the AoU file is parsed) and is passed in the ``safety_measures`` of the +``safety_analysis`` (so it counts as a measure of the root cause): .. code-block:: starlark - - safety_analysis( - name = "sample_safety_analysis", - safetymeasures = ["aou.trlc"], - # ... - ) + :caption: bazel/rules/rules_score/examples/seooc/docs/BUILD assumptions_of_use( name = "sample_aous", - srcs = ["aou.trlc"], - deps = [":sample_safety_analysis"], + srcs = ["aous.trlc"], + deps = ["//safety_analysis:sample_fault_trees"], ) +Raw ``.trlc`` files in ``safety_measures`` may only hold ``Mitigation`` +records, and a root cause whose failure mode is not in ``failure_modes`` fails +the build. + +The ``dependable_element`` derives its own AoUs from the ``safety_measures`` of its +safety analyses, so the AoU reaches the dependees once, however many safety +analyses list it. An ``assumptions_of_use`` target that no ``safety_analysis`` +lists in ``safety_measures`` is neither rendered nor forwarded. + The ``dependability_analysis`` target of the dependable element then -references one or more ``safety_analysis`` targets by label: +references a ``safety_analysis`` target by label: .. code-block:: starlark :caption: bazel/rules/rules_score/examples/seooc/BUILD @@ -527,7 +551,15 @@ references one or more ``safety_analysis`` targets by label: and traceability report; ``bazel test`` validates the full chain (see `Traceability Validation`_). +The dependable element stages the pages of each ``dependability_analysis`` under +its name as ``safety_analysis.rst`` and ``fta.puml``. List one +``safety_analysis`` per ``dependability_analysis``; for several safety analyses, +use several ``dependability_analysis`` targets in the ``dependable_element``. +Items shared by several safety analyses (e.g. the same AoU) are kept once. + For the complete attribute reference, see +:ref:`failure_modes `, +:ref:`fault_trees `, :ref:`safety_analysis ` and :ref:`dependability_analysis ` in the rule index. @@ -565,10 +597,11 @@ enclosing ``dependable_element``: Mitigation / AoU / CompReq A root cause that is not referenced by any ``Mitigation.root_causes``, -``AoU.root_causes`` (passed in ``safetymeasures``), or ``CompReq.derived_from`` +``AoU.root_causes`` (from an ``assumptions_of_use`` target in ``safety_measures``), or ``CompReq.derived_from`` is reported as *missing reference to Component Requirements or Safety Measures*. Fixing a traceability error means ensuring the reference is explicit: the -``Mitigation``/``CompReq``/``AoU`` must import the generated ``_fta`` -package and reference the ``RootCause`` stub by name (e.g. +``Mitigation``/``CompReq``/``AoU`` must import the generated ```` +package (the ``fta_package`` of the ``fault_trees`` target) and reference the +``RootCause`` stub by name (e.g. ``sample_safety_analysis_fta.JustBadLuck``). diff --git a/bazel/rules/rules_score/docs/user_guide/tutorial/requirements.rst b/bazel/rules/rules_score/docs/user_guide/tutorial/requirements.rst index 2af0aa0a..a377e133 100644 --- a/bazel/rules/rules_score/docs/user_guide/tutorial/requirements.rst +++ b/bazel/rules/rules_score/docs/user_guide/tutorial/requirements.rst @@ -68,7 +68,6 @@ BUILD name = "my_element", integrity_level = "B", requirements = [":feature_requirements"], - assumptions_of_use = [], architectural_design = [], components = [], dependability_analysis = [], diff --git a/bazel/rules/rules_score/docs/user_guide/tutorial/setup.rst b/bazel/rules/rules_score/docs/user_guide/tutorial/setup.rst index 5ddf22bc..6614b185 100644 --- a/bazel/rules/rules_score/docs/user_guide/tutorial/setup.rst +++ b/bazel/rules/rules_score/docs/user_guide/tutorial/setup.rst @@ -32,7 +32,6 @@ BUILD name = "my_element", integrity_level = "B", requirements = [], - assumptions_of_use = [], architectural_design = [], components = [], dependability_analysis = [], diff --git a/bazel/rules/rules_score/examples/integrator/BUILD b/bazel/rules/rules_score/examples/integrator/BUILD index 16e9055b..e19656e2 100644 --- a/bazel/rules/rules_score/examples/integrator/BUILD +++ b/bazel/rules/rules_score/examples/integrator/BUILD @@ -25,6 +25,8 @@ # integrator_seooc (this target) # - receives SampleType.SampleAoU (auto-forwarded from seooc) # - receives OtherLibrary.TimingConstraint (chain-forwarded through seooc) +# - also depends on other_seooc directly, so TimingConstraint arrives a +# second time; it is merged into one received AoU # - HANDLES both locally: COMP_INT_001 and COMP_INT_002 # @@ -76,7 +78,6 @@ component( dependable_element( name = "integrator_seooc", architectural_design = [":integrator_design"], - assumptions_of_use = [], components = [":integrator_component"], dependability_analysis = [], integrity_level = "B", @@ -85,5 +86,8 @@ dependable_element( "//docs/requirements:feature_requirements", ], tests = [], - deps = ["@seooc//:safety_software_seooc_example"], + deps = [ + "@seooc//:safety_software_seooc_example", + "@some_other_library//:other_seooc", + ], ) diff --git a/bazel/rules/rules_score/examples/minimal/BUILD b/bazel/rules/rules_score/examples/minimal/BUILD index 1210b200..72ca078a 100644 --- a/bazel/rules/rules_score/examples/minimal/BUILD +++ b/bazel/rules/rules_score/examples/minimal/BUILD @@ -94,7 +94,6 @@ component( dependable_element( name = "my_element", architectural_design = [":my_arch"], - assumptions_of_use = [], components = [":MyComponent"], dependability_analysis = [], integrity_level = "B", diff --git a/bazel/rules/rules_score/examples/seooc/BUILD b/bazel/rules/rules_score/examples/seooc/BUILD index 06ea6d14..461071fd 100644 --- a/bazel/rules/rules_score/examples/seooc/BUILD +++ b/bazel/rules/rules_score/examples/seooc/BUILD @@ -73,10 +73,10 @@ 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). + # TimingConstraint from other_seooc is forwarded to dependees (integrator); + # SpaceConstraint is handled locally by REQ_COMP_EXTRA_003. aou_forwarding = "aou_forwarding.yaml", architectural_design = ["//design:sample_seooc_design"], - assumptions_of_use = ["//docs:sample_aous"], components = [":component_example"], dependability_analysis = [ ":sample_dependability_analysis", diff --git a/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml b/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml index 22ddbba9..ea118720 100644 --- a/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml +++ b/bazel/rules/rules_score/examples/seooc/aou_forwarding.yaml @@ -19,5 +19,5 @@ # AoU to its own dependees — the system integrator. # forwarded_aous: - - aou_id: "OtherLibrary.TimingConstraint" + - aou_id: "OtherLibrary.TimingConstraint@1" 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 23a8993d..c5f0dfdb 100644 --- a/bazel/rules/rules_score/examples/seooc/docs/BUILD +++ b/bazel/rules/rules_score/examples/seooc/docs/BUILD @@ -24,6 +24,7 @@ assumptions_of_use( "aous.trlc", ], visibility = ["//visibility:public"], + deps = ["//safety_analysis:sample_fault_trees"], ) trlc_requirements_test( diff --git a/bazel/rules/rules_score/examples/seooc/docs/aous.trlc b/bazel/rules/rules_score/examples/seooc/docs/aous.trlc index ddd90865..e5b91cae 100644 --- a/bazel/rules/rules_score/examples/seooc/docs/aous.trlc +++ b/bazel/rules/rules_score/examples/seooc/docs/aous.trlc @@ -13,9 +13,11 @@ package SampleType import ScoreReq +import sample_safety_analysis_fta ScoreReq.AoU SampleAoU { description = "It shall be made sure that shared memory segments are never created with the wrong name (ShmemCreatedWrongName)" safety = ScoreReq.Asil.B version = 1 + root_causes = [sample_safety_analysis_fta.ShmemCreatedWrongName] } diff --git a/bazel/rules/rules_score/examples/seooc/docs/requirements/BUILD b/bazel/rules/rules_score/examples/seooc/docs/requirements/BUILD index d3103306..1988caf4 100644 --- a/bazel/rules/rules_score/examples/seooc/docs/requirements/BUILD +++ b/bazel/rules/rules_score/examples/seooc/docs/requirements/BUILD @@ -60,6 +60,8 @@ component_requirements( deps = [ ":assumed_system_requirements", ":feature_requirements", + # AoU received from other_seooc and handled locally (SpaceConstraint). + "@some_other_library//:other_library_aous", ], ) diff --git a/bazel/rules/rules_score/examples/seooc/docs/requirements/component_requirements_extra.trlc b/bazel/rules/rules_score/examples/seooc/docs/requirements/component_requirements_extra.trlc index 91475004..3f9861c6 100644 --- a/bazel/rules/rules_score/examples/seooc/docs/requirements/component_requirements_extra.trlc +++ b/bazel/rules/rules_score/examples/seooc/docs/requirements/component_requirements_extra.trlc @@ -14,6 +14,7 @@ package SampleComponentExtra import ScoreReq import SampleSEooC +import OtherLibrary ScoreReq.CompReq REQ_COMP_EXTRA_001 { description = "The numeric value management interface shall return a fixed, well-defined value immediately upon construction, without requiring any prior initialization calls" @@ -28,3 +29,10 @@ ScoreReq.CompReq REQ_COMP_EXTRA_002 { derived_from = [SampleSEooC.FEAT_002@1] version = 1 } + +ScoreReq.CompReq REQ_COMP_EXTRA_003 { + description = "The numeric value management interface shall keep its memory footprint within the 10 MB budget imposed by the underlying other_seooc dependency" + safety = ScoreReq.Asil.B + derived_from = [SampleSEooC.FEAT_002@1, OtherLibrary.SpaceConstraint@1] + version = 1 +} diff --git a/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD b/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD index 597f34dd..babb7b38 100644 --- a/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD +++ b/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD @@ -13,6 +13,8 @@ load( "@score_tooling//bazel/rules/rules_score:rules_score.bzl", + "failure_modes", + "fault_trees", "safety_analysis", ) @@ -27,11 +29,30 @@ filegroup( visibility = ["//visibility:public"], ) +failure_modes( + name = "sample_failure_modes", + srcs = ["sample_safety_analysis_failure_modes.trlc"], +) + +# Root causes of the failure modes. Requirement targets (the AoUs in +# //docs:sample_aous) list it in `deps` to reference the generated root causes. +fault_trees( + name = "sample_fault_trees", + srcs = [":sample_fta"], + fta_package = "sample_safety_analysis_fta", + visibility = ["//visibility:public"], + deps = [":sample_failure_modes"], +) + +# The failure modes with the measures addressing their root causes. safety_analysis( name = "sample_safety_analysis", arch_design = "//design:sample_seooc_design", - failuremodes = ["sample_safety_analysis_failure_modes.trlc"], - root_causes = [":sample_fta"], - safetymeasures = ["sample_safety_analysis_safetymeasures.trlc"], + failure_modes = [":sample_failure_modes"], + fault_trees = ":sample_fault_trees", + safety_measures = [ + "//docs:sample_aous", + "sample_safety_analysis_safetymeasures.trlc", + ], visibility = ["//visibility:public"], ) diff --git a/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fta.puml b/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fta.puml index 648152c1..53f721ca 100644 --- a/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fta.puml +++ b/bazel/rules/rules_score/examples/seooc/safety_analysis/sample_fta.puml @@ -23,6 +23,7 @@ $OrGate("OG1", "SampleLibrary.SampleFailureMode") $IntermediateEvent("SampleFailureMode is Angry", "IEF", "OG1") $RootCause("Just bad luck", "JustBadLuck", "OG1") +$RootCause("Shared memory segment created with the wrong name", "ShmemCreatedWrongName", "OG1") ' 3rd level cascades from AGF $AndGate("AG2", "IEF") diff --git a/bazel/rules/rules_score/examples/seooc/test_case_coverage.lock.yaml b/bazel/rules/rules_score/examples/seooc/test_case_coverage.lock.yaml index 956cb743..92d2841d 100644 --- a/bazel/rules/rules_score/examples/seooc/test_case_coverage.lock.yaml +++ b/bazel/rules/rules_score/examples/seooc/test_case_coverage.lock.yaml @@ -86,3 +86,11 @@ requirements: given: a const Foo instance when: GetNumber is called multiple times then: the same value is returned on each call + - id: SampleComponentExtra.REQ_COMP_EXTRA_003 + version: '1' + description: The numeric value management interface shall keep its memory footprint within the 10 MB budget imposed by the underlying other_seooc dependency + test_cases: + - uid: //Foo:FootprintWithinBudget + given: the Foo class definition + when: querying the size of Foo + then: it stays far below the 10 MB budget diff --git a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp index b184929b..a8323f28 100644 --- a/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp +++ b/bazel/rules/rules_score/examples/seooc/unit_1/foo_test.cpp @@ -96,3 +96,14 @@ TEST(Foo, ValueConsistentAcrossReads) { "the same value is returned on each call"); EXPECT_EQ(unit.GetNumber(), unit.GetNumber()); } + +TEST(Foo, FootprintWithinBudget) { + ::testing::Test::RecordProperty("lobster-tracing", + "SampleComponentExtra.REQ_COMP_EXTRA_003"); + + ::testing::Test::RecordProperty("given", "the Foo class definition"); + ::testing::Test::RecordProperty("when", "querying the size of Foo"); + ::testing::Test::RecordProperty("then", + "it stays far below the 10 MB budget"); + EXPECT_LT(sizeof(unit_1::Foo), 10u * 1024u * 1024u); +} diff --git a/bazel/rules/rules_score/examples/some_other_library/BUILD b/bazel/rules/rules_score/examples/some_other_library/BUILD index f1d70c7b..1b185bc4 100644 --- a/bazel/rules/rules_score/examples/some_other_library/BUILD +++ b/bazel/rules/rules_score/examples/some_other_library/BUILD @@ -16,12 +16,19 @@ load( "architectural_design", "assumptions_of_use", "component", + "dependability_analysis", "dependable_element", + "failure_modes", + "fault_trees", + "safety_analysis", "unit", ) architectural_design( name = "sample_seooc_design", + public_api = [ + "public_api.puml", + ], static = [ "static_design.puml", ], @@ -31,6 +38,33 @@ assumptions_of_use( name = "other_library_aous", srcs = ["aous.trlc"], visibility = ["//visibility:public"], + deps = [":other_library_fault_trees"], +) + +failure_modes( + name = "other_library_failure_modes", + srcs = ["failure_modes.trlc"], +) + +fault_trees( + name = "other_library_fault_trees", + srcs = ["fta.puml"], + fta_package = "other_library_fta", + deps = [":other_library_failure_modes"], +) + +safety_analysis( + name = "other_library_safety_analysis", + arch_design = ":sample_seooc_design", + failure_modes = [":other_library_failure_modes"], + fault_trees = ":other_library_fault_trees", + safety_measures = [":other_library_aous"], +) + +dependability_analysis( + name = "other_library_dependability_analysis", + arch_design = ":sample_seooc_design", + safety_analysis = [":other_library_safety_analysis"], ) unit( @@ -56,9 +90,8 @@ component( dependable_element( name = "other_seooc", architectural_design = [":sample_seooc_design"], - assumptions_of_use = [":other_library_aous"], components = [":component_example"], - dependability_analysis = [], + dependability_analysis = [":other_library_dependability_analysis"], integrity_level = "D", requirements = [], tests = [], diff --git a/bazel/rules/rules_score/examples/some_other_library/aous.trlc b/bazel/rules/rules_score/examples/some_other_library/aous.trlc index 8922285f..084ce93b 100644 --- a/bazel/rules/rules_score/examples/some_other_library/aous.trlc +++ b/bazel/rules/rules_score/examples/some_other_library/aous.trlc @@ -13,15 +13,18 @@ package OtherLibrary import ScoreReq +import other_library_fta ScoreReq.AoU TimingConstraint { description = "The caller shall ensure that invocations do not exceed 10ms cycle time" safety = ScoreReq.Asil.B version = 1 + root_causes = [other_library_fta.CycleTimeExceeded] } ScoreReq.AoU SpaceConstraint { description = "The caller shall ensure that invocations do not exceed 10 MB" safety = ScoreReq.Asil.B version = 1 + root_causes = [other_library_fta.MemoryBudgetExceeded] } diff --git a/bazel/rules/rules_score/examples/some_other_library/failure_modes.trlc b/bazel/rules/rules_score/examples/some_other_library/failure_modes.trlc new file mode 100644 index 00000000..feb35229 --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/failure_modes.trlc @@ -0,0 +1,24 @@ +/******************************************************************************* + * 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 OtherLibraryFmea + +import ScoreReq + +ScoreReq.FailureMode ComputeOverloaded { + guidewords = [ScoreReq.Guideword.TooLate] + description = "Compute is invoked beyond the supported load" + failureeffect = "Results are delivered late or not at all" + version = 1 + safety = ScoreReq.Asil.B + interface = "other_seooc.OtherLibraryAPI.Compute" +} diff --git a/bazel/rules/rules_score/examples/some_other_library/fta.puml b/bazel/rules/rules_score/examples/some_other_library/fta.puml new file mode 100644 index 00000000..62c3892e --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/fta.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' 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 + +!include fta_metamodel.puml + +$FailureMode("Compute is invoked beyond the supported load", "OtherLibraryFmea.ComputeOverloaded") + +$OrGate("OG1", "OtherLibraryFmea.ComputeOverloaded") + +$RootCause("Cycle time exceeded", "CycleTimeExceeded", "OG1") +$RootCause("Memory budget exceeded", "MemoryBudgetExceeded", "OG1") + +@enduml diff --git a/bazel/rules/rules_score/examples/some_other_library/public_api.puml b/bazel/rules/rules_score/examples/some_other_library/public_api.puml new file mode 100644 index 00000000..bc68cb05 --- /dev/null +++ b/bazel/rules/rules_score/examples/some_other_library/public_api.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 + +namespace other_seooc { + interface "OtherLibraryAPI" as OtherLibraryAPI { + + Compute(): int + } +} + +@enduml diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 4eacad4b..a96ea563 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -26,6 +26,8 @@ component_requirements, and assumed_system_requirements. Traceability to feature/assumed-system requirements is established at the dependable_element level (via its own `requirements` attribute), not here. +Every AoU references the fault-tree root causes it addresses (`root_causes`), +so the `fault_trees` target belongs in `deps`. """ load("@trlc//:trlc.bzl", "trlc_requirements_test") @@ -56,9 +58,11 @@ def assumptions_of_use( Accepts raw ``.trlc`` files, ``.rst`` files containing ``aou_req`` directives (converted to TRLC automatically), and/or labels to existing targets that already provide TrlcProviderInfo (e.g. - ``trlc_requirements`` targets). - deps: Optional list of other requirement targets (providing - TrlcProviderInfo) needed for cross-reference parsing. + ``trlc_requirements`` targets). RST ``aou_req`` directives take + the root causes as ``:root_causes: ., ...``. + deps: List of the ``fault_trees`` target(s) providing the root causes + the AoUs reference, and optionally other requirement targets + (providing TrlcProviderInfo) needed for cross-reference parsing. ref_package: Optional TRLC package prefix used for ``derived_from`` cross-references when converting RST sources. lobster_config: Lobster YAML configuration for AoU traceability @@ -74,6 +78,7 @@ def assumptions_of_use( assumptions_of_use( name = "my_assumptions_of_use", srcs = ["assumptions_of_use.trlc"], + deps = [":my_fault_trees"], ) ``` @@ -82,6 +87,7 @@ def assumptions_of_use( assumptions_of_use( name = "my_assumptions_of_use", srcs = ["docs/assumptions_of_use.rst"], + deps = [":my_fault_trees"], ) ``` """ diff --git a/bazel/rules/rules_score/private/component_requirements.bzl b/bazel/rules/rules_score/private/component_requirements.bzl index fd138eff..35b8e0a4 100644 --- a/bazel/rules/rules_score/private/component_requirements.bzl +++ b/bazel/rules/rules_score/private/component_requirements.bzl @@ -49,10 +49,12 @@ 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. Also list any assumptions_of_use target(s) here (this - element's own, or one received/forwarded from a dependable_element - dependency) to resolve AoU references in `derived_from` - (CompReqSourceId). These targets must provide TrlcProviderInfo. + parsing. Also list the assumptions_of_use target of an AoU + received/forwarded from a dependable_element dependency here to + resolve AoU references in `derived_from` + (CompReqSourceId). List the fault_trees target here to resolve + `.` references in `derived_from`. + 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 diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 3913ed48..fb437f54 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -28,8 +28,8 @@ requirements. """ load("@lobster//:lobster.bzl", "subrule_lobster_html_report", "subrule_lobster_report") -load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "DependabilityAnalysisInfo", "SphinxSourcesInfo") -load("//bazel/rules/rules_score/private:lobster_config.bzl", "format_lobster_sources") +load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "DependabilityAnalysisInfo", "SafetyAnalysisProviderInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:lobster_config.bzl", "MERGE_LOBSTER_ITEMS_ATTR", "format_lobster_sources", "merge_lobster_files") load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR") # ============================================================================ @@ -46,8 +46,8 @@ def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, rst_aux_list, rst_srcs_list: List of depsets to extend with SphinxSourcesInfo.srcs. rst_deps_list: List of depsets to extend with SphinxSourcesInfo.deps. rst_aux_list: List of depsets to extend with SphinxSourcesInfo.aux_srcs. - lobster_files: Dict to update with AnalysisInfo.lobster_files - (canonical name → File). + lobster_files: Dict to extend with AnalysisInfo.lobster_files + (canonical name → list of Files, without duplicates). """ if SphinxSourcesInfo in sa: rst_srcs_list.append(sa[SphinxSourcesInfo].srcs) @@ -55,7 +55,10 @@ def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, rst_aux_list, if sa[SphinxSourcesInfo].aux_srcs: rst_aux_list.append(sa[SphinxSourcesInfo].aux_srcs) if AnalysisInfo in sa: - lobster_files.update(sa[AnalysisInfo].lobster_files) + for name, lobster_file in sa[AnalysisInfo].lobster_files.items(): + files = lobster_files.setdefault(name, []) + if lobster_file not in files: + files.append(lobster_file) # ============================================================================ # Private Rule Implementation @@ -84,7 +87,9 @@ def _dependability_analysis_impl(ctx): rst_srcs_transitive = [dfa_rst_files] rst_deps_transitive = [dfa_rst_files] rst_aux_transitive = [] - lobster_files = {} # canonical name → File, merged from all sub-analyses + lobster_files = {} # canonical name → list of Files, merged from all sub-analyses + aou_targets = [] + aou_labels = {} # ------------------------------------------------------------------------- # Collect from safety_analysis targets @@ -93,6 +98,11 @@ def _dependability_analysis_impl(ctx): for sa in ctx.attr.safety_analysis: safety_analysis_output_files.append(sa[DefaultInfo].files) _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, rst_aux_transitive, lobster_files) + if SafetyAnalysisProviderInfo in sa: + for aou in sa[SafetyAnalysisProviderInfo].aou_targets: + if aou.label not in aou_labels: + aou_labels[aou.label] = True + aou_targets.append(aou) # ------------------------------------------------------------------------- # Collect from security_analysis targets @@ -106,6 +116,14 @@ def _dependability_analysis_impl(ctx): # architectural_design attribute, so they are not included in this rule's # sphinx deps to avoid orphan warnings. + # Safety analyses sharing a measure each carry its item; keep it once. + for name in list(lobster_files.keys()): + lobster_files[name], _ = merge_lobster_files( + ctx, + lobster_files[name], + "{}/{}".format(ctx.label.name, name), + ) + all_rst_srcs = depset(transitive = rst_srcs_transitive) all_rst_deps = depset(transitive = rst_deps_transitive) all_rst_aux = depset(transitive = rst_aux_transitive) if rst_aux_transitive else depset() @@ -117,7 +135,7 @@ def _dependability_analysis_impl(ctx): lobster_html_file = None report_files = [] - all_lobster_file_objects = lobster_files.values() + all_lobster_file_objects = [f for files in lobster_files.values() for f in files] arch_lobster_files = arch_design_info.public_api_lobster_files.to_list() if arch_design_info else [] all_lobster_file_objects = list(all_lobster_file_objects) + arch_lobster_files if all_lobster_file_objects: @@ -130,8 +148,8 @@ def _dependability_analysis_impl(ctx): output = lobster_config, substitutions = { "{ARCH_SOURCES}": format_lobster_sources(arch_lobster_files), - "{FM_SOURCES}": format_lobster_sources([lobster_files["failuremodes.lobster"]] if "failuremodes.lobster" in lobster_files else []), - "{RC_SOURCES}": format_lobster_sources([lobster_files["fta_root_causes.lobster"]] if "fta_root_causes.lobster" in lobster_files else []), + "{FM_SOURCES}": format_lobster_sources(lobster_files.get("failuremodes.lobster", [])), + "{RC_SOURCES}": format_lobster_sources(lobster_files.get("fta_root_causes.lobster", [])), }, ) @@ -198,6 +216,7 @@ def _dependability_analysis_impl(ctx): arch_design = arch_design_info, name = ctx.label.name, lobster_files = lobster_files, + aou_targets = aou_targets, ), SphinxSourcesInfo( srcs = all_rst_srcs, @@ -253,7 +272,7 @@ _dependability_analysis_test = rule( doc = "Lobster config template for safety analysis traceability.", ), }, - **VERBOSITY_ATTR + **dict(VERBOSITY_ATTR, **MERGE_LOBSTER_ITEMS_ATTR) ), subrules = [subrule_lobster_report, subrule_lobster_html_report], test = True, diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 0d131002..1f46a65d 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -53,7 +53,9 @@ load( ) load( "//bazel/rules/rules_score/private:lobster_config.bzl", + "MERGE_LOBSTER_ITEMS_ATTR", "format_lobster_block", + "merge_lobster_files", ) load("//bazel/rules/rules_score/private:sphinx_module.bzl", "sphinx_module") load("//bazel/rules/rules_score/private:validation.bzl", "PROFILES", "VALIDATION_ATTRS", "run_validation") @@ -1036,6 +1038,19 @@ def _symlink_validation_log(ctx, validation_log): # Index Generation Rule Implementation # ============================================================================ +def _own_aou_targets(ctx): + """AoU targets this element owns: the measures of its safety analyses, deduplicated by label.""" + targets = [] + seen = {} + for da_target in ctx.attr.dependability_analysis: + if DependabilityAnalysisInfo not in da_target: + continue + for aou in da_target[DependabilityAnalysisInfo].aou_targets: + if aou.label not in seen: + seen[aou.label] = True + targets.append(aou) + return targets + def _cond_names(*conditioned_names): """Flatten (condition, level_name) pairs into a name list, dropping false conditions. @@ -1076,7 +1091,6 @@ def _dependable_element_index_impl(ctx): # Process each well-known artifact type into symlinked output files and # toctree references for the index template. artifact_types = [ - "assumptions_of_use", "dependability_analysis", "checklists", "glossary", @@ -1088,6 +1102,21 @@ def _dependable_element_index_impl(ctx): output_files.extend(files) artifacts_by_type[artifact_name] = refs + # Own AoUs are the safety analyses' measures, namespaced like every other + # artifact type under each target's own name. + own_aou_refs = [] + for aou_target in _own_aou_targets(ctx): + label_files, label_refs = _process_artifact_files( + ctx, + "assumptions_of_use", + aou_target, + seen_paths = seen_staged_paths, + errors = staging_errors, + path_prefix = "{}/".format(aou_target.label.name), + ) + output_files.extend(label_files) + own_aou_refs.extend(label_refs) + arch_refs_by_view = {view_name: [] for view_name, _ in ARCH_VIEWS} arch_unclassified_refs = [] @@ -1231,7 +1260,7 @@ def _dependable_element_index_impl(ctx): assumed_system_ref = _section_page( "assumed_system", "Assumed System", - assumed_system_req_refs + artifacts_by_type["assumptions_of_use"], + assumed_system_req_refs + own_aou_refs, ) software_arch_ref = _generate_software_arch_page( ctx, @@ -1491,11 +1520,22 @@ def _dependable_element_index_impl(ctx): coverage_lobster_files.append(cov_lobster) # Collect safety analysis lobster files from dependability_analysis targets - sa_lobster_files = {} # canonical name -> File, merged from all DA targets + sa_lobster_files = {} # canonical name -> list of Files, merged from all DA targets for da_target in ctx.attr.dependability_analysis: if DependabilityAnalysisInfo in da_target: da_info = da_target[DependabilityAnalysisInfo] - sa_lobster_files.update(da_info.lobster_files) + for name, files in da_info.lobster_files.items(): + merged = sa_lobster_files.setdefault(name, []) + merged.extend([f for f in files if f not in merged]) + + # An item shared by several dependability analyses is kept once. + for name in list(sa_lobster_files.keys()): + sa_lobster_files[name], merged_files = merge_lobster_files( + ctx, + sa_lobster_files[name], + "{}/merged_{}".format(ctx.label.name, name), + ) + output_files.extend(merged_files) # Collect public api lobster files from architectural_design targets public_api_lobster_list = [] @@ -1510,37 +1550,51 @@ def _dependable_element_index_impl(ctx): comp_test_list = comp_test_lobster_depset.to_list() comp_arch_list = comp_arch_lobster_depset.to_list() interface_req_list = public_api_lobster_list - fm_list = [sa_lobster_files["failuremodes.lobster"]] if "failuremodes.lobster" in sa_lobster_files else [] - rc_list = [sa_lobster_files["fta_root_causes.lobster"]] if "fta_root_causes.lobster" in sa_lobster_files else [] - safetymeasures_list = [sa_lobster_files["safetymeasures.lobster"]] if "safetymeasures.lobster" in sa_lobster_files else [] + fm_list = sa_lobster_files.get("failuremodes.lobster", []) + rc_list = sa_lobster_files.get("fta_root_causes.lobster", []) + safetymeasures_list = sa_lobster_files.get("safetymeasures.lobster", []) # ========================================================================= # 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 AoUs are the measures of the safety analyses. + own_aou_lobster_files = [ + aou_target[AssumptionsOfUseInfo].aou_lobster + for aou_target in _own_aou_targets(ctx) + ] own_aou_lobster_depset = depset(transitive = own_aou_lobster_files) - # Collect forwarded AoU lobster files from deps (received AoUs) - received_aou_lobster_files = [] + # Collect forwarded AoU lobster files from deps (received AoUs). The same + # AoU can arrive along several paths; the merge keeps one item per tag, + # preferring the files of the defining elements. + received_own_lobster = [] + received_chain_lobster = [] 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_own_lobster.append(fwd_info.own_aou_lobster) + received_chain_lobster.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() + received_own_list = depset(transitive = received_own_lobster).to_list() + received_aou_list = received_own_list + [ + f + for f in depset(transitive = received_chain_lobster).to_list() + if f not in received_own_list + ] + received_aou_list, merged_aous = merge_lobster_files( + ctx, + received_aou_list, + "{}/received_aous.lobster".format(ctx.label.name), + ) + output_files.extend(merged_aous) # Chain-forwarding: if aou_forwarding YAML is provided, filter received AoUs. + # The tool also runs without received AoUs, so entries naming a missing + # AoU fail the build. chain_forwarded_lobster_depset = depset() forwarded_aou_markers_list = [] - if ctx.file.aou_forwarding and received_aou_list: + if ctx.file.aou_forwarding: chain_forwarded_lobster_file = ctx.actions.declare_file( ctx.label.name + "/chain_forwarded_aous.lobster", ) @@ -1843,10 +1897,6 @@ def _dependable_element_index_attrs(): mandatory = True, doc = "Name of the dependable element module (used as document title)", ), - "assumptions_of_use": attr.label_list( - mandatory = True, - doc = "Assumptions of Use targets or files.", - ), "requirements": attr.label_list( mandatory = True, providers = [[FeatureRequirementsInfo], [AssumedSystemRequirementsInfo]], @@ -1946,6 +1996,7 @@ def _dependable_element_index_attrs(): } attrs.update(VALIDATION_ATTRS) attrs.update(VERBOSITY_ATTR) + attrs.update(MERGE_LOBSTER_ITEMS_ATTR) return attrs _dependable_element_index = rule( @@ -2086,7 +2137,6 @@ _dependable_element_test = rule( # lobster-trace: Tools.ArchitectureModelingDependableElement def dependable_element( name, - assumptions_of_use, requirements, architectural_design, dependability_analysis, @@ -2115,14 +2165,14 @@ def dependable_element( Args: name: The name of the dependable element. Used as the base name for all generated targets. - assumptions_of_use: List of labels to assumptions_of_use targets that - define the safety-relevant operating conditions and constraints. requirements: List of labels to requirements targets (component_requirements, feature_requirements, etc.) that define functional and safety requirements. architectural_design: List of labels to architectural_design targets that describe the software architecture and design decisions. dependability_analysis: List of labels to dependability_analysis targets containing safety analysis results (FMEA, FMEDA, FTA, DFA, etc.). + The AoUs the element exposes to its dependees are the `safety_measures` + of their safety_analysis targets. components: List of labels to component and/or unit targets that implement this dependable element. tests: List of labels to Bazel test targets that verify the dependable @@ -2184,7 +2234,6 @@ def dependable_element( module_name = name, template = Label("//bazel/rules/rules_score:templates/dependable_element_index.template.rst"), section_template = Label("//bazel/rules/rules_score:templates/section_page.template.rst"), - assumptions_of_use = assumptions_of_use, requirements = requirements, components = components, architectural_design = architectural_design, diff --git a/bazel/rules/rules_score/private/failure_modes.bzl b/bazel/rules/rules_score/private/failure_modes.bzl new file mode 100644 index 00000000..a4e252dc --- /dev/null +++ b/bazel/rules/rules_score/private/failure_modes.bzl @@ -0,0 +1,69 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +Failure Modes build rule for S-CORE projects. + +Holds the ``ScoreReq.FailureMode`` records of a safety analysis. The fault-tree +diagrams reference them as the top events of their trees (``fault_trees`` lists +the target in ``deps``); ``safety_analysis`` renders and traces them. +""" + +load("@trlc//:trlc.bzl", "trlc_requirements_test") +load("//bazel/rules/rules_score/private:requirements.bzl", "score_requirements_rule") + +def failure_modes( + name, + srcs, + deps = [], + lobster_config = Label("//bazel/rules/rules_score/lobster/config:failuremodes_config"), + **kwargs): + """Define the failure modes of a safety analysis. + + Creates a target providing FailureModesInfo, TrlcProviderInfo and + SphinxSourcesInfo, plus a validation test target ``_test``. + + Args: + name: The name of the target. + srcs: List of ``.trlc`` files containing ``ScoreReq.FailureMode`` records. + deps: Optional targets (providing TrlcProviderInfo) needed to parse the + records. + lobster_config: Lobster YAML configuration for FailureMode traceability + extraction. Defaults to the standard S-CORE failure mode config. + **kwargs: Additional arguments (e.g. ``visibility``, ``tags``). + + Generated Targets: + : Main target providing FailureModesInfo and TrlcProviderInfo. + _test: TRLC validation test (runs ``trlc --verify``). + + Example: + ```starlark + failure_modes( + name = "my_failure_modes", + srcs = ["failure_modes.trlc"], + ) + ``` + """ + score_requirements_rule( + name = name, + srcs = srcs, + deps = deps, + req_kind = "failure_mode", + lobster_config = lobster_config, + **kwargs + ) + trlc_requirements_test( + name = name + "_test", + reqs = [":" + name], + **kwargs + ) diff --git a/bazel/rules/rules_score/private/lobster_config.bzl b/bazel/rules/rules_score/private/lobster_config.bzl index 7e8188fb..16136494 100644 --- a/bazel/rules/rules_score/private/lobster_config.bzl +++ b/bazel/rules/rules_score/private/lobster_config.bzl @@ -20,6 +20,52 @@ format_lobster_sources() to build the source-line block, then pass it as a substitution value to ctx.actions.expand_template(). """ +load("//bazel/rules/rules_score/private:verbosity.bzl", "get_log_level") + +MERGE_LOBSTER_ITEMS_ATTR = { + "_merge_lobster_items_tool": attr.label( + default = Label("//bazel/rules/rules_score:merge_lobster_items"), + executable = True, + cfg = "exec", + doc = "Tool merging lobster files, collapsing items defined by several of them.", + ), +} + +def merge_lobster_files(ctx, files, output_name): + """Merge lobster files so an item defined by several of them is kept once. + + Lobster rejects an item defined in two input files; this collapses identical + definitions and fails the build on conflicting ones. The rule must declare + ``MERGE_LOBSTER_ITEMS_ATTR`` and ``VERBOSITY_ATTR``. + + Args: + ctx: Rule context. + files: List of File objects; the first file defining a tag wins. + output_name: Path of the merged file, relative to the package. + + Returns: + Tuple ``(files, created)``: the files to use in place of *files* (the + input list when it has at most one file), and the files this call + created. + """ + if len(files) <= 1: + return files, [] + merged = ctx.actions.declare_file(output_name) + args = ctx.actions.args() + args.add("--output", merged) + args.add("--log-level", get_log_level(ctx)) + args.add("--input-lobster") + args.add_all(files) + ctx.actions.run( + inputs = files, + outputs = [merged], + executable = ctx.executable._merge_lobster_items_tool, + arguments = [args], + progress_message = "Merging lobster items into %s" % merged.short_path, + mnemonic = "MergeLobsterItems", + ) + return [merged], [merged] + def format_lobster_sources(files): """Format a list of File objects as lobster source lines. diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index 7e4240ed..c2a6c57a 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -21,7 +21,7 @@ public-facing macros. load("@lobster//:lobster.bzl", "subrule_lobster_trlc") load("@trlc//:trlc.bzl", "TrlcProviderInfo", "subrule_trlc_image_stage") -load("//bazel/rules/rules_score:providers.bzl", "AssumedSystemRequirementsInfo", "AssumptionsOfUseInfo", "ComponentRequirementsInfo", "FeatureRequirementsInfo", "SafetyAnalysisProviderInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score:providers.bzl", "AssumedSystemRequirementsInfo", "AssumptionsOfUseInfo", "ComponentRequirementsInfo", "FailureModesInfo", "FeatureRequirementsInfo", "SphinxSourcesInfo") load("//bazel/rules/rules_score/private:rst_to_trlc.bzl", "rst_to_trlc") _DEFAULT_SPEC = Label("//bazel/rules/rules_score/trlc/config:score_requirements_model") @@ -34,6 +34,7 @@ _REQ_KIND_TITLES = { "component": "Component Requirements", "assumed_system": "Assumed System Requirements", "aou": "Assumptions of Use", + "failure_mode": "Failure Modes", } # ============================================================================ @@ -57,6 +58,8 @@ def _requirements_impl(ctx): transitive_spec = [] transitive_reqs = [] for dep in ctx.attr.deps: + if ctx.attr.req_kind == "aou" and AssumptionsOfUseInfo in dep: + fail("{}: deps must not hold the assumptions_of_use target {}; its AoUs would be extracted as own AoUs".format(ctx.label, dep.label)) trlc_info = dep[TrlcProviderInfo] transitive_spec.append(trlc_info.spec) transitive_reqs.append(trlc_info.reqs) @@ -117,6 +120,11 @@ def _requirements_impl(ctx): aou_lobster = depset([lobster_file]), name = ctx.label.name, ) + elif ctx.attr.req_kind == "failure_mode": + req_provider = FailureModesInfo( + srcs = depset([lobster_file]), + name = ctx.label.name, + ) else: # assumed_system req_provider = AssumedSystemRequirementsInfo( srcs = depset([lobster_file]), @@ -129,9 +137,7 @@ def _requirements_impl(ctx): transitive_sphinx = [sphinx_srcs] for dep in ctx.attr.deps: - # safety_analysis deps only supply TRLC symbols; its docs belong to the - # dependability analysis section. - if SphinxSourcesInfo in dep and SafetyAnalysisProviderInfo not in dep: + if SphinxSourcesInfo in dep: transitive_sphinx.append(dep[SphinxSourcesInfo].deps) return [ @@ -173,7 +179,7 @@ _score_requirements_rule = rule( doc = "Other requirement targets whose TRLC records are needed for cross-reference parsing.", ), "req_kind": attr.string( - values = ["feature", "component", "assumed_system", "aou"], + values = ["feature", "component", "assumed_system", "aou", "failure_mode"], mandatory = True, doc = "Kind of requirements; determines which domain provider is emitted.", ), diff --git a/bazel/rules/rules_score/private/safety_analysis.bzl b/bazel/rules/rules_score/private/safety_analysis.bzl index fc9bfc61..5768690b 100644 --- a/bazel/rules/rules_score/private/safety_analysis.bzl +++ b/bazel/rules/rules_score/private/safety_analysis.bzl @@ -14,31 +14,46 @@ """ Safety analysis (FMEA – Failure Mode and Effects Analysis) build rules for S-CORE projects. -The rule generates a single, failure-mode-centric ``safety_analysis.rst`` page: an -overview summary table followed by one section per failure mode. Each section -carries the full failure-mode safety attributes and one card per root cause -(``RootCause``) listing the measures (``Mitigation``, ``AoU``, ``CompReq``) that -address it. A "Fault Trees" section shows every fault-tree diagram once -(``.. uml::``). +The analysis is split into stages so that the measures addressing a root +cause (AoUs, component requirements) can reference the root causes without a +Bazel dependency cycle: + + 1. ``failure_modes`` – the ``FailureMode`` records and their lobster file + (see ``failure_modes.bzl``). + 2. ``fault_trees`` – the root causes. Takes only ``.puml`` diagrams; the + ``failure_modes`` targets they reference are listed in ``deps``. + ``puml_cli`` (``--fta-output-dir`` mode) parses the + ``$FailureMode``/``$RootCause``/gate macro calls of each diagram and emits + ``fta_events.trlc`` — the generated ``ScoreReq.RootCause`` records, each + linking to the ``FailureMode`` records of its tree (see + ``puml_fta::render_trlc_stub``). The root-cause lobster file is produced + here. Requirement targets (``assumptions_of_use``, + ``component_requirements``) list it in ``deps`` to resolve + ``.``. + 3. ``safety_analysis`` – the page. Takes the failure modes, the fault trees + and the ``safety_measures``: the measure targets (``assumptions_of_use`` / + ``component_requirements``) and ``Mitigation`` ``.trlc`` files that address + the root causes. + +``safety_analysis`` generates a single, failure-mode-centric +``safety_analysis.rst`` page: an overview summary table followed by one section +per failure mode. Each section carries the full failure-mode safety attributes +and one card per root cause (``RootCause``) listing the measures +(``Mitigation``, ``AoU``, ``CompReq``) that address it. A "Fault Trees" section +shows every fault-tree diagram once (``.. uml::``). Failure modes not covered by any fault tree, and measures not referencing any generated root cause, are still rendered so nothing is dropped. -Pipeline: +Pipeline of ``safety_analysis``: - 1. **FTA** (``puml_cli`` in ``--fta-output-dir`` mode) – parses the - ``$FailureMode``/``$RootCause``/gate macro calls straight from each - ``root_causes`` diagram and emits ``fta_events.trlc`` — the generated - ``ScoreReq.RootCause`` records, each linking to the ``FailureMode`` - records of its tree (see ``puml_fta::render_trlc_stub``). Each source - diagram is staged (symlinked) alongside ``safety_analysis.rst``, - unmodified, so ``.. uml:: `` resolves in the Sphinx tree. - 2. **Assembly** (``safety_analysis_assembler``) – a single in-process TRLC parse (via + 1. **Assembly** (``safety_analysis_assembler``) – a single in-process TRLC parse (via the extended ``TRLCRST`` library) over ``fta_events.trlc`` plus the FailureMode / Mitigation / AoU records renders the overview table, the root-cause cards and the fault-tree diagrams into ``safety_analysis.rst``. - 3. **Lobster** (``lobster-trlc``) – FailureMode, safety-measure (Mitigation / AoU) - and RootCause traceability files, driven by real TRLC references - (``failure_modes``, ``root_causes``) rather than + Each source diagram is staged (symlinked) alongside ``safety_analysis.rst``, + unmodified, so ``.. uml:: `` resolves in the Sphinx tree. + 2. **Lobster** (``lobster-trlc``) – safety-measure (Mitigation / AoU) + traceability, driven by real TRLC references (``root_causes``) rather than alias-name matching. The ``.puml`` diagrams travel as ``aux_srcs`` so Sphinx can @@ -46,22 +61,30 @@ resolve ``.. uml::`` without adding them to the toctree. ``AnalysisInfo`` carries all lobster traceability files (failuremodes, safetymeasures, fta_root_causes) as a ``lobster_files`` dict -keyed by canonical filename. ``SafetyAnalysisProviderInfo`` marks the target. All Sphinx source files travel +keyed by canonical filename. ``SafetyAnalysisProviderInfo`` carries the AoU +targets consumed as measures; the ``dependable_element`` derives its own AoUs +from them. All Sphinx source files travel via ``SphinxSourcesInfo``. -This is a **build-only** rule. The combined traceability *test* is owned by the -``dependability_analysis`` rule which wraps this one. +Both rules are **build-only**. The combined traceability *test* is owned by the +``dependability_analysis`` rule which wraps ``safety_analysis``. """ load("@trlc//:trlc.bzl", "TrlcProviderInfo") -load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "SafetyAnalysisProviderInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "AssumptionsOfUseInfo", "ComponentRequirementsInfo", "FailureModesInfo", "FaultTreesInfo", "SafetyAnalysisProviderInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:lobster_config.bzl", "MERGE_LOBSTER_ITEMS_ATTR", "merge_lobster_files") load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") -def _default_fta_package(name): - """Best-effort conversion of a Bazel target *name* into a valid TRLC - package identifier (``[A-Za-z_][A-Za-z0-9_]*``): non-identifier - characters (e.g. ``-``) become ``_``; a name starting with a digit gets a - leading ``_``. +def _default_fta_package(label): + """Best-effort conversion of a Bazel *label* into a valid TRLC package + identifier (``[A-Za-z_][A-Za-z0-9_]*``). + + Package and name are joined with ``__`` so equally named targets of + different packages get distinct packages in one TRLC build. The repository + is left out: it differs between a root build and a build as a dependency, + which would change the package that downstream records reference. + Non-identifier characters (e.g. ``-``, ``/``) become ``_``; a result + starting with a digit gets a leading ``_``. A ``_fta`` suffix is appended: TRLC rejects packages whose name is "too similar" (same modulo case/underscores) to another declared package, and @@ -70,6 +93,7 @@ def _default_fta_package(name): ``MySafetyAnalysis``) -- using the bare sanitized name here would then always collide with that package. """ + name = "__".join([p for p in [label.package, label.name] if p]) out = "" for i in range(len(name)): c = name[i] @@ -83,34 +107,29 @@ def _default_fta_package(name): # ============================================================================ def _process_root_causes(ctx, fta_package): - """Extract the generated TRLC stub and stage the diagrams for rendering. + """Generate the TRLC stub of the fault-tree diagrams. ``puml_cli`` (FTA mode) parses the ``$FailureMode``/``$RootCause``/gate macro calls straight from each diagram and emits ``fta_events.trlc`` into ``{label}/`` (the generated ``RootCause`` records, package *fta_package*; see ``puml_fta::render_trlc_stub``). - The diagrams are *not* rewritten: each source ``.puml`` is symlinked next to - ``safety_analysis.rst`` so ``.. uml:: `` resolves to the authored diagram. - Its ``!include fta_metamodel.puml`` is resolved at render time via the docs + The diagrams are *not* rewritten; ``safety_analysis`` stages them unmodified. + Their ``!include fta_metamodel.puml`` is resolved at render time via the docs toolchain's global PlantUML include path (the metamodel is shipped with - the registered ``sphinx_toolchain``), so the metamodel is not staged here. + the registered ``sphinx_toolchain``). Args: ctx: Rule context. fta_package: TRLC package name for the generated ``fta_events.trlc``. Returns: - Tuple ``(diagram_aux_files, fta_events_trlc)``. - ``diagram_aux_files`` (the staged ``.puml`` diagrams) is empty when + Tuple ``(puml_inputs, fta_events_trlc)``. + ``puml_inputs`` (the authored ``.puml`` diagrams) is empty when there are no PlantUML inputs; ``fta_events_trlc`` is always a File (an empty stub package when there are no diagrams). """ - puml_inputs = [ - f - for f in ctx.files.root_causes - if f.extension in ("puml", "plantuml") - ] + puml_inputs = ctx.files.srcs fta_events_trlc = ctx.actions.declare_file("{}/fta_events.trlc".format(ctx.label.name)) @@ -125,14 +144,6 @@ def _process_root_causes(ctx, fta_package): ) return [], fta_events_trlc - # Symlink each authored diagram next to safety_analysis.rst so ``.. uml:: `` - # resolves in the Sphinx tree. - diagram_aux_files = [] - for src in puml_inputs: - staged = ctx.actions.declare_file("{}/{}".format(ctx.label.name, src.basename)) - ctx.actions.symlink(output = staged, target_file = src) - diagram_aux_files.append(staged) - args = ctx.actions.args() for src in puml_inputs: args.add("--file", src.path) @@ -147,14 +158,14 @@ def _process_root_causes(ctx, fta_package): progress_message = "Processing root cause FTA diagrams for %s" % ctx.label.name, ) - return diagram_aux_files, fta_events_trlc + return puml_inputs, fta_events_trlc # ============================================================================ # Lobster (TRLC traceability) helper # ============================================================================ def _lobster_trlc(ctx, trlc_files, config, out_name): - """Run ``lobster-trlc`` over *trlc_files* producing ``{label}/``.""" + """Run ``lobster-trlc`` over *trlc_files* (incl. the model spec) producing ``{label}/``.""" if not trlc_files: return None out = ctx.actions.declare_file("{}/{}".format(ctx.label.name, out_name)) @@ -162,7 +173,7 @@ def _lobster_trlc(ctx, trlc_files, config, out_name): args.add("--config", config.path) args.add("--out", out.path) ctx.actions.run( - inputs = trlc_files + ctx.files.spec + [config], + inputs = depset(trlc_files + [config]), outputs = [out], executable = ctx.executable._lobster_trlc, arguments = [args], @@ -174,81 +185,171 @@ def _lobster_trlc(ctx, trlc_files, config, out_name): # Private Rule Implementation # ============================================================================ +def _unseen_files(depsets, known): + """Files of *depsets* whose path is not in *known*; adds them to *known*.""" + files = [] + for f in depset(transitive = depsets).to_list(): + if f.path not in known: + known[f.path] = True + files.append(f) + return files + +def _fault_trees_impl(ctx): + fta_package = ctx.attr.fta_package if ctx.attr.fta_package else _default_fta_package(ctx.label) + + diagrams, fta_events_trlc = _process_root_causes(ctx, fta_package) + + # fta_events.trlc ``import``s every FailureMode package its RootCause + # records reference, so the failure mode files travel with it in every + # lobster-trlc input set and in the provider's deps. + fm_infos = [dep[TrlcProviderInfo] for dep in ctx.attr.deps] + fm_files = depset(transitive = [info.reqs for info in fm_infos] + [info.deps for info in fm_infos]) + spec = depset(ctx.files.spec, transitive = [info.spec for info in fm_infos]) + + lobster_files = {} + if diagrams: + lobster_files["fta_root_causes.lobster"] = _lobster_trlc( + ctx, + [fta_events_trlc] + fm_files.to_list() + spec.to_list(), + ctx.file._rc_lobster_config, + "fta_root_causes.lobster", + ) + + return [ + DefaultInfo(files = depset([fta_events_trlc] + lobster_files.values())), + FaultTreesInfo( + fta_package = fta_package, + fta_events = fta_events_trlc, + diagrams = diagrams, + lobster_files = lobster_files, + ), + # Lets requirement targets (assumptions_of_use, component_requirements) + # list this target in `deps` to resolve `.`. + TrlcProviderInfo( + spec = spec, + reqs = depset([fta_events_trlc]), + deps = fm_files, + ), + ] + +def _split_safety_measures(ctx): + """Split ``safety_measures`` into measure targets and raw Mitigation files.""" + measure_targets = [] + mitigation_files = [] + for target in ctx.attr.safety_measures: + if AssumptionsOfUseInfo in target or ComponentRequirementsInfo in target: + measure_targets.append(target) + elif TrlcProviderInfo in target: + fail("{}: safety_measures entry {} must be an assumptions_of_use or component_requirements target, or a Mitigation .trlc file".format(ctx.label, target.label)) + else: + mitigation_files.extend(target[DefaultInfo].files.to_list()) + return measure_targets, mitigation_files + def _safety_analysis_impl(ctx): - output_files = [] + fault_trees = ctx.attr.fault_trees[FaultTreesInfo] + fault_trees_trlc = ctx.attr.fault_trees[TrlcProviderInfo] + fta_events_trlc = fault_trees.fta_events + + fm_infos = [fm[TrlcProviderInfo] for fm in ctx.attr.failure_modes] + failuremode_files = depset(transitive = [info.reqs for info in fm_infos]).to_list() - fta_package = ctx.attr.fta_package if ctx.attr.fta_package else _default_fta_package(ctx.label.name) + # Failure mode files the root causes import. Parsed with the page, but only + # the records of ``failure_modes`` are rendered and traced. + fm_context_files = [ + f + for f in depset(transitive = [fault_trees_trlc.deps] + [info.deps for info in fm_infos]).to_list() + if f not in failuremode_files + ] + fta_and_fm_files = [fta_events_trlc] + failuremode_files + fm_context_files + + # Stage each authored diagram next to safety_analysis.rst so + # ``.. uml:: `` resolves in the Sphinx tree. + diagram_aux_files = [] + for src in fault_trees.diagrams: + staged = ctx.actions.declare_file("{}/{}".format(ctx.label.name, src.basename)) + ctx.actions.symlink(output = staged, target_file = src) + diagram_aux_files.append(staged) - # 0. FTA: extract the generated TRLC stub and stage diagrams for rendering. - diagram_aux_files, fta_events_trlc = _process_root_causes(ctx, fta_package) - output_files.extend(diagram_aux_files) - has_root_causes = bool(diagram_aux_files) + # The records of the measure targets are rendered and traced. The TRLC they + # resolve against (their deps) is only parsed, so records held there (e.g. a + # received AoU a component requirement derives from) are not treated as + # measures of this analysis. The fault-tree files are passed separately. + measure_targets, mitigation_files = _split_safety_measures(ctx) + infos = [measure[TrlcProviderInfo] for measure in measure_targets] + spec_files = depset(ctx.files.spec, transitive = [fault_trees_trlc.spec] + [info.spec for info in fm_infos + infos]).to_list() + known = {f.path: True for f in fta_and_fm_files + mitigation_files + spec_files} + aou_targets = [m for m in measure_targets if AssumptionsOfUseInfo in m] + measure_files = _unseen_files([info.reqs for info in infos], known) + context_files = _unseen_files([info.deps for info in infos], known) + + # lobster-trlc registers every input and extracts Mitigation / AoU records + # only, so it gets the AoU measures plus the context they need to parse; a + # component requirement measure is traced via its own level. + aou_paths = { + f.path: True + for f in depset(transitive = [m[TrlcProviderInfo].reqs for m in aou_targets] + [m[TrlcProviderInfo].deps for m in aou_targets]).to_list() + } + lobster_measure_files = mitigation_files + [f for f in measure_files + context_files if f.path in aou_paths] # 1. Assemble safety_analysis.rst from the generated FTA stub + TRLC records (single in-process parse). safety_analysis_rst = ctx.actions.declare_file("{}/safety_analysis.rst".format(ctx.label.name)) - title = ctx.label.name args = ctx.actions.args() args.add("--output", safety_analysis_rst.path) args.add("--template", ctx.file._template.path) - args.add("--title", title) - args.add("--fta-package", fta_package) + args.add("--title", ctx.label.name) + args.add("--fta-package", fault_trees.fta_package) args.add("--fta-events", fta_events_trlc.path) args.add_all("--diagrams", [f.basename for f in diagram_aux_files]) args.add("--log-level", get_log_level(ctx)) - if ctx.files.failuremodes: + if failuremode_files: args.add("--failuremodes") - args.add_all(ctx.files.failuremodes) - if ctx.files.safetymeasures: + args.add_all(failuremode_files) + if mitigation_files: + args.add("--mitigations") + args.add_all(mitigation_files) + if measure_files: args.add("--safetymeasures") - args.add_all(ctx.files.safetymeasures) - if ctx.files.spec: - args.add("--spec") - args.add_all(ctx.files.spec) + args.add_all(measure_files) + dep_files = spec_files + fm_context_files + context_files + if dep_files: + args.add("--dep-files") + args.add_all(dep_files) ctx.actions.run( - inputs = ( - ctx.files.failuremodes + - ctx.files.safetymeasures + - ctx.files.spec + - [fta_events_trlc, ctx.file._template] + inputs = depset( + failuremode_files + + mitigation_files + + measure_files + + dep_files + + [fta_events_trlc, ctx.file._template], ), outputs = [safety_analysis_rst], executable = ctx.executable._safety_analysis_assembler, arguments = [args], progress_message = "Assembling safety-analysis page for %s" % ctx.label.name, ) - output_files.append(safety_analysis_rst) - - # 2. lobster-trlc traceability for FailureMode / Mitigation / RootCause records. - # - # fta_events.trlc always ``import``s every FailureMode package referenced by - # its RootCause records (regardless of which record a given invocation is - # actually converting), so ctx.files.failuremodes must travel alongside it - # in every lobster-trlc input set, or TRLC parsing fails on the unresolved - # import. - fta_and_fm_files = [fta_events_trlc] + ctx.files.failuremodes - - fm_lobster = _lobster_trlc(ctx, ctx.files.failuremodes, ctx.file._fm_lobster_config, "failuremodes.lobster") - safetymeasures_trlc_files = ctx.files.safetymeasures + (fta_and_fm_files if ctx.files.safetymeasures else []) - safetymeasures_lobster = _lobster_trlc(ctx, safetymeasures_trlc_files, ctx.file._safetymeasures_lobster_config, "safetymeasures.lobster") - - rc_lobster = None - if has_root_causes: - rc_lobster = _lobster_trlc( + + # 2. Lobster traceability: failure modes (merged over the failure_modes + # targets), root causes (from the fault trees) and Mitigation / AoU records. + lobster_files = dict(fault_trees.lobster_files) + fm_lobster_files, _ = merge_lobster_files( + ctx, + depset(transitive = [fm[FailureModesInfo].srcs for fm in ctx.attr.failure_modes]).to_list(), + "{}/failuremodes.lobster".format(ctx.label.name), + ) + if fm_lobster_files: + lobster_files["failuremodes.lobster"] = fm_lobster_files[0] + + safetymeasures_lobster = None + if lobster_measure_files: + safetymeasures_lobster = _lobster_trlc( ctx, - fta_and_fm_files, - ctx.file._rc_lobster_config, - "fta_root_causes.lobster", + lobster_measure_files + fta_and_fm_files + spec_files, + ctx.file._safetymeasures_lobster_config, + "safetymeasures.lobster", ) - - # 3. Providers. - lobster_files = {} - if fm_lobster: - lobster_files["failuremodes.lobster"] = fm_lobster if safetymeasures_lobster: lobster_files["safetymeasures.lobster"] = safetymeasures_lobster - if rc_lobster: - lobster_files["fta_root_causes.lobster"] = rc_lobster # The .puml diagrams are referenced inline via ``.. uml::`` but # must not be toctree documents, so they travel as aux_srcs (symlinked @@ -257,22 +358,13 @@ def _safety_analysis_impl(ctx): return [ DefaultInfo( - files = depset(output_files + [v for v in lobster_files.values()]), + files = depset(diagram_aux_files + [safety_analysis_rst] + lobster_files.values()), ), AnalysisInfo( name = ctx.label.name, lobster_files = lobster_files, ), - SafetyAnalysisProviderInfo(), - # Lets requirement targets (assumptions_of_use, component_requirements) - # list this target in `deps` to resolve `.`. - # safetymeasures are excluded: they may define AoU/CompReq records that - # the depending target also owns, which would duplicate them. - TrlcProviderInfo( - spec = depset(ctx.files.spec), - reqs = depset([fta_events_trlc] + ctx.files.failuremodes), - deps = depset(), - ), + SafetyAnalysisProviderInfo(aou_targets = aou_targets), SphinxSourcesInfo( srcs = sphinx_srcs, deps = depset(transitive = [sphinx_srcs]), @@ -284,22 +376,22 @@ def _safety_analysis_impl(ctx): # Rule Definition # ============================================================================ -_safety_analysis = rule( - implementation = _safety_analysis_impl, - doc = "Renders a failure-mode-centric safety-analysis page (overview table, one section per " + - "failure mode with its root causes and measures, and the fault-tree diagrams) and lobster " + - "traceability files. Build-only rule; traceability testing is owned by dependability_analysis.", +_fault_trees = rule( + implementation = _fault_trees_impl, + doc = "Generates the fault-tree root-cause TRLC stub from FTA PlantUML diagrams and the " + + "root-cause lobster traceability file. Build-only rule.", attrs = dict( { - "failuremodes": attr.label_list( - allow_files = [".trlc"], + "srcs": attr.label_list( + allow_files = [".puml", ".plantuml"], mandatory = False, - doc = "Failure mode ``.trlc`` source files.", + doc = "FTA PlantUML diagram files. " + + "The ``fta_events.trlc`` TRLC stub is generated from them.", ), - "safetymeasures": attr.label_list( - allow_files = [".trlc"], + "deps": attr.label_list( + providers = [[FailureModesInfo, TrlcProviderInfo]], mandatory = False, - doc = "Safety measure (``Mitigation``/``AoU``/``CompReq``) ``.trlc`` source files.", + doc = "``failure_modes`` targets holding the failure modes the diagrams reference.", ), "spec": attr.label_list( allow_files = [".rsl", ".trlc"], @@ -307,23 +399,12 @@ _safety_analysis = rule( doc = "TRLC model specification files (``.rsl``) required for import resolution. " + "Defaults to the S-CORE requirements model.", ), - "root_causes": attr.label_list( - allow_files = [".puml", ".plantuml"], - mandatory = False, - doc = "Root cause FTA PlantUML diagram files. " + - "The ``fta_events.trlc`` TRLC stub is generated from them.", - ), "fta_package": attr.string( mandatory = False, default = "", doc = "TRLC package name for the generated ``fta_events.trlc`` stub file " + - "(the ``RootCause`` records generated from ``root_causes``). " + - "Defaults to a sanitized form of the target name.", - ), - "arch_design": attr.label( - providers = [ArchitecturalDesignInfo], - mandatory = True, - doc = "architectural_design target for traceability.", + "(the ``RootCause`` records generated from ``srcs``). " + + "Defaults to a sanitized form of the target's package and name.", ), "_puml_cli": attr.label( default = Label("//plantuml/parser/puml_cli:puml_cli"), @@ -332,6 +413,58 @@ _safety_analysis = rule( cfg = "exec", doc = "puml_cli binary used in FTA mode to extract fta_events.trlc.", ), + "_lobster_trlc": attr.label( + default = Label("@lobster//:lobster-trlc"), + executable = True, + allow_files = True, + cfg = "exec", + doc = "lobster-trlc executable used to generate the RC lobster file.", + ), + "_rc_lobster_config": attr.label( + default = Label("//bazel/rules/rules_score/lobster/config:fta_root_causes_config"), + allow_single_file = True, + doc = "lobster-trlc YAML config for generated RootCause records.", + ), + }, + **VERBOSITY_ATTR + ), +) + +_safety_analysis = rule( + implementation = _safety_analysis_impl, + doc = "Renders a failure-mode-centric safety-analysis page (overview table, one section per " + + "failure mode with its root causes and measures, and the fault-tree diagrams) and lobster " + + "traceability files. Build-only rule; traceability testing is owned by dependability_analysis.", + attrs = dict( + { + "failure_modes": attr.label_list( + providers = [[FailureModesInfo, TrlcProviderInfo]], + mandatory = False, + doc = "``failure_modes`` targets whose records are rendered and traced.", + ), + "fault_trees": attr.label( + providers = [[FaultTreesInfo, TrlcProviderInfo]], + mandatory = True, + doc = "``fault_trees`` target providing the root causes.", + ), + "safety_measures": attr.label_list( + allow_files = [".trlc"], + mandatory = False, + doc = "Measures addressing the root causes: ``assumptions_of_use`` / ``component_requirements`` " + + "targets and ``Mitigation`` ``.trlc`` files. " + + "Only the own records of a target are rendered and traced; the targets in its ``deps`` are used to resolve references only.", + ), + "spec": attr.label_list( + allow_files = [".rsl", ".trlc"], + default = [Label("//bazel/rules/rules_score/trlc/config:score_requirements_model")], + doc = "TRLC model specification files (``.rsl``) required for import resolution. " + + "Defaults to the S-CORE requirements model.", + ), + "arch_design": attr.label( + providers = [ArchitecturalDesignInfo], + mandatory = True, + doc = "architectural_design target for traceability.", + ), "_safety_analysis_assembler": attr.label( default = Label("//bazel/rules/rules_score:safety_analysis_assembler"), executable = True, @@ -344,22 +477,12 @@ _safety_analysis = rule( executable = True, allow_files = True, cfg = "exec", - doc = "lobster-trlc executable used to generate FM and CM lobster files.", - ), - "_fm_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:failuremodes_config"), - allow_single_file = True, - doc = "lobster-trlc YAML config for FailureMode records.", + doc = "lobster-trlc executable used to generate the safety measure lobster file.", ), "_safetymeasures_lobster_config": attr.label( default = Label("//bazel/rules/rules_score/lobster/config:safetymeasures_config"), allow_single_file = True, - doc = "lobster-trlc YAML config for Mitigation records.", - ), - "_rc_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:fta_root_causes_config"), - allow_single_file = True, - doc = "lobster-trlc YAML config for generated RootCause records.", + doc = "lobster-trlc YAML config for Mitigation / AoU records.", ), "_template": attr.label( default = Label("//bazel/rules/rules_score:templates/safety_analysis.template.rst"), @@ -367,22 +490,62 @@ _safety_analysis = rule( doc = "RST template for the safety-analysis page (single ``{body}`` placeholder).", ), }, - **VERBOSITY_ATTR + **dict(VERBOSITY_ATTR, **MERGE_LOBSTER_ITEMS_ATTR) ), ) # ============================================================================ -# Public Macro +# Public Macros # ============================================================================ +def fault_trees( + name, + srcs = [], + deps = [], + spec = None, + fta_package = None, + **kwargs): + """Define the fault trees of a safety analysis: the root causes of the failure modes. + + FTA diagrams passed via ``srcs`` are preprocessed to extract fault-tree + topology, emitted as strongly-typed TRLC ``RootCause`` records + (``fta_events.trlc``, package ``fta_package``), each linking to the + ``FailureMode`` records of its tree. + + The target emits ``TrlcProviderInfo`` (FTA stub; failure modes as deps), so + the ``assumptions_of_use`` or ``component_requirements`` targets that + reference these root causes list it in their ``deps``. A ``safety_analysis`` + target consumes it via ``fault_trees``. + + Args: + name: Target name. + srcs: FTA PlantUML diagram files (``.puml`` / ``.plantuml``). + deps: ``failure_modes`` targets holding the failure modes the diagrams + reference. + spec: TRLC model specification files (``.rsl``) for resolving imports. + Defaults to the S-CORE requirements model. Override only when using + a custom TRLC schema. + fta_package: TRLC package name for the generated ``fta_events.trlc`` + stub file. Defaults to a sanitized form of the target's package + and ``name``. + **kwargs: Additional arguments (e.g. ``visibility``, ``tags``). + """ + _fault_trees( + name = name, + srcs = srcs, + deps = deps, + spec = spec, + fta_package = fta_package or "", + **kwargs + ) + def safety_analysis( name, arch_design, + fault_trees, + failure_modes = [], + safety_measures = [], spec = None, - failuremodes = [], - safetymeasures = [], - root_causes = [], - fta_package = None, **kwargs): """Define a safety analysis (FMEA - Failure Mode and Effects Analysis) following S-CORE process guidelines. @@ -390,17 +553,12 @@ def safety_analysis( summary table followed by one section per failure mode (failure-mode detail and one card per root cause with its measures), plus the fault-tree diagrams. - FTA diagrams passed via ``root_causes`` are preprocessed to extract - fault-tree topology, emitted as strongly-typed TRLC ``RootCause`` records - (``fta_events.trlc``, package ``fta_package``), each linking to the - ``FailureMode`` records of its tree. ``Mitigation``, ``AoU`` (own, preventive - measure) and ``CompReq`` (control measure) records reference a root cause via - ``root_causes``/``derived_from``. - The target emits ``TrlcProviderInfo`` (FTA stub + failure modes), so an - ``assumptions_of_use`` or ``component_requirements`` target referencing - these root causes lists it in its ``deps``. Pass the ``.trlc`` files holding - the measures as ``safetymeasures`` so they appear on the page and in the - traceability report. + ``Mitigation``, ``AoU`` (own, preventive measure) and ``CompReq`` (control + measure) records reference a root cause via ``root_causes``/``derived_from``. + Pass the targets holding the ``AoU`` / ``CompReq`` records and the + ``Mitigation`` files as ``safety_measures``; they appear on the page and in + the traceability report. The ``AoU`` targets among them are the AoUs the + ``dependable_element`` exposes and forwards to its dependees. This is a **build-only** rule. The combined traceability test (FM + FTA root causes) is owned by the ``dependability_analysis`` that wraps @@ -411,24 +569,25 @@ def safety_analysis( name: Target name. arch_design: ``architectural_design`` target for traceability; its ``public_api`` is what ``FailureMode.interface`` references. + fault_trees: ``fault_trees`` target providing the root causes. + failure_modes: ``failure_modes`` targets whose records are rendered and + traced. Every failure mode a root cause references must be listed. + safety_measures: ``assumptions_of_use`` / ``component_requirements`` + targets and ``Mitigation`` ``.trlc`` files addressing the root + causes. Only the own records of a target are rendered and traced; + the targets in its ``deps`` are used to resolve references. Other + record types in a ``.trlc`` file are rejected. spec: TRLC model specification files (``.rsl``) for resolving imports. Defaults to the S-CORE requirements model. Override only when using a custom TRLC schema. - failuremodes: Failure mode ``.trlc`` source files. - safetymeasures: Safety measure (``Mitigation``/``AoU``/``CompReq``) ``.trlc`` source files. - root_causes: Optional FTA PlantUML diagram files (``.puml`` / - ``.plantuml``) representing the root causes of failure modes. - fta_package: TRLC package name for the generated ``fta_events.trlc`` - stub file. Defaults to a sanitized form of ``name``. **kwargs: Additional arguments (e.g. ``visibility``, ``tags``). """ _safety_analysis( name = name, spec = spec, - failuremodes = failuremodes, - safetymeasures = safetymeasures, - root_causes = root_causes, - fta_package = fta_package or "", + fault_trees = fault_trees, + failure_modes = failure_modes, + safety_measures = safety_measures, arch_design = arch_design, **kwargs ) diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 196f299b..7f4c10c4 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -123,13 +123,28 @@ AnalysisInfo = provider( ) SafetyAnalysisProviderInfo = provider( - doc = """Marker provider on safety_analysis targets. + doc = "Provider on safety_analysis targets: the AoU targets the analysis consumes as safety measures.", + fields = { + "aou_targets": "List of the assumptions_of_use Targets passed as `safety_measures`; a dependable_element derives its own AoUs from them.", + }, +) - Requirement targets that list a safety_analysis target in ``deps`` (only to - resolve its TRLC symbols) use it to leave that target's docs out of their - own Sphinx tree. - """, - fields = {}, +FailureModesInfo = provider( + doc = "Provider on failure_modes targets: the FailureMode records and their traceability file.", + fields = { + "srcs": "Depset of .lobster traceability files generated from the FailureMode TRLC sources.", + "name": "Name of the failure_modes target.", + }, +) + +FaultTreesInfo = provider( + doc = "Provider on fault_trees targets: the root causes generated from the fault-tree diagrams.", + fields = { + "fta_package": "TRLC package name of the generated fta_events.trlc.", + "fta_events": "File: generated fta_events.trlc (RootCause records).", + "diagrams": "List of authored fault-tree .puml Files.", + "lobster_files": "Dict mapping the canonical lobster file name ('fta_root_causes.lobster') to its File.", + }, ) AssumptionsOfUseInfo = provider( @@ -245,7 +260,8 @@ DependabilityAnalysisInfo = provider( "dfa": "Depset of DFA documentation files (placeholder).", "arch_design": "ArchitecturalDesignInfo from the linked architectural design (placeholder).", "name": "Name of the dependability analysis target.", - "lobster_files": "Dict mapping canonical lobster file names to File objects collected from sub-analyses only (FM, CM, RC).", + "lobster_files": "Dict mapping canonical lobster file names to lists of File objects collected from all sub-analyses (FM, CM, RC).", + "aou_targets": "List of the assumptions_of_use Targets consumed by the safety_analysis targets, deduplicated by label.", }, ) diff --git a/bazel/rules/rules_score/rules_score.bzl b/bazel/rules/rules_score/rules_score.bzl index 85b0ae42..5b48a32b 100644 --- a/bazel/rules/rules_score/rules_score.bzl +++ b/bazel/rules/rules_score/rules_score.bzl @@ -45,6 +45,10 @@ load( "//bazel/rules/rules_score/private:dependable_element.bzl", _dependable_element = "dependable_element", ) +load( + "//bazel/rules/rules_score/private:failure_modes.bzl", + _failure_modes = "failure_modes", +) load( "//bazel/rules/rules_score/private:feature_requirements.bzl", _feature_requirements = "feature_requirements", @@ -59,6 +63,7 @@ load( ) load( "//bazel/rules/rules_score/private:safety_analysis.bzl", + _fault_trees = "fault_trees", _safety_analysis = "safety_analysis", ) load( @@ -79,7 +84,9 @@ assumptions_of_use = _assumptions_of_use assumed_system_requirements = _assumed_system_requirements component_requirements = _component_requirements dependability_analysis = _dependability_analysis +failure_modes = _failure_modes feature_requirements = _feature_requirements +fault_trees = _fault_trees glossary = _glossary filter_execpath = _filter_execpath safety_analysis = _safety_analysis 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 042da0d6..a3c43c0e 100644 --- a/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py +++ b/bazel/rules/rules_score/src/aou_forwarding_to_lobster.py @@ -34,6 +34,8 @@ import argparse import logging +import re +from dataclasses import dataclass from pathlib import Path import yaml @@ -54,21 +56,45 @@ } -def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: +@dataclass(frozen=True) +class ForwardingEntry: + """One entry of the forwarding YAML.""" + + aou_id: str + justification: str + line: int = 1 + + +_VERSIONED_ID = re.compile(r"^[^@\s]+@\d+$") + + +def _entry_lines(root: yaml.Node | None) -> list[int]: + if not isinstance(root, yaml.MappingNode): + return [] + for key, value in root.value: + if isinstance(key, yaml.ScalarNode) and key.value == "forwarded_aous" and isinstance(value, yaml.SequenceNode): + return [node.start_mark.line + 1 for node in value.value] + return [] + + +def parse_forwarding_yaml(yaml_path: str) -> list[ForwardingEntry]: """Parse the AoU forwarding YAML file. Args: yaml_path: Path to the YAML file. Returns: - List of dicts with 'aou_id' and 'justification' keys. + The entries in file order. Raises: - SystemExit: If YAML is malformed or missing required fields. + SystemExit: If YAML is malformed, an entry lacks a field, an 'aou_id' + has no version, or an 'aou_id' is listed twice. """ try: with open(yaml_path, encoding="utf-8") as f: - data = yaml.safe_load(f) + text = f.read() + root = yaml.compose(text, Loader=yaml.SafeLoader) + data = yaml.safe_load(text) except (OSError, yaml.YAMLError) as e: raise SystemExit(f"Failed to parse YAML {yaml_path}: {e}") from e @@ -79,7 +105,9 @@ def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: if not isinstance(entries, list): raise SystemExit(f"YAML {yaml_path}: 'forwarded_aous' must be a list.") - result = [] + lines = _entry_lines(root) + result: list[ForwardingEntry] = [] + seen: dict[str, int] = {} 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'.") @@ -91,7 +119,15 @@ def parse_forwarding_yaml(yaml_path: str) -> list[dict[str, str]]: 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}) + if not isinstance(aou_id, str) or not _VERSIONED_ID.match(aou_id): + raise SystemExit( + f"YAML {yaml_path}: entry {i} (aou_id='{aou_id}') must name the AoU version, e.g. 'Pkg.Name@1', " + "so that the justification is reviewed again when the AoU changes." + ) + if aou_id in seen: + raise SystemExit(f"YAML {yaml_path}: aou_id '{aou_id}' is listed in entries {seen[aou_id]} and {i}.") + seen[aou_id] = i + result.append(ForwardingEntry(aou_id, justification, lines[i] if i < len(lines) else 1)) logger.info("Parsed %d forwarding entr%s from %s", len(result), "y" if len(result) == 1 else "ies", yaml_path) return result @@ -123,18 +159,16 @@ def load_lobster_items(lobster_paths: list[str]) -> list[Requirement]: def _match_forwarded_entries( - forwarding_entries: list[dict[str, str]], + forwarding_entries: list[ForwardingEntry], lobster_items: list[Requirement], -) -> list[tuple[dict[str, str], Requirement]]: +) -> list[tuple[ForwardingEntry, 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". - The YAML can reference either the full versioned ID or the base name - (without @version suffix). + An entry matches the item whose tag (including '@version') equals its + 'aou_id'. Args: - forwarding_entries: Parsed YAML entries with 'aou_id' fields. + forwarding_entries: Parsed YAML entries. lobster_items: All lobster items from received AoU files. Returns: @@ -143,45 +177,34 @@ def _match_forwarded_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), 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]] = {} + item_by_id = {f"{item.tag.tag}@{item.tag.version}": item for item in lobster_items if item.tag.version} + versions_by_base_id: dict[str, set[int]] = {} for item in lobster_items: - 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())) + versions_by_base_id.setdefault(item.tag.tag, set()).add(item.tag.version) 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)" + item = item_by_id.get(entry.aou_id) + if item is None: + base_id = entry.aou_id.rsplit("@", 1)[0] hint = "" - if aou_id in base_id_versions and len(base_id_versions[aou_id]) > 1: - hint = " (ambiguous: multiple versions received -- specify '@version' explicitly)" + if base_id in versions_by_base_id: + received = ", ".join(str(v) for v in sorted(versions_by_base_id[base_id])) + hint = f" (received version(s): {received}; review the justification and update the version)" + available = ", ".join(sorted(item_by_id)) if item_by_id else "(none)" raise SystemExit( - f"AoU ID '{aou_id}' listed in forwarding YAML not found in received AoUs.{hint} Available IDs: {available}" + f"AoU ID '{entry.aou_id}' listed in forwarding YAML not found in received AoUs.{hint} " + f"Available IDs: {available}" ) - matched.append((entry, item_by_id[aou_id])) + matched.append((entry, item)) logger.info("Matched %d/%d forwarding entries to received AoU items", len(matched), len(forwarding_entries)) return matched def filter_forwarded_aous( - forwarding_entries: list[dict[str, str]], + forwarding_entries: list[ForwardingEntry], lobster_items: list[Requirement], ) -> list[Requirement]: """Filter lobster items to only those listed in the forwarding YAML. @@ -205,7 +228,7 @@ def filter_forwarded_aous( def build_forwarded_markers( - forwarding_entries: list[dict[str, str]], + forwarding_entries: list[ForwardingEntry], lobster_items: list[Requirement], yaml_path: str, ) -> list[Requirement]: @@ -233,14 +256,13 @@ def build_forwarded_markers( """ 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), + tag=Tracing_Tag("req", f"{item.tag.tag}__forwarded"), + location=File_Reference(yaml_path, line=entry.line), framework="AoUForwarding", kind="ForwardedAoU", - name=aou_id, - text=entry["justification"], + name=item.tag.tag, + text=entry.justification, ) marker.add_tracing_target(item.tag) markers.append(marker) @@ -257,9 +279,9 @@ def main() -> None: ) parser.add_argument( "--input-lobster", - nargs="+", + nargs="*", required=True, - help="One or more .lobster files received from deps containing AoU entries.", + help="The received AoU .lobster file(s) holding the AoUs this element can forward.", ) parser.add_argument( "--output", diff --git a/bazel/rules/rules_score/src/merge_lobster_items.py b/bazel/rules/rules_score/src/merge_lobster_items.py new file mode 100644 index 00000000..0d30568c --- /dev/null +++ b/bazel/rules/rules_score/src/merge_lobster_items.py @@ -0,0 +1,123 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Merge lobster files into one, collapsing duplicate items. + +One item reaches a dependable_element along several paths, e.g. an AoU through +a direct dependency and through a dependency that forwards it, or an AoU +listed by several safety analyses. Each path is a separate file carrying an +item with the same tag, which lobster rejects as a duplicate definition. Items +with the same tag and the same definition (location aside) are kept once; items +with the same tag but a different definition, such as two versions of one AoU, +are an error. +""" + +from __future__ import annotations + +import argparse +import logging +from pathlib import Path + +from lobster.common.errors import LOBSTER_Error, Message_Handler +from lobster.common.io import lobster_read, lobster_write +from lobster.common.items import Requirement + +GENERATOR = "merge_lobster_items" + +# Not part of an item's definition: where it was found, and report-time state. +_NON_DEFINITION_FIELDS = frozenset({"location", "messages", "ref_up", "ref_down", "tracing_status"}) + +logger = logging.getLogger(__name__) + +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + + +def _content(item: Requirement) -> dict: + return {key: value for key, value in item.to_json().items() if key not in _NON_DEFINITION_FIELDS} + + +def merge_items(lobster_paths: list[str]) -> list[Requirement]: + """Merge the items of all files; the first file defining a tag wins. + + Args: + lobster_paths: Paths to the .lobster files, origin files first. + + Returns: + Items in order of first appearance, one per tag. + + Raises: + SystemExit: If a file cannot be read, or two files define the same tag + with a different definition. + """ + mh = Message_Handler() + merged: dict[str, Requirement] = {} + origin: dict[str, str] = {} + for path in lobster_paths: + items: dict[str, Requirement] = {} + try: + lobster_read(mh, path, "merged", items) + except (OSError, LOBSTER_Error) as e: + raise SystemExit(f"Failed to parse lobster file {path}: {e}") from e + for key, item in items.items(): + known = merged.get(key) + if known is None: + merged[key] = item + origin[key] = path + continue + known_content, content = _content(known), _content(item) + if known_content != content: + differing = sorted( + f for f in known_content.keys() | content.keys() if known_content.get(f) != content.get(f) + ) + raise SystemExit( + f"Conflicting definitions of '{key}' in {origin[key]} and {path}: {', '.join(differing)} differ." + ) + logger.info("Dropped duplicate of '%s' from %s", key, path) + return list(merged.values()) + + +def main() -> None: + """Entry point for the lobster item merge tool.""" + parser = argparse.ArgumentParser(description="Merge lobster files, collapsing duplicate items.") + parser.add_argument( + "--input-lobster", + nargs="+", + required=True, + help="Lobster files to merge; on a duplicate tag the first file wins.", + ) + parser.add_argument("--output", required=True, help="Output .lobster file path.") + 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") + + merged = merge_items(args.input_lobster) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + lobster_write(f, Requirement, GENERATOR, merged) + logger.info("Wrote %d item(s) to %s", len(merged), output_path) + + +if __name__ == "__main__": + main() diff --git a/bazel/rules/rules_score/src/rst_to_trlc.py b/bazel/rules/rules_score/src/rst_to_trlc.py index 59e429d3..8b295a0c 100644 --- a/bazel/rules/rules_score/src/rst_to_trlc.py +++ b/bazel/rules/rules_score/src/rst_to_trlc.py @@ -67,6 +67,7 @@ "safety", # → safety "satisfies", # → derived_from cross-reference "derived_from", # → derived_from cross-reference + "root_causes", # → root_causes (AoU only) "rationale", # → rationale (mandatory for AssumedSystemReq) "version", # → version } @@ -75,12 +76,16 @@ # TRLC types that require a rationale field. _ASSUMED_SYSTEM_REQ_TYPES = {"ScoreReq.AssumedSystemReq"} +# TRLC types that require a root_causes field. +_SAFETY_MEASURE_TYPES = {"ScoreReq.AoU"} + _DEFAULT_SAFETY = "QM" _DEFAULT_VERSION = "1" _DEFAULT_REF_PACKAGE = "TODO_PACKAGE" _IMPORTS = ["ScoreReq"] _RE_MARKUP = re.compile(r"\*\*?(.*?)\*\*?") +_RE_QUALIFIED_ROOT_CAUSE = re.compile(r"^[A-Za-z_]\w*\.[A-Za-z_]\w*$") _RE_DIRECTIVE = re.compile(r"^\.\.\s+([\w]+)::\s*(.*)") _RE_FIELD = re.compile(r"^\s+:([\w]+):\s*(.*)") # noqa: E501 @@ -140,6 +145,37 @@ def _collect_refs(fields: dict[str, str]) -> list[str]: return [r.strip() for k in _REF_FIELDS if k in fields for r in fields[k].split(",") if r.strip()] +def _collect_root_causes(fields: dict[str, str]) -> list[str]: + """Extract the fully qualified ``Package.RootCause`` names of the root_causes field.""" + return [r.strip() for r in fields.get("root_causes", "").split(",") if r.strip()] + + +def _record_name(item: dict[str, Any]) -> str: + """TRLC record name of a parsed directive.""" + return item["fields"].get("id") or re.sub(r"\W+", "_", item["title"]).strip("_") + + +def _require_root_causes(name: str, fields: dict[str, str]) -> list[str]: + """Return the root causes of AoU *name*; raise ValueError if missing or not ``Package.RootCause``.""" + root_causes = _collect_root_causes(fields) + if not root_causes: + raise ValueError(f"AoU '{name}': missing mandatory ':root_causes:' field") + for root_cause in root_causes: + if not _RE_QUALIFIED_ROOT_CAUSE.match(root_cause): + raise ValueError(f"AoU '{name}': root cause '{root_cause}' is not of the form '.'") + return root_causes + + +def _root_causes_of(item: dict[str, Any]) -> list[str]: + """Root causes of a directive: required for safety measures, rejected elsewhere.""" + name = _record_name(item) + if DIRECTIVE_TO_TRLC[item["directive"]] in _SAFETY_MEASURE_TYPES: + return _require_root_causes(name, item["fields"]) + if "root_causes" in item["fields"]: + raise ValueError(f"{item['directive']} '{name}': ':root_causes:' is only supported on 'aou_req'") + return [] + + def parse_directives(content: str) -> list[dict[str, Any]]: """Parse supported requirement directives from RST content.""" results: list[dict[str, Any]] = [] @@ -168,13 +204,19 @@ def render_trlc(directives: list[dict[str, Any]], package: str, ref_package: str imports = list(_IMPORTS) if has_refs and ref_package and ref_package != _DEFAULT_REF_PACKAGE and ref_package not in imports: imports.append(ref_package) + root_causes_by_record = [_root_causes_of(item) for item in directives] + for root_causes in root_causes_by_record: + for root_cause in root_causes: + root_cause_package = root_cause.rsplit(".", 1)[0] + if root_cause_package not in imports and root_cause_package != package: + imports.append(root_cause_package) import_lines = [f"import {name}" for name in imports] lines_out = [_TRLC_HEADER, f"package {package}", "", *import_lines, ""] - for item in directives: + for item, root_causes in zip(directives, root_causes_by_record): fields = item["fields"] trlc_type = DIRECTIVE_TO_TRLC[item["directive"]] - name = fields.get("id") or re.sub(r"\W+", "_", item["title"]).strip("_") + name = _record_name(item) safety = SAFETY_MAP.get( fields.get("safety", _DEFAULT_SAFETY).upper(), SAFETY_MAP[_DEFAULT_SAFETY], @@ -190,6 +232,9 @@ def render_trlc(directives: list[dict[str, Any]], package: str, ref_package: str ref_list = ", ".join(f"{ref_package}.{r}@1" for r in refs) lines_out.append(f" derived_from = [{ref_list}]") + if trlc_type in _SAFETY_MEASURE_TYPES: + lines_out.append(f" root_causes = [{', '.join(root_causes)}]") + if trlc_type in _ASSUMED_SYSTEM_REQ_TYPES: rationale = fields.get("rationale", "TODO: add rationale") lines_out.append(f' rationale = "{_escape(rationale)}"') @@ -238,5 +283,8 @@ def convert( if not args.input_file.exists(): sys.exit(f"ERROR: file not found: {args.input_file}") output_file = args.output_dir / (args.input_file.stem + ".trlc") - record_count = convert(args.input_file, output_file, package=args.package, ref_package=args.ref_package) + try: + record_count = convert(args.input_file, output_file, package=args.package, ref_package=args.ref_package) + except ValueError as e: + sys.exit(f"ERROR: {args.input_file}: {e}") logging.info("%s -> %s (%d record(s))", args.input_file, output_file, record_count) diff --git a/bazel/rules/rules_score/src/safety_analysis_assembler.py b/bazel/rules/rules_score/src/safety_analysis_assembler.py index 8377da61..ab69a6c4 100644 --- a/bazel/rules/rules_score/src/safety_analysis_assembler.py +++ b/bazel/rules/rules_score/src/safety_analysis_assembler.py @@ -34,6 +34,7 @@ import argparse import dataclasses import logging +import os import re import sys @@ -331,6 +332,30 @@ def _render_measures(renderer: TRLCRST, obj_map: dict, measure_fqns: list[str]) return _heading(_SAFETY_MEASURES_TITLE, "-") + "\n" + body + "\n" +def _check_inputs(obj_map: dict, mitigation_files: list[str]) -> list[str]: + """Input errors: a root cause whose failure mode is not rendered, or a + non-Mitigation record in a ``--mitigations`` file. + + Only records of the source files are in *obj_map*; a failure mode parsed for + reference resolution only is therefore reported. + """ + errors = [] + fm_fqns = {fqn for fqn, obj in obj_map.items() if obj.n_typ.name == "FailureMode"} + mitigation_paths = {os.path.abspath(f) for f in mitigation_files} + for fqn, obj in obj_map.items(): + kind = obj.n_typ.name + if kind == "RootCause": + for fm_fqn in obj.to_python_dict().get("failure_modes") or []: + if fm_fqn not in fm_fqns: + errors.append(f"RootCause {fqn} references failure mode {fm_fqn}, which is not in failure_modes") + if kind != "Mitigation" and os.path.abspath(obj.location.file_name) in mitigation_paths: + errors.append( + f"{kind} {fqn} is in a mitigations file; only Mitigation records belong there " + "(pass AoU / CompReq records via an assumptions_of_use / component_requirements target)" + ) + return errors + + def _build_body(renderer: TRLCRST, title: str, fta_package: str, diagram_names: list[str]) -> str: obj_map = renderer.objects_by_fqn() fm_fqns = [fqn for fqn, obj in obj_map.items() if obj.n_typ.name == "FailureMode"] @@ -371,17 +396,24 @@ def main() -> None: help="Basenames of the fault-tree .puml diagrams staged next to the page; each is shown once.", ) parser.add_argument("--failuremodes", nargs="*", default=[], help="FailureMode .trlc files.") + parser.add_argument( + "--mitigations", + nargs="*", + default=[], + help="Mitigation .trlc files; any other record type in them is an error.", + ) parser.add_argument( "--safetymeasures", nargs="*", default=[], - help="Mitigation/AoU/CompReq .trlc files scoped to this safety_analysis target.", + help="AoU/CompReq .trlc files of the measure targets scoped to this safety_analysis target.", ) parser.add_argument( - "--spec", + "--dep-files", nargs="*", default=[], - help="TRLC .rsl/.trlc spec files for import resolution.", + dest="dep_files", + help="TRLC .rsl/.trlc files parsed for import resolution only; their records are not rendered.", ) parser.add_argument( "--log-level", @@ -394,11 +426,11 @@ def main() -> None: logging.basicConfig(level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s") - source_files = list(args.failuremodes) + list(args.safetymeasures) + [args.fta_events] + source_files = list(args.failuremodes) + list(args.mitigations) + list(args.safetymeasures) + [args.fta_events] renderer = TRLCRST( input_directory=None, source_files=source_files, - dep_files=list(args.spec), + dep_files=list(args.dep_files), ) try: renderer.parse_trlc_files() @@ -406,6 +438,12 @@ def main() -> None: logger.error("TRLC parse error: %s", exc) sys.exit(1) + errors = _check_inputs(renderer.objects_by_fqn(), args.mitigations) + if errors: + for error in errors: + logger.error("%s", error) + sys.exit(1) + body = _build_body(renderer, args.title, args.fta_package, args.diagrams) with open(args.template, encoding="utf-8", newline="") as fh: diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index f2c2db62..69427643 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -25,6 +25,8 @@ load( "component_requirements", "dependability_analysis", "dependable_element", + "failure_modes", + "fault_trees", "feature_requirements", "safety_analysis", "sphinx_module", @@ -34,6 +36,12 @@ load( load("@score_tooling//bazel/rules/rules_score:sphinx_toolchain.bzl", "score_sphinx_toolchain") load("@score_tooling//cpp/libclang:libclang_toolchain.bzl", "libclang_toolchain") load("@trlc//:trlc.bzl", "trlc_requirements", "trlc_requirements_test", "trlc_specification") +load( + ":dependable_element_aou_test.bzl", + "aou_derived_test", + "aou_merged_test", + "aou_none_test", +) load( ":dependable_element_staged_path_collision_test.bzl", "cross_label_collision_fails_test", @@ -84,6 +92,13 @@ load( "feat_req_rst_sphinx_test", "requirements_rst_test_suite", ) +load( + ":safety_analysis_measures_test.bzl", + "aou_in_aou_deps_fails_test", + "fta_default_package_test", + "measure_deps_are_context_only_test", + "measure_spec_is_merged_test", +) load( ":score_module_providers_test.bzl", "config_auto_generation_test", @@ -264,10 +279,8 @@ component_requirements( ) # - Assumptions of Use: wp__requirements_comp_aou -assumptions_of_use( - name = "aous", - srcs = ["fixtures/seooc_test/assumptions_of_use.rst"], -) +# (AoUs reach a dependable_element as `safety_measures` of its safety_analysis; see +# :aou_root_cause_aous.) # - Architecture Design: wp__component_arch architectural_design( @@ -355,7 +368,6 @@ stem_collision_fails_test( dependable_element( name = "staged_path_same_label_collision_repro", architectural_design = [], - assumptions_of_use = [":aous"], checklists = ["//fixtures/staged_path_collision:dup_srcs_aux"], components = [], dependability_analysis = [":dependability_analysis_target"], @@ -381,7 +393,6 @@ same_label_collision_fails_test( dependable_element( name = "staged_path_cross_label_collision_repro", architectural_design = [], - assumptions_of_use = [":aous"], checklists = [ "//fixtures/staged_path_collision/pkg_a:dup", "//fixtures/staged_path_collision/pkg_b:dup", @@ -416,7 +427,6 @@ dependable_element( ":arch_design_authored_rst_override_repro", ":arch_design_diagram_free_repro", ], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -437,13 +447,20 @@ dependability_analysis( safety_analysis = [":samplelibrary_safety_analysis"], ) +fault_trees( + name = "samplelibrary_fault_trees", + # TODO: add failure modes and root causes here + # srcs = [], + # deps = [], +) + safety_analysis( name = "samplelibrary_safety_analysis", - # TODO: add failure modes, safety measures, and root causes here - # failuremodes = [], - # safetymeasures = [], # can be AoUs or requirements - # root_causes = [], + # TODO: add failure modes and safety measures here + # failure_modes = [], + # safety_measures = [], # AoUs, requirements, Mitigation files arch_design = ":arch_design", + fault_trees = ":samplelibrary_fault_trees", ) architectural_design( @@ -472,7 +489,6 @@ dependable_element( ":arch_design_clickable_example_overview", ":arch_design_clickable_example_proxy_detail", ], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -507,7 +523,6 @@ architectural_design( dependable_element( name = "interface_example_lib", architectural_design = [":arch_design_interface_example"], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -537,7 +552,6 @@ architectural_design( dependable_element( name = "class_example_lib", architectural_design = [":arch_design_class_example"], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -570,11 +584,22 @@ architectural_design( # dependability_analysis_target/samplelibrary_safety_analysis fixture used by # other targets in this file is deliberately empty, so this scenario gets its # own minimal safety analysis that traces to "package_pub.PublicInterface". +failure_modes( + name = "public_api_example_failure_modes", + srcs = ["fixtures/clickable_example/public_api_failure_modes.trlc"], +) + +fault_trees( + name = "public_api_example_fault_trees", + srcs = ["fixtures/clickable_example/public_api_fta.puml"], + deps = [":public_api_example_failure_modes"], +) + safety_analysis( name = "public_api_example_safety_analysis", arch_design = ":arch_design_public_api_example", - failuremodes = ["fixtures/clickable_example/public_api_failure_modes.trlc"], - root_causes = ["fixtures/clickable_example/public_api_fta.puml"], + failure_modes = [":public_api_example_failure_modes"], + fault_trees = ":public_api_example_fault_trees", ) dependability_analysis( @@ -589,11 +614,22 @@ dependability_analysis( # lobster-trlc accepts the resulting RootCause.failure_modes fan-out. Reuses # :arch_design_public_api_example so the failure modes' `interface` field has # a real public_api item to trace up to (see :public_api_example_safety_analysis above). +failure_modes( + name = "multi_failure_mode_example_failure_modes", + srcs = ["fixtures/clickable_example/multi_failure_mode_failure_modes.trlc"], +) + +fault_trees( + name = "multi_failure_mode_example_fault_trees", + srcs = ["fixtures/clickable_example/multi_failure_mode_fta.puml"], + deps = [":multi_failure_mode_example_failure_modes"], +) + safety_analysis( name = "multi_failure_mode_example_safety_analysis", arch_design = ":arch_design_public_api_example", - failuremodes = ["fixtures/clickable_example/multi_failure_mode_failure_modes.trlc"], - root_causes = ["fixtures/clickable_example/multi_failure_mode_fta.puml"], + failure_modes = [":multi_failure_mode_example_failure_modes"], + fault_trees = ":multi_failure_mode_example_fault_trees", ) dependability_analysis( @@ -604,22 +640,33 @@ dependability_analysis( # Regression fixture: an AoU referencing a generated fault-tree root cause # (`AouRootCauseFta.UserProvidedWrongConfiguration`). The AoU file is parsed -# standalone by assumptions_of_use, so the safety_analysis target must be in its -# `deps` to make the generated package resolvable; passing the same file as -# `safetymeasures` makes the AoU count towards root-cause coverage. -safety_analysis( - name = "aou_root_cause_safety_analysis", - arch_design = ":arch_design_public_api_example", - failuremodes = ["fixtures/aou_root_cause/failure_modes.trlc"], +# standalone by assumptions_of_use, so the fault_trees target must be in its +# `deps` to make the generated package resolvable; passing the AoU target as +# `safety_measures` makes the AoU count towards root-cause coverage. +failure_modes( + name = "aou_root_cause_failure_modes", + srcs = ["fixtures/aou_root_cause/failure_modes.trlc"], +) + +fault_trees( + name = "aou_root_cause_fault_trees", + srcs = ["fixtures/aou_root_cause/fta.puml"], fta_package = "AouRootCauseFta", - root_causes = ["fixtures/aou_root_cause/fta.puml"], - safetymeasures = ["fixtures/aou_root_cause/aou.trlc"], + deps = [":aou_root_cause_failure_modes"], ) assumptions_of_use( name = "aou_root_cause_aous", srcs = ["fixtures/aou_root_cause/aou.trlc"], - deps = [":aou_root_cause_safety_analysis"], + deps = [":aou_root_cause_fault_trees"], +) + +safety_analysis( + name = "aou_root_cause_safety_analysis", + arch_design = ":arch_design_public_api_example", + failure_modes = [":aou_root_cause_failure_modes"], + fault_trees = ":aou_root_cause_fault_trees", + safety_measures = [":aou_root_cause_aous"], ) dependability_analysis( @@ -628,10 +675,66 @@ dependability_analysis( safety_analysis = [":aou_root_cause_safety_analysis"], ) +# A component requirement measure derives from the AoU held in its deps; the +# AoU is only used to resolve the reference, it is not a measure of the analysis. +component_requirements( + name = "comp_req_measure_reqs", + srcs = ["fixtures/comp_req_measure/comp_req.trlc"], + deps = [ + ":aou_root_cause_aous", + ":aou_root_cause_fault_trees", + ], +) + +safety_analysis( + name = "comp_req_measure_safety_analysis", + arch_design = ":arch_design_public_api_example", + failure_modes = [":aou_root_cause_failure_modes"], + fault_trees = ":aou_root_cause_fault_trees", + safety_measures = [":comp_req_measure_reqs"], +) + +measure_deps_are_context_only_test( + name = "measure_deps_are_context_only_test", + target_under_test = ":comp_req_measure_safety_analysis", +) + +build_test( + name = "comp_req_measure_builds", + targets = [":comp_req_measure_safety_analysis"], +) + +# A measure target with its own spec: the assembler needs the extra RSL to parse it. +safety_analysis( + name = "measure_spec_safety_analysis", + arch_design = ":arch_design_public_api_example", + failure_modes = [":aou_root_cause_failure_modes"], + fault_trees = ":aou_root_cause_fault_trees", + safety_measures = ["//fixtures/spec_extras:spec_extras_comp_req"], +) + +measure_spec_is_merged_test( + name = "measure_spec_is_merged_test", + target_under_test = ":measure_spec_safety_analysis", +) + +# An assumptions_of_use target must not carry another one in its deps: the +# deps' AoUs would be extracted as its own. +assumptions_of_use( + name = "aou_in_aou_deps_repro", + srcs = ["fixtures/aou_root_cause/aou.trlc"], + tags = ["manual"], + deps = [":aou_root_cause_aous"], +) + +aou_in_aou_deps_fails_test( + name = "aou_in_aou_deps_fails_test", + target_under_test = ":aou_in_aou_deps_repro", +) + dependable_element( name = "public_api_example_lib", architectural_design = [":arch_design_public_api_example"], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":public_api_example_dependability_analysis"], integrity_level = "B", @@ -652,7 +755,6 @@ dependable_element( name = "comp_req_root_cause_example_lib", testonly = True, architectural_design = [":arch_design_public_api_example"], - assumptions_of_use = [":aous"], components = [":test_component"], dependability_analysis = [":public_api_example_dependability_analysis"], integrity_level = "B", @@ -680,7 +782,6 @@ architectural_design( dependable_element( name = "unit_example_lib", architectural_design = [":arch_design_unit_example"], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -743,7 +844,6 @@ dependable_element( name = "namespace_unit_example_lib", testonly = True, architectural_design = [":arch_design_namespace_unit_example"], - assumptions_of_use = [":aous"], components = [":namespace_unit_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -810,7 +910,6 @@ dependable_element( name = "leaf_unit_example_lib", testonly = True, architectural_design = [":arch_design_leaf_unit_example"], - assumptions_of_use = [":aous"], components = [":leaf_unit_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -868,11 +967,22 @@ architectural_design( # safety analysis (see :public_api_example_safety_analysis above for the same reasoning) # - the shared dependability_analysis_target fixture is deliberately empty, so # this scenario gets its own minimal safety analysis that traces to "TaskInterface". +failure_modes( + name = "dynamic_example_failure_modes", + srcs = ["fixtures/clickable_example/dynamic_public_api_failure_modes.trlc"], +) + +fault_trees( + name = "dynamic_example_fault_trees", + srcs = ["fixtures/clickable_example/dynamic_public_api_fta.puml"], + deps = [":dynamic_example_failure_modes"], +) + safety_analysis( name = "dynamic_example_safety_analysis", arch_design = ":arch_design_dynamic_example", - failuremodes = ["fixtures/clickable_example/dynamic_public_api_failure_modes.trlc"], - root_causes = ["fixtures/clickable_example/dynamic_public_api_fta.puml"], + failure_modes = [":dynamic_example_failure_modes"], + fault_trees = ":dynamic_example_fault_trees", ) dependability_analysis( @@ -884,7 +994,6 @@ dependability_analysis( dependable_element( name = "dynamic_example_lib", architectural_design = [":arch_design_dynamic_example"], - assumptions_of_use = [":aous"], components = [], dependability_analysis = [":dynamic_example_dependability_analysis"], integrity_level = "B", @@ -926,7 +1035,6 @@ component( dependable_element( name = "dep_seooc_lib", architectural_design = [":arch_design_dep"], - assumptions_of_use = [":aous"], components = [":dep_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -970,7 +1078,6 @@ component( dependable_element( name = "design_name_example_lib", architectural_design = [":arch_design_design_name_example"], - assumptions_of_use = [":aous"], components = [":design_name_component_bazel_target"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1004,7 +1111,6 @@ component( dependable_element( name = "seooc_test_lib", architectural_design = [":arch_design"], - assumptions_of_use = [":aous"], components = [":seooc_test_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1117,7 +1223,6 @@ dependable_element( name = "test_dependable_element", testonly = True, architectural_design = [":arch_design_elem"], - assumptions_of_use = [":aous"], components = [":test_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1133,7 +1238,6 @@ dependable_element( name = "test_dependable_element_coverage_release", testonly = True, architectural_design = [":arch_design_elem"], - assumptions_of_use = [":aous"], components = [":test_component_with_test_case_coverage_lock"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1147,7 +1251,6 @@ dependable_element( name = "test_dependable_element_coverage_development", testonly = True, architectural_design = [":arch_design_elem"], - assumptions_of_use = [":aous"], components = [":test_component_with_test_case_coverage_lock"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1157,6 +1260,115 @@ dependable_element( tests = [], # Empty for testing ) +# The AoUs of a dependable_element are the `safety_measures` of its safety analyses. +dependable_element( + name = "aou_derived_example_lib", + architectural_design = [":arch_design_public_api_example"], + components = [], + dependability_analysis = [":aou_root_cause_dependability_analysis"], + integrity_level = "B", + # Same reasoning as public_api_example_lib above. + maturity = "development", + requirements = [":feat_req"], + tests = [], + deps = [], +) + +aou_derived_test( + name = "aou_derived_test", + target_under_test = ":aou_derived_example_lib_index", +) + +# Two safety analyses (under two dependability analyses) consuming the same +# AoU target contribute it once. +safety_analysis( + name = "aou_root_cause_safety_analysis_dup", + arch_design = ":arch_design_public_api_example", + failure_modes = [":aou_root_cause_failure_modes"], + fault_trees = ":aou_root_cause_fault_trees", + safety_measures = [":aou_root_cause_aous"], +) + +dependability_analysis( + name = "aou_root_cause_dependability_analysis_dup", + arch_design = ":arch_design_public_api_example", + safety_analysis = [":aou_root_cause_safety_analysis_dup"], +) + +dependable_element( + name = "aou_deduplicated_example_lib", + architectural_design = [":arch_design_public_api_example"], + components = [], + dependability_analysis = [ + ":aou_root_cause_dependability_analysis", + ":aou_root_cause_dependability_analysis_dup", + ], + integrity_level = "B", + maturity = "development", + requirements = [":feat_req"], + tags = ["manual"], + tests = [], + deps = [], +) + +aou_derived_test( + name = "aou_deduplicated_test", + target_under_test = ":aou_deduplicated_example_lib_index", +) + +build_test( + name = "aou_deduplicated_builds", + targets = [":aou_deduplicated_example_lib_index"], +) + +# A dependable_element whose safety analyses have no AoU measure has no AoUs. +aou_none_test( + name = "aou_none_test", + target_under_test = ":public_api_example_lib_index", +) + +# Diamond: the AoU of aou_derived_example_lib reaches aou_diamond_top directly +# and chain-forwarded through aou_diamond_mid; it is received once. +dependable_element( + name = "aou_diamond_mid", + aou_forwarding = "fixtures/aou_root_cause/forwarding.yaml", + architectural_design = [":arch_design_public_api_example"], + components = [], + dependability_analysis = [], + integrity_level = "B", + maturity = "development", + requirements = [":feat_req"], + tags = ["manual"], + tests = [], + deps = [":aou_derived_example_lib"], +) + +dependable_element( + name = "aou_diamond_top", + architectural_design = [":arch_design_public_api_example"], + components = [], + dependability_analysis = [], + integrity_level = "B", + maturity = "development", + requirements = [":feat_req"], + tags = ["manual"], + tests = [], + deps = [ + ":aou_derived_example_lib", + ":aou_diamond_mid", + ], +) + +aou_merged_test( + name = "aou_merged_test", + target_under_test = ":aou_diamond_top_index", +) + +build_test( + name = "aou_diamond_builds", + targets = [":aou_diamond_top_index"], +) + # ============================================================================ # Test Fixtures - Nested Components for Recursive Testing # ============================================================================ @@ -1232,7 +1444,6 @@ dependable_element( name = "test_dependable_element_nested", testonly = True, architectural_design = [":arch_design_nested"], - assumptions_of_use = [":aous"], components = [":test_parent_component"], dependability_analysis = [":dependability_analysis_target"], integrity_level = "B", @@ -1775,6 +1986,7 @@ component_requirements( assumptions_of_use( name = "aous_rst", srcs = ["fixtures/rst_requirements/assumptions_of_use.rst"], + deps = [":aou_root_cause_fault_trees"], ) asr_rst_output_test( @@ -1938,6 +2150,16 @@ py_test( ], ) +py_test( + name = "test_merge_lobster_items", + size = "small", + srcs = ["test_merge_lobster_items.py"], + deps = [ + "@lobster//lobster/common", + "@score_tooling//bazel/rules/rules_score:merge_lobster_items", + ], +) + py_test( name = "test_sphinx_html_merge", size = "small", @@ -1979,17 +2201,37 @@ py_test( deps = ["@trlc//tools/trlc_rst:trlc_rst_lib"], ) +# The default `fta_package` of a fault_trees target contains its Bazel package +# path, so equally named targets of different packages do not collide. +fta_default_package_test( + name = "fta_default_package_test", + expected_package = "fixtures_fta_default_package__default_package_fault_trees_fta", + target_under_test = "//fixtures/fta_default_package:default_package_fault_trees", +) + # Combined test suite for all tests test_suite( name = "all_tests", tests = [ + ":aou_deduplicated_builds", + ":aou_deduplicated_test", + ":aou_derived_test", + ":aou_diamond_builds", + ":aou_in_aou_deps_fails_test", + ":aou_merged_test", + ":aou_none_test", + ":comp_req_measure_builds", + ":fta_default_package_test", ":lobster_config_tests", + ":measure_deps_are_context_only_test", + ":measure_spec_is_merged_test", ":requirements_multi_spec_tests", ":requirements_rst_tests", ":seooc_tests", ":sphinx_module_tests", ":test_aou_forwarding_to_lobster", ":test_bazel_sphinx_needs", + ":test_merge_lobster_items", ":test_rst_to_trlc", ":test_safety_analysis_assembler", ":test_sphinx_html_merge", diff --git a/bazel/rules/rules_score/test/dependable_element_aou_test.bzl b/bazel/rules/rules_score/test/dependable_element_aou_test.bzl new file mode 100644 index 00000000..e3c452f8 --- /dev/null +++ b/bazel/rules/rules_score/test/dependable_element_aou_test.bzl @@ -0,0 +1,79 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Analysis tests for the AoUs a dependable_element derives from its safety analyses.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@score_tooling//bazel/rules/rules_score:providers.bzl", "ForwardedAoUInfo") + +def _staged_aou_paths(target): + return [ + f.short_path + for f in target[DefaultInfo].files.to_list() + if "/assumptions_of_use/" in f.short_path + ] + +def _aou_derived_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + + asserts.equals( + env, + 1, + len(target[ForwardedAoUInfo].own_aou_lobster.to_list()), + ) + staged = [p for p in _staged_aou_paths(target) if "/assumptions_of_use/aou_root_cause_aous/" in p] + asserts.true(env, staged, "AoU measure of the safety analysis must be staged for Sphinx") + return analysistest.end(env) + +aou_derived_test = analysistest.make(_aou_derived_test_impl) + +def _aou_none_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + + asserts.equals( + env, + [], + target[ForwardedAoUInfo].own_aou_lobster.to_list(), + ) + asserts.equals(env, [], _staged_aou_paths(target)) + return analysistest.end(env) + +aou_none_test = analysistest.make(_aou_none_test_impl) + +def _aou_merged_test_impl(ctx): + env = analysistest.begin(ctx) + + merge_actions = [ + a + for a in analysistest.target_actions(env) + if a.mnemonic == "MergeLobsterItems" and a.outputs.to_list()[0].basename == "received_aous.lobster" + ] + asserts.equals(env, 1, len(merge_actions)) + if len(merge_actions) != 1: + return analysistest.end(env) + + # The AoU arrives directly and chain-forwarded: one file per path. + inputs = [ + f.basename + for f in merge_actions[0].inputs.to_list() + if f.extension == "lobster" + ] + asserts.equals( + env, + ["aou_root_cause_aous.lobster", "chain_forwarded_aous.lobster"], + sorted(inputs), + ) + return analysistest.end(env) + +aou_merged_test = analysistest.make(_aou_merged_test_impl) diff --git a/bazel/rules/rules_score/test/fixtures/aou_root_cause/aou.trlc b/bazel/rules/rules_score/test/fixtures/aou_root_cause/aou.trlc index e2092b86..f10b1de4 100644 --- a/bazel/rules/rules_score/test/fixtures/aou_root_cause/aou.trlc +++ b/bazel/rules/rules_score/test/fixtures/aou_root_cause/aou.trlc @@ -13,7 +13,7 @@ // Regression fixture: an AoU that prevents a generated fault-tree root cause // (package AouRootCauseFta). Resolving that reference needs the -// safety_analysis target in the `deps` of assumptions_of_use. +// fault_trees target in the `deps` of assumptions_of_use. package AouRootCauseAous import ScoreReq diff --git a/bazel/rules/rules_score/test/fixtures/aou_root_cause/forwarding.yaml b/bazel/rules/rules_score/test/fixtures/aou_root_cause/forwarding.yaml new file mode 100644 index 00000000..87a8fe3e --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/aou_root_cause/forwarding.yaml @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# +# Forwards the AoU of aou_derived_example_lib, which the diamond fixture +# receives a second time directly. +# +forwarded_aous: + - aou_id: "AouRootCauseAous.ConfigurationIsCorrect@1" + justification: "Handled by the dependee of this element." diff --git a/bazel/rules/rules_score/test/fixtures/comp_req_measure/comp_req.trlc b/bazel/rules/rules_score/test/fixtures/comp_req_measure/comp_req.trlc new file mode 100644 index 00000000..3fc0658e --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/comp_req_measure/comp_req.trlc @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 CompReqMeasure + +import ScoreReq +import AouRootCauseFta +import AouRootCauseAous + +ScoreReq.CompReq ValidatesConfiguration { + description = "The component shall validate its configuration" + safety = ScoreReq.Asil.B + version = 1 + derived_from = [AouRootCauseFta.UserProvidedWrongConfiguration, AouRootCauseAous.ConfigurationIsCorrect@1] +} diff --git a/bazel/rules/rules_score/test/fixtures/fta_default_package/BUILD b/bazel/rules/rules_score/test/fixtures/fta_default_package/BUILD new file mode 100644 index 00000000..67990ebe --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/fta_default_package/BUILD @@ -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 +# ******************************************************************************* + +# Sub-package fixture: a fault_trees target without `fta_package` gets a +# default package name that contains the Bazel package path. + +load("@score_tooling//bazel/rules/rules_score:rules_score.bzl", "fault_trees") + +package(default_visibility = ["//visibility:public"]) + +fault_trees( + name = "default_package_fault_trees", +) diff --git a/bazel/rules/rules_score/test/fixtures/rst_requirements/assumptions_of_use.rst b/bazel/rules/rules_score/test/fixtures/rst_requirements/assumptions_of_use.rst index d15b706f..bce5a34f 100644 --- a/bazel/rules/rules_score/test/fixtures/rst_requirements/assumptions_of_use.rst +++ b/bazel/rules/rules_score/test/fixtures/rst_requirements/assumptions_of_use.rst @@ -17,6 +17,7 @@ Assumptions of Use (RST) .. aou_req:: Operating Conditions :id: aou_req__rst_test__001 + :root_causes: AouRootCauseFta.UserProvidedWrongConfiguration :safety: ASIL_B :status: valid diff --git a/bazel/rules/rules_score/test/fixtures/seooc_test/assumptions_of_use.rst b/bazel/rules/rules_score/test/fixtures/seooc_test/assumptions_of_use.rst deleted file mode 100644 index 6e95110e..00000000 --- a/bazel/rules/rules_score/test/fixtures/seooc_test/assumptions_of_use.rst +++ /dev/null @@ -1,93 +0,0 @@ -.. - # ******************************************************************************* - # 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 - # ******************************************************************************* -Assumptions of Use -================== - -This document describes the assumptions of use for the test SEooC module. - -.. aou_req:: Operating Temperature Range - :id: aou_req__seooc_test__operating_temperature_range - :status: valid - :tags: environment, iso26262, seooc_test - :safety: ASIL_B - :security: NO - - The SEooC shall operate within temperature range -40°C to +85°C. - -.. aou_req:: Supply Voltage - :id: aou_req__seooc_test__supply_voltage - :status: valid - :tags: power, iso26262, seooc_test - :safety: ASIL_B - :security: NO - - The SEooC shall operate with supply voltage 12V ±10%. - - Maximum current consumption: 2.5A - -.. aou_req:: Processing Load - :id: aou_req__seooc_test__processing_load - :status: valid - :tags: performance, iso26262, seooc_test - :safety: ASIL_B - :security: NO - - The maximum processing load shall not exceed 80% to ensure - timing requirements are met. - -Environmental Assumptions -------------------------- - -.. aou_req:: Controlled Environment - :id: aou_req__seooc_test__controlled_environment - :status: valid - :tags: environment, seooc_test - :safety: ASIL_B - :security: NO - - The system operates in a controlled automotive environment - compliant with ISO 16750 standards. - -.. aou_req:: Maintenance - :id: aou_req__seooc_test__maintenance - :status: valid - :tags: maintenance, seooc_test - :safety: ASIL_B - :security: NO - - Regular maintenance is performed according to the maintenance - schedule defined in the integration manual. - -Integration Constraints ------------------------ - -.. aou_req:: CAN Bus Interface - :id: aou_req__seooc_test__can_bus_interface - :status: valid - :tags: interface, communication, seooc_test - :safety: ASIL_B - :security: NO - - The host system shall provide a CAN 2.0B compliant interface - for communication with the SEooC. - -.. aou_req:: Memory Requirements - :id: aou_req__seooc_test__memory_requirements - :status: valid - :tags: resource, seooc_test - :safety: ASIL_B - :security: NO - - The host system shall provide at least 512KB of RAM and - 2MB of flash memory for the SEooC. diff --git a/bazel/rules/rules_score/test/fixtures/seooc_test/safety_measures_fixtures.trlc b/bazel/rules/rules_score/test/fixtures/seooc_test/safety_measures_fixtures.trlc index c03e9431..65247ecc 100644 --- a/bazel/rules/rules_score/test/fixtures/seooc_test/safety_measures_fixtures.trlc +++ b/bazel/rules/rules_score/test/fixtures/seooc_test/safety_measures_fixtures.trlc @@ -15,12 +15,11 @@ // CompReq): every fault-tree root cause (RootCause) must be addressed by at // least one concrete measure. // - SafetyMeasure: abstract, never instantiated directly; only exists so -// Mitigation can inherit the mandatory ``root_causes`` field. +// Mitigation and AoU inherit the mandatory ``root_causes`` field. // - Mitigation: extends SafetyMeasure, adds a mandatory ``justification`` // explaining why the root cause does not apply in this context. -// - AoU: independent type (does not extend SafetyMeasure); its -// ``root_causes`` field is optional -- an AoU acts as a preventive measure -// only when it addresses a fault-tree root cause. +// - AoU: extends SafetyMeasure; its ``root_causes`` field is mandatory -- an +// AoU is a preventive measure for the fault-tree root causes it addresses. // - CompReq (elsewhere): acts as a control measure when its ``derived_from`` // references a fault-tree RootCause. diff --git a/bazel/rules/rules_score/test/rst_to_trlc_test.py b/bazel/rules/rules_score/test/rst_to_trlc_test.py index a2734741..6d3d519a 100644 --- a/bazel/rules/rules_score/test/rst_to_trlc_test.py +++ b/bazel/rules/rules_score/test/rst_to_trlc_test.py @@ -91,6 +91,7 @@ def test_required_attrs_present(self): "safety", "satisfies", "derived_from", + "root_causes", "rationale", "version", ): @@ -415,13 +416,49 @@ def test_comp_req_without_satisfies_has_no_derived_from(self): # --- AoU --- def test_aou_req_produces_aou_type(self): - out = render_trlc(self._single("aou_req"), "Pkg", "") + out = render_trlc(self._single("aou_req", {"root_causes": "FtaPkg.RcA"}), "Pkg", "") self.assertIn("ScoreReq.AoU", out) def test_aou_req_no_rationale(self): - out = render_trlc(self._single("aou_req"), "Pkg", "") + out = render_trlc(self._single("aou_req", {"root_causes": "FtaPkg.RcA"}), "Pkg", "") self.assertNotIn("rationale", out) + def test_aou_req_root_causes_rendered_and_package_imported(self): + items = self._single( + "aou_req", + {"root_causes": "FtaPkg.RcA, OtherFta.RcB"}, + ) + out = render_trlc(items, "Pkg", "") + self.assertIn("root_causes = [FtaPkg.RcA, OtherFta.RcB]", out) + self.assertIn("import FtaPkg", out) + self.assertIn("import OtherFta", out) + + def test_aou_req_without_root_causes_is_rejected(self): + with self.assertRaisesRegex(ValueError, r"aou_req__test__001.*:root_causes:"): + render_trlc(self._single("aou_req"), "Pkg", "") + + def test_aou_req_unqualified_root_cause_is_rejected(self): + items = self._single("aou_req", {"root_causes": "FtaPkg.RcA, RcB"}) + with self.assertRaisesRegex(ValueError, r"'RcB'.*\."): + render_trlc(items, "Pkg", "") + + def test_aou_req_malformed_root_cause_is_rejected(self): + items = self._single("aou_req", {"root_causes": "A.B.C"}) + with self.assertRaises(ValueError): + render_trlc(items, "Pkg", "") + + def test_root_causes_on_non_aou_is_rejected(self): + items = self._single("comp_req", {"root_causes": "FtaPkg.RcA"}) + with self.assertRaisesRegex(ValueError, r"comp_req__test__001.*only supported on 'aou_req'"): + render_trlc(items, "Pkg", "") + + def test_aou_req_root_cause_in_output_package_is_not_imported(self): + items = self._single("aou_req", {"root_causes": "Pkg.RcA, FtaPkg.RcB"}) + out = render_trlc(items, "Pkg", "") + self.assertNotIn("import Pkg", out) + self.assertIn("import FtaPkg", out) + self.assertIn("root_causes = [Pkg.RcA, FtaPkg.RcB]", out) + # --- Ignored Sphinx-only attributes --- def test_sphinx_only_fields_do_not_appear_in_output(self): @@ -687,13 +724,25 @@ def test_aou_req_round_trip(self): ".. aou_req:: Operating Conditions\n" " :id: aou_req__test__001\n" " :safety: ASIL_B\n" + " :root_causes: FtaPkg.RcA\n" "\n" " The SEooC shall operate within defined conditions.\n" ) out = self._convert(rst) self.assertIn("ScoreReq.AoU", out) + self.assertIn("root_causes = [FtaPkg.RcA]", out) self.assertNotIn("rationale", out) + def test_aou_req_without_root_causes_fails_and_writes_nothing(self): + rst = ".. aou_req:: No Cause\n :id: aou_req__test__002\n :safety: QM\n\n Body.\n" + with tempfile.TemporaryDirectory() as tmpdir: + src = Path(tmpdir) / "aous.rst" + src.write_text(rst, encoding="utf-8") + out = Path(tmpdir) / "aous.trlc" + with self.assertRaises(ValueError): + convert(src, out) + self.assertFalse(out.exists()) + def test_package_name_derived_from_file_stem(self): rst = ".. assumed_system_req:: Pkg Test\n :id: asr_req__test__001\n :safety: QM\n\n Body.\n" out = self._convert(rst, filename="my_requirements.rst") diff --git a/bazel/rules/rules_score/test/safety_analysis_measures_test.bzl b/bazel/rules/rules_score/test/safety_analysis_measures_test.bzl new file mode 100644 index 00000000..4e58c96a --- /dev/null +++ b/bazel/rules/rules_score/test/safety_analysis_measures_test.bzl @@ -0,0 +1,96 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Analysis tests for the fault_trees / safety_analysis rules and their measures.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@score_tooling//bazel/rules/rules_score:providers.bzl", "FaultTreesInfo") + +def _action_with_output(env, basename): + for action in analysistest.target_actions(env): + for out in action.outputs.to_list(): + if out.basename == basename: + return action + return None + +def _flag_values(argv, flag): + """Values following *flag* up to the next ``--`` option.""" + if flag not in argv: + return [] + values = [] + for arg in argv[argv.index(flag) + 1:]: + if arg.startswith("--"): + break + values.append(arg) + return values + +def _basenames(paths): + return [p.split("/")[-1] for p in paths] + +def _measure_deps_are_context_only_test_impl(ctx): + env = analysistest.begin(ctx) + + # A component requirement measure derives from an AoU held in its deps. + # That AoU is parsed for reference resolution only: it is not rendered and + # lobster-trlc, which only extracts Mitigation / AoU records, never sees it. + assembler = _action_with_output(env, "safety_analysis.rst") + if assembler == None: + asserts.true(env, False, "assembler action not found") + return analysistest.end(env) + argv = assembler.argv + asserts.equals( + env, + ["comp_req.trlc"], + _basenames(_flag_values(argv, "--safetymeasures")), + ) + asserts.true(env, "aou.trlc" in _basenames(_flag_values(argv, "--dep-files"))) + asserts.equals(env, None, _action_with_output(env, "safetymeasures.lobster")) + return analysistest.end(env) + +measure_deps_are_context_only_test = analysistest.make(_measure_deps_are_context_only_test_impl) + +def _measure_spec_is_merged_test_impl(ctx): + env = analysistest.begin(ctx) + + # The RSL types of a measure target are needed to parse its records. + assembler = _action_with_output(env, "safety_analysis.rst") + if assembler == None: + asserts.true(env, False, "assembler action not found") + return analysistest.end(env) + asserts.true(env, "extra_model.rsl" in _basenames(_flag_values(assembler.argv, "--dep-files"))) + return analysistest.end(env) + +measure_spec_is_merged_test = analysistest.make(_measure_spec_is_merged_test_impl) + +def _fta_default_package_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.equals( + env, + ctx.attr.expected_package, + analysistest.target_under_test(env)[FaultTreesInfo].fta_package, + ) + return analysistest.end(env) + +fta_default_package_test = analysistest.make( + _fta_default_package_test_impl, + attrs = {"expected_package": attr.string(mandatory = True)}, +) + +def _aou_in_aou_deps_fails_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "deps must not hold the assumptions_of_use target") + return analysistest.end(env) + +aou_in_aou_deps_fails_test = analysistest.make( + _aou_in_aou_deps_fails_test_impl, + expect_failure = True, +) 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 324ad7d2..409fde4b 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,6 +12,7 @@ # ******************************************************************************* """Tests for aou_forwarding_to_lobster.""" +import os import tempfile import unittest @@ -20,6 +21,7 @@ from lobster.common.location import Void_Reference from aou_forwarding_to_lobster import ( + ForwardingEntry, build_forwarded_markers, filter_forwarded_aous, load_lobster_items, @@ -44,6 +46,7 @@ class TestParseForwardingYaml(unittest.TestCase): def _write_yaml(self, data: dict) -> str: f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) + self.addCleanup(os.unlink, f.name) yaml.dump(data, f) f.close() return f.name @@ -52,14 +55,14 @@ def test_valid_yaml(self) -> None: path = self._write_yaml( { "forwarded_aous": [ - {"aou_id": "Pkg.AoU1", "justification": "reason"}, + {"aou_id": "Pkg.AoU1@1", "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") + self.assertEqual(result[0].aou_id, "Pkg.AoU1@1") + self.assertEqual(result[0].justification, "reason") def test_missing_forwarded_aous_key(self) -> None: path = self._write_yaml({"wrong_key": []}) @@ -80,14 +83,52 @@ def test_multiple_entries(self) -> None: path = self._write_yaml( { "forwarded_aous": [ - {"aou_id": "A.B", "justification": "r1"}, - {"aou_id": "C.D", "justification": "r2"}, + {"aou_id": "A.B@1", "justification": "r1"}, + {"aou_id": "C.D@2", "justification": "r2"}, ] } ) result = parse_forwarding_yaml(path) self.assertEqual(len(result), 2) + def test_unversioned_aou_id_is_rejected(self) -> None: + path = self._write_yaml({"forwarded_aous": [{"aou_id": "Pkg.AoU1", "justification": "r"}]}) + with self.assertRaises(SystemExit) as ctx: + parse_forwarding_yaml(path) + self.assertIn("must name the AoU version", str(ctx.exception)) + + def test_non_numeric_version_is_rejected(self) -> None: + path = self._write_yaml({"forwarded_aous": [{"aou_id": "Pkg.AoU1@x", "justification": "r"}]}) + with self.assertRaises(SystemExit): + parse_forwarding_yaml(path) + + def test_duplicate_aou_id_is_rejected(self) -> None: + path = self._write_yaml( + { + "forwarded_aous": [ + {"aou_id": "Pkg.AoU1@1", "justification": "r1"}, + {"aou_id": "Pkg.AoU1@1", "justification": "r2"}, + ] + } + ) + with self.assertRaises(SystemExit) as ctx: + parse_forwarding_yaml(path) + self.assertIn("'Pkg.AoU1@1' is listed in entries 0 and 1", str(ctx.exception)) + + def test_entry_lines_are_recorded(self) -> None: + f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) + self.addCleanup(os.unlink, f.name) + f.write( + "# comment\n" + "forwarded_aous:\n" + " - aou_id: A.B@1\n" + " justification: r1\n" + " - aou_id: C.D@1\n" + " justification: r2\n" + ) + f.close() + self.assertEqual([e.line for e in parse_forwarding_yaml(f.name)], [3, 5]) + class TestLoadLobsterItems(unittest.TestCase): """Tests for load_lobster_items.""" @@ -97,6 +138,7 @@ def _write_lobster(self, tags: list[str]) -> str: items = [_req(tag, tag.split(" ", 1)[1].split("@")[0]) for tag in tags] f = tempfile.NamedTemporaryFile(mode="w", suffix=".lobster", delete=False) + self.addCleanup(os.unlink, f.name) lobster_write(f, Requirement, "test", items) f.close() return f.name @@ -123,76 +165,60 @@ class TestFilterForwardedAous(unittest.TestCase): """Tests for filter_forwarded_aous.""" def test_filters_correctly(self) -> None: - items = [_req("req Pkg.AoU1", "AoU1"), _req("req Pkg.AoU2", "AoU2")] - entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + items = [_req("req Pkg.AoU1@1", "AoU1"), _req("req Pkg.AoU2@1", "AoU2")] + entries = [ForwardingEntry("Pkg.AoU1@1", "reason")] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) - self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1") + self.assertEqual(str(filtered[0].tag), "req Pkg.AoU1@1") def test_multiple_filters(self) -> None: items = [ - _req("req A.B", "B"), - _req("req C.D", "D"), - _req("req E.F", "F"), + _req("req A.B@1", "B"), + _req("req C.D@1", "D"), + _req("req E.F@1", "F"), ] entries = [ - {"aou_id": "A.B", "justification": "r1"}, - {"aou_id": "E.F", "justification": "r2"}, + ForwardingEntry("A.B@1", "r1"), + ForwardingEntry("E.F@1", "r2"), ] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 2) def test_nonexistent_aou_id_raises(self) -> None: - items = [_req("req Pkg.AoU1", "AoU1")] - entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] + items = [_req("req Pkg.AoU1@1", "AoU1")] + entries = [ForwardingEntry("NonExistent.Foo@1", "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 = [ - _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(str(filtered[0].tag), "req Pkg.AoU1@1") + def test_no_received_items_raises(self) -> None: + entries = [ForwardingEntry("Pkg.AoU1@1", "reason")] + with self.assertRaises(SystemExit) as ctx: + filter_forwarded_aous(entries, []) + self.assertIn("Available IDs: (none)", str(ctx.exception)) - def test_versioned_tag_matches_full_id(self) -> None: - """Full versioned ID should also work.""" - 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_no_entries_and_no_items_is_fine(self) -> None: + self.assertEqual(filter_forwarded_aous([], []), []) - 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): + def test_version_mismatch_raises_with_hint(self) -> None: + items = [_req("req Pkg.AoU1@2", "AoU1")] + entries = [ForwardingEntry("Pkg.AoU1@1", "reason")] + with self.assertRaises(SystemExit) as ctx: filter_forwarded_aous(entries, items) + self.assertIn("received version(s): 2", str(ctx.exception)) - 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_version_mismatch_hint_lists_all_received_versions(self) -> None: + items = [_req("req Pkg.AoU1@2", "AoU1"), _req("req Pkg.AoU1@3", "AoU1")] + entries = [ForwardingEntry("Pkg.AoU1@1", "reason")] + with self.assertRaises(SystemExit) as ctx: + filter_forwarded_aous(entries, items) + self.assertIn("received version(s): 2, 3", str(ctx.exception)) - 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.""" + def test_duplicate_identical_item_matches_once(self) -> None: items = [ _req("req Pkg.AoU1@1", "AoU1"), _req("req Pkg.AoU1@1", "AoU1"), ] - entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + entries = [ForwardingEntry("Pkg.AoU1@1", "reason")] filtered = filter_forwarded_aous(entries, items) self.assertEqual(len(filtered), 1) @@ -203,8 +229,8 @@ class TestBuildForwardedMarkers(unittest.TestCase): 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"}, + ForwardingEntry("Pkg.AoU1@1", "reason 1"), + ForwardingEntry("Pkg.AoU2@1", "reason 2"), ] markers = build_forwarded_markers(entries, items, "aou_forwarding.yaml") self.assertEqual(len(markers), 2) @@ -214,38 +240,45 @@ def test_marker_has_distinct_tag_and_refs_original(self) -> None: (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"}] + entries = [ForwardingEntry("Pkg.AoU1@1", "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_tag_survives_lobster_round_trip(self) -> None: + items = [_req("req Pkg.AoU1@1", "AoU1")] + markers = build_forwarded_markers([ForwardingEntry("Pkg.AoU1@1", "reason")], items, "aou_forwarding.yaml") + reread = Tracing_Tag.from_json(str(markers[0].tag)) + self.assertEqual(reread.tag, "Pkg.AoU1__forwarded") + self.assertIsNone(reread.version) + 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"}] + entries = [ForwardingEntry("Pkg.AoU1@1", "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"}] + entries = [ForwardingEntry("Pkg.AoU1@1", "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: + def test_marker_location_uses_yaml_path_and_entry_line(self) -> None: items = [_req("req Pkg.AoU1@1", "AoU1")] - entries = [{"aou_id": "Pkg.AoU1", "justification": "reason"}] + entries = [ForwardingEntry("Pkg.AoU1@1", "reason", line=7)] markers = build_forwarded_markers(entries, items, "some/path/aou_forwarding.yaml") self.assertEqual(markers[0].location.filename, "some/path/aou_forwarding.yaml") + self.assertEqual(markers[0].location.line, 7) def test_nonexistent_aou_id_raises(self) -> None: items = [_req("req Pkg.AoU1@1", "AoU1")] - entries = [{"aou_id": "NonExistent.Foo", "justification": "reason"}] + entries = [ForwardingEntry("NonExistent.Foo@1", "reason")] with self.assertRaises(SystemExit): build_forwarded_markers(entries, items, "aou_forwarding.yaml") diff --git a/bazel/rules/rules_score/test/test_merge_lobster_items.py b/bazel/rules/rules_score/test/test_merge_lobster_items.py new file mode 100644 index 00000000..528c6298 --- /dev/null +++ b/bazel/rules/rules_score/test/test_merge_lobster_items.py @@ -0,0 +1,106 @@ +# ******************************************************************************* +# 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 merge_lobster_items.""" + +import os +import tempfile +import unittest + +from lobster.common.io import lobster_write +from lobster.common.items import Requirement, Tracing_Tag +from lobster.common.location import File_Reference + +from merge_lobster_items import merge_items + + +def _req(tag: str, text: str = "must hold", source: str = "aou.trlc") -> Requirement: + namespace, rest = tag.split(" ", 1) + return Requirement( + tag=Tracing_Tag.from_text(namespace, rest), + location=File_Reference(source, line=1), + framework="TRLC", + kind="AoU", + name=rest.split("@")[0], + text=text, + ) + + +class TestMergeItems(unittest.TestCase): + """Tests for merge_items.""" + + def _write(self, items: list[Requirement]) -> str: + f = tempfile.NamedTemporaryFile(mode="w", suffix=".lobster", delete=False) + self.addCleanup(os.unlink, f.name) + lobster_write(f, Requirement, "test", items) + f.close() + return f.name + + def test_distinct_items_are_all_kept(self) -> None: + merged = merge_items([self._write([_req("req A.X@1")]), self._write([_req("req B.Y@1")])]) + self.assertEqual([str(i.tag) for i in merged], ["req A.X@1", "req B.Y@1"]) + + def test_same_item_via_two_files_is_kept_once(self) -> None: + merged = merge_items([self._write([_req("req A.X@1")]), self._write([_req("req A.X@1")])]) + self.assertEqual([str(i.tag) for i in merged], ["req A.X@1"]) + + def test_same_item_via_three_files_is_kept_once(self) -> None: + paths = [self._write([_req("req A.X@1")]) for _ in range(3)] + self.assertEqual(len(merge_items(paths)), 1) + + def test_location_is_ignored_and_first_file_wins(self) -> None: + origin = self._write([_req("req A.X@1", source="origin.trlc")]) + copy = self._write([_req("req A.X@1", source="copy.trlc")]) + merged = merge_items([origin, copy]) + self.assertEqual(len(merged), 1) + self.assertEqual(merged[0].location.filename, "origin.trlc") + + def test_different_text_is_a_conflict(self) -> None: + paths = [self._write([_req("req A.X@1", text="one")]), self._write([_req("req A.X@1", text="two")])] + with self.assertRaises(SystemExit) as ctx: + merge_items(paths) + self.assertIn("Conflicting definitions of 'req A.X'", str(ctx.exception)) + self.assertIn("text", str(ctx.exception)) + + def test_report_state_is_not_part_of_the_definition(self) -> None: + plain = _req("req A.X@1") + resolved = _req("req A.X@1") + resolved.messages.append("checked") + resolved.ref_up.append(Tracing_Tag("req", "B.Y", "1")) + merged = merge_items([self._write([plain]), self._write([resolved])]) + self.assertEqual(len(merged), 1) + + def test_different_references_are_a_conflict(self) -> None: + first = _req("req A.X@1") + second = _req("req A.X@1") + second.add_tracing_target(Tracing_Tag("req", "B.Y", "1")) + with self.assertRaises(SystemExit) as ctx: + merge_items([self._write([first]), self._write([second])]) + self.assertIn("refs", str(ctx.exception)) + + def test_different_version_is_a_conflict(self) -> None: + paths = [self._write([_req("req A.X@1")]), self._write([_req("req A.X@2")])] + with self.assertRaises(SystemExit) as ctx: + merge_items(paths) + self.assertIn("tag", str(ctx.exception)) + + def test_unreadable_file_is_reported(self) -> None: + with self.assertRaises(SystemExit) as ctx: + merge_items(["/nonexistent/items.lobster"]) + self.assertIn("Failed to parse lobster file", str(ctx.exception)) + + def test_no_files_gives_no_items(self) -> None: + self.assertEqual(merge_items([]), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/bazel/rules/rules_score/test/test_safety_analysis_assembler.py b/bazel/rules/rules_score/test/test_safety_analysis_assembler.py index f9c546eb..fbaa5bae 100644 --- a/bazel/rules/rules_score/test/test_safety_analysis_assembler.py +++ b/bazel/rules/rules_score/test/test_safety_analysis_assembler.py @@ -15,6 +15,7 @@ import os import sys import tempfile +import types import unittest import safety_analysis_assembler as fa @@ -26,10 +27,11 @@ def __init__(self, name): class _Obj: - def __init__(self, name, type_name, fields=None): + def __init__(self, name, type_name, fields=None, file_name="x.trlc"): self.name = name self.n_typ = _Type(type_name) self._fields = fields or {} + self.location = types.SimpleNamespace(file_name=file_name) def to_python_dict(self): return self._fields @@ -167,6 +169,35 @@ def test_root_causes_for_failure_mode_orphan_is_empty(self): self.assertEqual(fa._root_causes_for_failure_mode(self.obj_map, "Lib.FM_Orphan"), []) +class CheckInputsTest(unittest.TestCase): + def test_consistent_inputs_have_no_errors(self): + self.assertEqual(fa._check_inputs(_objs(), []), []) + + def test_root_cause_failure_mode_outside_failure_modes_is_reported(self): + objs = {fqn: obj for fqn, obj in _objs().items() if fqn != "Lib.FM_A"} + errors = fa._check_inputs(objs, []) + self.assertEqual(len(errors), 1) + self.assertIn("Lib.RC_A", errors[0]) + self.assertIn("Lib.FM_A", errors[0]) + + def test_mitigation_in_mitigations_file_is_accepted(self): + objs = _objs() + objs["Lib.CM_1"] = _Obj("CM_1", "Mitigation", {"root_causes": ["Lib.RC_A"]}, file_name="m.trlc") + self.assertEqual(fa._check_inputs(objs, ["m.trlc"]), []) + + def test_non_mitigation_in_mitigations_file_is_reported(self): + objs = _objs() + objs["Lib.Aou_1"] = _Obj("Aou_1", "AoU", {"root_causes": ["Lib.RC_A"]}, file_name="m.trlc") + errors = fa._check_inputs(objs, ["m.trlc"]) + self.assertEqual(len(errors), 1) + self.assertIn("AoU Lib.Aou_1", errors[0]) + + def test_non_mitigation_outside_mitigations_file_is_accepted(self): + objs = _objs() + objs["Lib.Aou_1"] = _Obj("Aou_1", "AoU", {"root_causes": ["Lib.RC_A"]}, file_name="a.trlc") + self.assertEqual(fa._check_inputs(objs, ["m.trlc"]), []) + + class BuildBodyTest(unittest.TestCase): def setUp(self): self.renderer = _FakeRenderer(_objs()) @@ -297,10 +328,7 @@ def test_no_fault_trees_renders_all_failure_modes_without_fta(self): type Mitigation extends SafetyMeasure { } -type AoU { - safety optional String - description optional String - root_causes optional RootCause [1..*] +type AoU extends SafetyMeasure { } tuple CompReqSourceId { @@ -338,7 +366,9 @@ def test_no_fault_trees_renders_all_failure_modes_without_fta(self): } """ -_COMPREQ_BODY = """\ +_COMPREQ_TRLC = """\ +package TestSafetyAnalysis + CompReq CrB { description = "cr b description" derived_from = [TestSafetyAnalysis.RcB@1] @@ -393,7 +423,8 @@ def test_full_page_assembled_from_real_trlc(self): with tempfile.TemporaryDirectory() as tmp: rsl = self._write(tmp, "types.rsl", _RSL) fm = self._write(tmp, "fm.trlc", _FM_TRLC) - cm = self._write(tmp, "cm.trlc", _MITIGATION_TRLC + "\n" + _COMPREQ_BODY) + cm = self._write(tmp, "cm.trlc", _MITIGATION_TRLC) + cr = self._write(tmp, "cr.trlc", _COMPREQ_TRLC) fta = self._write(tmp, "fta_events.trlc", _FTA_TRLC) template = self._write(tmp, "tmpl.rst", "{body}\n") out = os.path.join(tmp, "safety_analysis.rst") @@ -415,9 +446,11 @@ def test_full_page_assembled_from_real_trlc(self): "a.puml", "--failuremodes", fm, - "--safetymeasures", + "--mitigations", cm, - "--spec", + "--safetymeasures", + cr, + "--dep-files", rsl, ] ) @@ -447,6 +480,51 @@ def test_full_page_assembled_from_real_trlc(self): # by neither and is the only root cause flagged. self.assertEqual(rst.count(":bdg-danger:`No safety measure`"), 1) + def _assemble(self, tmp, failuremodes=(), mitigations=(), spec=()): + rsl = self._write(tmp, "types.rsl", _RSL) + fta = self._write(tmp, "fta_events.trlc", _FTA_TRLC) + template = self._write(tmp, "tmpl.rst", "{body}\n") + argv = [ + "safety_analysis_assembler", + "--output", + os.path.join(tmp, "safety_analysis.rst"), + "--template", + template, + "--title", + "T", + "--fta-package", + "TestSafetyAnalysis", + "--fta-events", + fta, + "--failuremodes", + *failuremodes, + "--mitigations", + *mitigations, + "--dep-files", + rsl, + *spec, + ] + self._run_main(argv) + + def test_root_cause_failure_mode_outside_failure_modes_exits_nonzero(self): + with tempfile.TemporaryDirectory() as tmp: + fm = self._write(tmp, "fm.trlc", _FM_TRLC) + with self.assertRaises(SystemExit) as ctx: + self._assemble(tmp, spec=[fm]) + self.assertEqual(ctx.exception.code, 1) + + def test_non_mitigation_in_mitigations_file_exits_nonzero(self): + with tempfile.TemporaryDirectory() as tmp: + fm = self._write(tmp, "fm.trlc", _FM_TRLC) + aou = self._write( + tmp, + "aou.trlc", + "package TestSafetyAnalysis\n\nAoU AouA {\n root_causes = [TestSafetyAnalysis.RcA]\n}\n", + ) + with self.assertRaises(SystemExit) as ctx: + self._assemble(tmp, failuremodes=[fm], mitigations=[aou]) + self.assertEqual(ctx.exception.code, 1) + def test_malformed_fta_events_exits_nonzero(self): with tempfile.TemporaryDirectory() as tmp: template = self._write(tmp, "tmpl.rst", "{body}\n") 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 98fa7b20..adde3be1 100644 --- a/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl +++ b/bazel/rules/rules_score/trlc/config/score_requirements_model.rsl @@ -163,7 +163,7 @@ type RootCause "A fault-tree root cause (leaf node), generated from a $RootCause // concrete measure: a CompReq (control measure, via derived_from), an AoU // (preventive measure, via root_causes) or a Mitigation (via root_causes, // arguing the root cause does not apply). SafetyMeasure itself is never -// instantiated directly -- it only exists so Mitigation can inherit the +// instantiated directly -- it only exists so Mitigation and AoU inherit the // mandatory root_causes field. abstract type SafetyMeasure "Base type for measures that address one or more fault-tree root causes." extends RequirementSafety { @@ -176,9 +176,7 @@ type Mitigation "Argues that a fault-tree root cause does not apply in this cont String } -type AoU "Assumption of Use — a safety-relevant condition that a user must fulfil. Acts as a preventive measure when it addresses a fault-tree root cause." extends RequirementSafety { - root_causes "Fault-tree root causes that this assumption of use prevents." - optional RootCause[1 .. *] +type AoU "Assumption of Use — a safety-relevant condition that a user must fulfil. Acts as a preventive measure for the fault-tree root causes it addresses." extends SafetyMeasure { } ///////////////////////////////