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 new file mode 100644 index 00000000..c1164b3b --- /dev/null +++ b/bazel/rules/rules_score/docs/_assets/safety_analysis_doc_pipeline.puml @@ -0,0 +1,88 @@ +' ******************************************************************************* +' 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 safety_analysis_doc_pipeline + +' Data flow from safety analysis source files through Bazel rules into the +' Sphinx staging tree. + +skinparam linetype ortho +skinparam ArrowFontSize 10 +skinparam defaultTextAlignment center +skinparam nodesep 40 +skinparam ranksep 50 + +skinparam rectangle { + BackgroundColor<> #EFF6FB + BorderColor<> #0066B1 + BackgroundColor<> #FFF3E0 + BorderColor<> #EF6C00 + BackgroundColor<> #E8F5E9 + BorderColor<> #2E7D32 + BackgroundColor<> #F3E5F5 + BorderColor<> #7B1FA2 + BackgroundColor<> #FFFDE7 + BorderColor<> #F9A825 +} + +' ── Inputs ──────────────────────────────────────────────────────────────────── +rectangle "failuremodes.trlc\ncontrolmeasures.trlc" <> as trlc +rectangle "fta_*.puml" <> as fta +rectangle "dfa.rst" <> as dfa + +' ── fmea rule ───────────────────────────────────────────────────────────────── +rectangle "**fmea**" <> as fmea + +' ── fmea generated files ────────────────────────────────────────────────────── +rectangle "fmea.rst" <> as fmea_rst +rectangle "failuremodes.inc\ncontrolmeasures.inc" <> as inc_files +rectangle "fta_*.puml (inlined)\nroot_causes.lobster" <> as puml_proc +rectangle "detail_*.rst" <> as detail_rst + +' ── fmea SphinxSourcesInfo ──────────────────────────────────────────────────── +rectangle "SphinxSourcesInfo\n──────────────────\nsrcs: fmea.rst\ndeps: fmea.rst + *.inc + *.puml\naux_srcs: detail_*.rst" <> as fmea_ssi + +' ── dependability_analysis rule ─────────────────────────────────────────────── +rectangle "**dependability_analysis**" <> as da + +' ── dependability_analysis SphinxSourcesInfo ───────────────────────────────── +rectangle "SphinxSourcesInfo\n──────────────────\nsrcs: dfa.rst + fmea.rst\ndeps: + *.inc + *.puml\naux_srcs: detail_*.rst" <> as da_ssi + +' ── dependable_element rule ─────────────────────────────────────────────────── +rectangle "**dependable_element**" <> as de + +' ── Sphinx staging tree ─────────────────────────────────────────────────────── +rectangle "dependability_analysis/\n dfa.rst ← toctree\n fmea.rst ← toctree\n failuremodes.inc\n controlmeasures.inc\n fta_*.puml\n detail_*.rst ← sub-pages" <> as stage + +' ── Edges ───────────────────────────────────────────────────────────────────── +trlc --> fmea : trlc_rst\nlobster-trlc +fta --> fmea : safety_analysis_tools + +fmea --> fmea_rst +fmea --> inc_files +fmea --> puml_proc +fmea --> detail_rst +fmea_rst --> fmea_ssi +inc_files --> fmea_ssi +puml_proc --> fmea_ssi +detail_rst --> fmea_ssi + +dfa --> da +fmea_ssi --> da : merge + +da --> da_ssi + +da_ssi --> de : dependability_analysis attr +de --> stage : symlink srcs+deps\nsymlink aux_srcs\n(no outer toctree entry) + +@enduml diff --git a/bazel/rules/rules_score/docs/tooling_architecture.rst b/bazel/rules/rules_score/docs/tooling_architecture.rst index e5b0621c..4838664d 100644 --- a/bazel/rules/rules_score/docs/tooling_architecture.rst +++ b/bazel/rules/rules_score/docs/tooling_architecture.rst @@ -261,3 +261,32 @@ self-contained. ├── index.html ├── dep1/ ← merged └── dep2/ ← merged + +.. _safety-analysis-doc-pipeline: + +Safety analysis document pipeline +---------------------------------- + +The diagram below shows how FMEA and FTA source files travel through the three +rules (``fmea`` → ``dependability_analysis`` → ``dependable_element``) and land +in the Sphinx staging tree. Blue boxes are source files authored by the +component team; orange boxes are generated files; yellow boxes are the +``SphinxSourcesInfo`` provider payloads; the purple box is the final staging +directory consumed by Sphinx. + +.. uml:: _assets/safety_analysis_doc_pipeline.puml + :align: center + :alt: Safety analysis document pipeline + :width: 100% + +``SphinxSourcesInfo`` carries three depsets: + +- **srcs** — files that become top-level toctree entries in the enclosing + document section (``fmea.rst``, ``dfa.rst``). +- **deps** — all files that must be present in the staging directory: own + ``srcs`` plus ``.inc`` rendered sections and preprocessed ``.puml`` diagrams + that ``fmea.rst`` pulls in via ``.. include::`` / ``.. uml::``. +- **aux_srcs** — files to symlink alongside ``srcs``/``deps`` but **not** added + to the outer index toctree. ``fmea`` uses this for the ``detail_*.rst`` + sub-pages, which are referenced from the inner ``.. toctree::`` inside + ``fmea.rst`` rather than from the section index. diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index b875e2ea..c941d5ae 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -174,6 +174,7 @@ def _architectural_design_impl(ctx): SphinxSourcesInfo( srcs = sphinx_srcs, deps = sphinx_srcs, + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 3fdf32be..f0e69914 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -89,6 +89,7 @@ def _assumptions_of_use_impl(ctx): SphinxSourcesInfo( srcs = all_srcs, deps = depset(transitive = transitive), + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index f6963ac5..e665d533 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -207,6 +207,7 @@ def _component_impl(ctx): SphinxSourcesInfo( srcs = req_sphinx_depset, deps = sphinx_depset, + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 452c2dea..b83eaa92 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -30,7 +30,7 @@ load("//bazel/rules/rules_score/private:lobster_config.bzl", "format_lobster_sou # Private Helpers # ============================================================================ -def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, lobster_files): +def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, rst_aux_list, lobster_files): """Collect analysis providers from a single sub-analysis target. Updates the provided lists/dicts in-place. @@ -39,12 +39,15 @@ def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, lobster_files) sa: A sub-analysis target (fmea or security). 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). """ if SphinxSourcesInfo in sa: rst_srcs_list.append(sa[SphinxSourcesInfo].srcs) rst_deps_list.append(sa[SphinxSourcesInfo].deps) + if sa[SphinxSourcesInfo].aux_srcs: + rst_aux_list.append(sa[SphinxSourcesInfo].aux_srcs) if AnalysisInfo in sa: lobster_files.update(sa[AnalysisInfo].lobster_files) @@ -74,6 +77,7 @@ 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 # ------------------------------------------------------------------------- @@ -82,7 +86,7 @@ def _dependability_analysis_impl(ctx): fmea_output_files = [] for sa in ctx.attr.fmea: fmea_output_files.append(sa[DefaultInfo].files) - _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, lobster_files) + _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, rst_aux_transitive, lobster_files) # ------------------------------------------------------------------------- # Collect from security_analysis targets @@ -90,14 +94,15 @@ def _dependability_analysis_impl(ctx): security_output_files = [] for sa in ctx.attr.security_analysis: security_output_files.append(sa[DefaultInfo].files) - _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, lobster_files) + _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, rst_aux_transitive, lobster_files) - # Architectural design sphinx deps (optional) - if ctx.attr.arch_design and SphinxSourcesInfo in ctx.attr.arch_design: - rst_deps_transitive.append(ctx.attr.arch_design[SphinxSourcesInfo].deps) + # arch_design files are handled separately by dependable_element via its + # architectural_design attribute, so they are not included in this rule's + # sphinx deps to avoid orphan warnings. 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() # ========================================================================= # Lobster traceability report (combined FM + CM + FTA) @@ -182,6 +187,7 @@ def _dependability_analysis_impl(ctx): SphinxSourcesInfo( srcs = all_rst_srcs, deps = all_rst_deps, + aux_srcs = all_rst_aux, ), ] diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index c5e94dff..8d391749 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -254,16 +254,21 @@ def _process_artifact_files(ctx, artifact_name, label): all_files = _get_sphinx_files(label) doc_files = _filter_doc_files(all_files) - if not doc_files: + # aux_srcs are files to symlink but NOT add to the outer toctree index. + aux_files = [] + if label[SphinxSourcesInfo].aux_srcs: + aux_files = label[SphinxSourcesInfo].aux_srcs.to_list() + + if not doc_files and not aux_files: return (output_files, index_refs) # Build a lookup of srcs paths so we know which files are toctree entries. srcs_paths = {f.path: True for f in label[SphinxSourcesInfo].srcs.to_list()} - # Find common directory to preserve hierarchy - common_dir = _find_common_directory(doc_files) + # Find common directory across all files (regular + aux) to preserve hierarchy. + common_dir = _find_common_directory(doc_files + aux_files) - # Process each file + # Process regular deps files for artifact_file in doc_files: # Compute paths relative_path = _compute_relative_path(artifact_file, common_dir) @@ -289,6 +294,17 @@ def _process_artifact_files(ctx, artifact_name, label): .replace(".md", "") index_refs.append(doc_ref) + # Process aux_srcs: symlink without adding to outer toctree index. + for artifact_file in aux_files: + relative_path = _compute_relative_path(artifact_file, common_dir) + output_file = _create_artifact_symlink( + ctx, + artifact_name, + artifact_file, + relative_path, + ) + output_files.append(output_file) + return (output_files, index_refs) def _process_artifact_type(ctx, artifact_name): diff --git a/bazel/rules/rules_score/private/fmea.bzl b/bazel/rules/rules_score/private/fmea.bzl index 07da9390..0c288e3a 100644 --- a/bazel/rules/rules_score/private/fmea.bzl +++ b/bazel/rules/rules_score/private/fmea.bzl @@ -282,14 +282,18 @@ def _fmea_impl(ctx): lobster_files["root_causes.lobster"] = f # detail_rsts are NOT top-level toctree entries (they live in sub-toctrees - # within fmea.rst), but they must be present in the Sphinx tree. They go - # into deps so dependable_element symlinks them alongside toctree files. + # within fmea.rst), but they must be symlinked alongside fmea.rst so Sphinx + # can resolve the toctree references. They go into aux_srcs so that + # dependable_element symlinks them without adding them to the outer index. toctree_files = [f for f in output_files if f not in detail_rsts] all_sphinx_srcs = depset(toctree_files) - sphinx_deps = [all_sphinx_srcs, depset(detail_rsts)] - if ctx.attr.arch_design and SphinxSourcesInfo in ctx.attr.arch_design: - sphinx_deps.append(ctx.attr.arch_design[SphinxSourcesInfo].deps) + # Only include fmea's own generated files in the sphinx deps. arch_design + # files are handled separately by dependable_element via its + # architectural_design attribute, so omitting them here avoids their RST + # wrappers being symlinked into the dependability_analysis/ section as + # orphaned documents. + sphinx_deps = [all_sphinx_srcs] return [ DefaultInfo( @@ -302,6 +306,7 @@ def _fmea_impl(ctx): SphinxSourcesInfo( srcs = all_sphinx_srcs, deps = depset(transitive = sphinx_deps), + aux_srcs = depset(detail_rsts), ), ] diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index 55a449ad..4645ba7d 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -125,6 +125,7 @@ def _requirements_impl(ctx): SphinxSourcesInfo( srcs = sphinx_srcs, deps = depset(transitive = transitive_sphinx), + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 6fae4449..5a6dbb92 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -167,6 +167,7 @@ def _score_html_impl(ctx): "json_path": dep[SphinxNeedsInfo].needs_json_file.path, # Use direct file "id_prefix": "", "css_class": "", + "version": "1.0", } for dep in ctx.attr.deps: if SphinxModuleInfo in dep: diff --git a/bazel/rules/rules_score/private/unit.bzl b/bazel/rules/rules_score/private/unit.bzl index 60f58cf8..03c44ba1 100644 --- a/bazel/rules/rules_score/private/unit.bzl +++ b/bazel/rules/rules_score/private/unit.bzl @@ -96,6 +96,7 @@ def _unit_impl(ctx): SphinxSourcesInfo( srcs = all_files, deps = depset(transitive = [all_files] + sphinx_design_deps), + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/private/unit_design.bzl b/bazel/rules/rules_score/private/unit_design.bzl index 6c641a96..afcf1bb1 100644 --- a/bazel/rules/rules_score/private/unit_design.bzl +++ b/bazel/rules/rules_score/private/unit_design.bzl @@ -95,6 +95,7 @@ def _unit_design_impl(ctx): SphinxSourcesInfo( srcs = all_source_files, deps = all_source_files, + aux_srcs = depset(), ), ] diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index f125870c..7c952e9f 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -51,17 +51,24 @@ SphinxSourcesInfo = provider( this to enable integration with sphinx_module and dependable_element. Semantics: - srcs — Files directly owned/generated by this rule. Only these are - used as top-level toctree entries by dependable_element. - For leaf rules (no children), srcs == deps. - deps — All files needed in the Sphinx tree: own srcs plus all - transitive files from children. dependable_element symlinks - everything in deps into the output tree. Parent/container - rules bubble up children's deps into their own deps field. + srcs — Files directly owned/generated by this rule. Only these are + used as top-level toctree entries by dependable_element. + For leaf rules (no children), srcs == deps. + deps — All files needed in the Sphinx tree: own srcs plus all + transitive files from children. dependable_element symlinks + everything in deps into the output tree. Parent/container + rules bubble up children's deps into their own deps field. + aux_srcs — Auxiliary files that must be present in the Sphinx source + tree but must NOT appear as top-level toctree entries. + Typically detail pages (e.g. detail_*_fta.rst) that are + referenced from within another page's own toctree directive. + dependable_element symlinks these alongside srcs/deps but + does not add them to the outer index toctree. """, fields = { "srcs": "Depset of files directly owned by this rule (.rst, .md, .puml, etc.). Used by dependable_element as toctree entries.", "deps": "Depset of all files needed in the Sphinx tree — own srcs plus transitive deps from all children. For leaf rules, equals srcs.", + "aux_srcs": "Depset of files to symlink into the Sphinx tree but NOT add to the outer toctree (e.g. detail sub-pages referenced from within another page).", }, )