From b748d885a854f65b58174f085e719c7843452cfb Mon Sep 17 00:00:00 2001 From: Jochen Hoenle Date: Thu, 7 May 2026 15:08:07 +0200 Subject: [PATCH 1/2] [rules score] rework input to safety analysis --- .../examples/seooc/safety_analysis/BUILD | 41 +------- bazel/rules/rules_score/private/fmea.bzl | 93 +++++++++---------- 2 files changed, 47 insertions(+), 87 deletions(-) diff --git a/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD b/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD index 1a4847af..e3067db8 100644 --- a/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD +++ b/bazel/rules/rules_score/examples/seooc/safety_analysis/BUILD @@ -11,46 +11,11 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@trlc//:trlc.bzl", "trlc_requirements", "trlc_requirements_test") load( "//bazel/rules/rules_score:rules_score.bzl", "fmea", ) -# FMEA -trlc_requirements( - name = "sample_fmea_failure_modes", - srcs = [ - "sample_fmea_failure_modes.trlc", - ], - spec = [ - "@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model", - ], - visibility = ["//visibility:public"], -) - -trlc_requirements( - name = "sample_fmea_control_measures", - srcs = [ - "sample_fmea_control_measures.trlc", - ], - spec = [ - "@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model", - ], - visibility = ["//visibility:public"], - deps = [ - ":sample_fmea_failure_modes", - ], -) - -trlc_requirements_test( - name = "sample_fmea_test", - reqs = [ - ":sample_fmea_control_measures", - ], - visibility = ["//visibility:public"], -) - # FTA filegroup( @@ -64,10 +29,8 @@ filegroup( fmea( name = "sample_fmea", arch_design = "//bazel/rules/rules_score/examples/seooc/design:sample_seooc_design", - controlmeasures = [ - ":sample_fmea_control_measures", - ], - failuremodes = [":sample_fmea_failure_modes"], + controlmeasures = ["sample_fmea_control_measures.trlc"], + failuremodes = ["sample_fmea_failure_modes.trlc"], root_causes = [":sample_fta"], visibility = ["//visibility:public"], ) diff --git a/bazel/rules/rules_score/private/fmea.bzl b/bazel/rules/rules_score/private/fmea.bzl index f59b419d..3f2d1eae 100644 --- a/bazel/rules/rules_score/private/fmea.bzl +++ b/bazel/rules/rules_score/private/fmea.bzl @@ -41,7 +41,6 @@ This is a **build-only** rule. The combined traceability *test* is owned by the ``dependability_analysis`` rule which wraps this one. """ -load("@trlc//:trlc.bzl", "TrlcProviderInfo") load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "SphinxSourcesInfo") load("//bazel/rules/rules_score/private:puml_utils.bzl", "make_puml_rst_wrappers") load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") @@ -129,33 +128,37 @@ def _process_root_causes(ctx): # Private Helpers # ============================================================================ -def _render_trlc_inc(ctx, src, suffix): - """Render a trlc_requirements target to an ``.inc`` file via trlc_rst. +def _render_trlc_inc(ctx, trlc_files, spec_files, out_name): + """Render a list of ``.trlc`` source files to an ``.inc`` file via trlc_rst. The ``.inc`` extension means the file is symlinked into the output directory (via ``_filter_doc_files``) but is NOT added to any Sphinx toctree (``_is_document_file`` only matches ``.rst`` / ``.md``). Args: - ctx: Rule context. - src: Label carrying TrlcProviderInfo. - suffix: Suffix appended to the target name before ``.inc``. + ctx: Rule context. + trlc_files: List of ``.trlc`` File objects to render. + spec_files: List of ``.rsl`` spec File objects needed for TRLC import + resolution (passed as sandbox inputs only). + out_name: Output filename (e.g. ``"failuremodes.inc"``). Returns: - Declared ``.inc`` output File inside ``{label.name}/``. + Declared ``.inc`` output File inside ``{label.name}/``, or ``None`` + when ``trlc_files`` is empty. """ - trlc_provider = src[TrlcProviderInfo] + if not trlc_files: + return None rendered = ctx.actions.declare_file( - "{}/{}{}.inc".format(ctx.label.name, src.label.name, suffix), + "{}/{}".format(ctx.label.name, out_name), ) args = ctx.actions.args() args.add("--output", rendered.path) args.add("--input-dir", ".") args.add("--title", "") args.add("--source-files") - args.add_all(trlc_provider.reqs) + args.add_all(trlc_files) ctx.actions.run( - inputs = src[DefaultInfo].files, + inputs = trlc_files + spec_files, outputs = [rendered], arguments = [args], executable = ctx.executable._renderer, @@ -179,36 +182,25 @@ def _fmea_impl(ctx): # ------------------------------------------------------------------------- # 1. Render failure modes: TRLC -> .inc via trlc_rst # ------------------------------------------------------------------------- - failuremodes_inc = [ - _render_trlc_inc(ctx, src, "_failuremodes") - for src in ctx.attr.failuremodes - ] + spec_files = ctx.files.spec + fm_inc = _render_trlc_inc(ctx, ctx.files.failuremodes, spec_files, "failuremodes.inc") + failuremodes_inc = [fm_inc] if fm_inc else [] output_files.extend(failuremodes_inc) # ------------------------------------------------------------------------- # 2. Render control measures: TRLC -> .inc via trlc_rst # ------------------------------------------------------------------------- - controlmeasures_inc = [ - _render_trlc_inc(ctx, src, "_controlmeasures") - for src in ctx.attr.controlmeasures - ] + cm_inc = _render_trlc_inc(ctx, ctx.files.controlmeasures, spec_files, "controlmeasures.inc") + controlmeasures_inc = [cm_inc] if cm_inc else [] output_files.extend(controlmeasures_inc) # ------------------------------------------------------------------------- - # 3. Run lobster-trlc on TRLC sources -> lobster files - # Use TrlcProviderInfo.reqs to check if there are any TRLC sources to - # process. Pass DefaultInfo.files as sandbox inputs so that the .rsl - # spec files (needed to resolve `import ScoreReq` etc.) are available - # alongside the .trlc record files. + # 3. Run lobster-trlc on TRLC sources -> lobster files. + # Spec files must be sandbox inputs so the TRLC parser can resolve + # ``import ScoreReq`` etc. # ------------------------------------------------------------------------- - failure_mode_trlc_srcs = [] - failure_mode_inputs = [] - for src in ctx.attr.failuremodes: - failure_mode_trlc_srcs.extend(src[TrlcProviderInfo].reqs.to_list()) - failure_mode_inputs.extend(src[DefaultInfo].files.to_list()) - failuremodes_lobster_files = [] - if failure_mode_trlc_srcs: + if ctx.files.failuremodes: failuremodes_lobster = ctx.actions.declare_file( "{}/failuremodes.lobster".format(ctx.label.name), ) @@ -216,7 +208,7 @@ def _fmea_impl(ctx): args.add("--config", ctx.file._fm_lobster_config.path) args.add("--out", failuremodes_lobster.path) ctx.actions.run( - inputs = failure_mode_inputs + [ctx.file._fm_lobster_config], + inputs = ctx.files.failuremodes + spec_files + [ctx.file._fm_lobster_config], outputs = [failuremodes_lobster], executable = ctx.executable._lobster_trlc, arguments = [args], @@ -224,14 +216,8 @@ def _fmea_impl(ctx): ) failuremodes_lobster_files.append(failuremodes_lobster) - control_measure_trlc_srcs = [] - control_measure_inputs = [] - for src in ctx.attr.controlmeasures: - control_measure_trlc_srcs.extend(src[TrlcProviderInfo].reqs.to_list()) - control_measure_inputs.extend(src[DefaultInfo].files.to_list()) - controlmeasures_lobster_files = [] - if control_measure_trlc_srcs: + if ctx.files.controlmeasures: controlmeasures_lobster = ctx.actions.declare_file( "{}/controlmeasures.lobster".format(ctx.label.name), ) @@ -239,7 +225,7 @@ def _fmea_impl(ctx): args.add("--config", ctx.file._cm_lobster_config.path) args.add("--out", controlmeasures_lobster.path) ctx.actions.run( - inputs = control_measure_inputs + [ctx.file._cm_lobster_config], + inputs = ctx.files.controlmeasures + spec_files + [ctx.file._cm_lobster_config], outputs = [controlmeasures_lobster], executable = ctx.executable._lobster_trlc, arguments = [args], @@ -330,14 +316,20 @@ _fmea = rule( attrs = dict( { "failuremodes": attr.label_list( - providers = [TrlcProviderInfo], + allow_files = [".trlc"], mandatory = False, - doc = "Failure modes as trlc_requirements targets (rendered to .inc via trlc_rst).", + doc = "Failure mode ``.trlc`` source files.", ), "controlmeasures": attr.label_list( - providers = [TrlcProviderInfo], + allow_files = [".trlc"], mandatory = False, - doc = "Control measures as trlc_requirements targets (rendered to .inc via trlc_rst).", + doc = "Control measure ``.trlc`` source files.", + ), + "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.", ), "root_causes": attr.label_list( allow_files = [".puml", ".plantuml"], @@ -407,6 +399,7 @@ _fmea = rule( def fmea( name, + spec = None, failuremodes = [], controlmeasures = [], root_causes = [], @@ -421,7 +414,7 @@ def fmea( FTA diagrams passed via ``root_causes`` are preprocessed to inline ``fta_metamodel.puml`` (hermetic, no ``!include`` at render time) and - lobster traceability items are extracted to ``fta.lobster``. + lobster traceability items are extracted to ``root_causes.lobster``. This is a **build-only** rule. The combined traceability test (FM + CM + FTA) is owned by the ``dependability_analysis`` that wraps @@ -429,16 +422,20 @@ def fmea( Args: name: Target name. - failuremodes: trlc_requirements targets for failure mode records. - controlmeasures: trlc_requirements targets for control measure records. + 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. + controlmeasures: Control measure ``.trlc`` source files. root_causes: Optional FTA PlantUML diagram files (``.puml`` / ``.plantuml``) representing the root causes of failure modes. - arch_design: Optional architectural_design target for traceability. + arch_design: Optional ``architectural_design`` target for traceability. visibility: Bazel visibility. tags: Additional Bazel tags. """ _fmea( name = name, + spec = spec, failuremodes = failuremodes, controlmeasures = controlmeasures, root_causes = root_causes, From bfc3af2c8ceb406650ac6aeebed7d577d6a83d96 Mon Sep 17 00:00:00 2001 From: Jochen Hoenle Date: Thu, 7 May 2026 15:50:16 +0200 Subject: [PATCH 2/2] [rules score] clean up deps in sphinx build --- .../private/architectural_design.bzl | 1 - .../private/assumptions_of_use.bzl | 1 - bazel/rules/rules_score/private/component.bzl | 1 - .../private/dependability_analysis.bzl | 20 ++++++-------- .../private/dependable_element.bzl | 27 ++++++------------- bazel/rules/rules_score/private/fmea.bzl | 8 +++--- .../rules_score/private/requirements.bzl | 1 - bazel/rules/rules_score/private/unit.bzl | 1 - .../rules/rules_score/private/unit_design.bzl | 1 - bazel/rules/rules_score/providers.bzl | 14 +++++++--- 10 files changed, 31 insertions(+), 44 deletions(-) diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index 75682a0f..14a09454 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -174,7 +174,6 @@ def _architectural_design_impl(ctx): SphinxSourcesInfo( srcs = sphinx_srcs, deps = sphinx_srcs, - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/private/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 02012a9d..f04b06f6 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -89,7 +89,6 @@ def _assumptions_of_use_impl(ctx): SphinxSourcesInfo( srcs = all_srcs, deps = depset(transitive = transitive), - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/private/component.bzl b/bazel/rules/rules_score/private/component.bzl index 60a9f109..bd070b6e 100644 --- a/bazel/rules/rules_score/private/component.bzl +++ b/bazel/rules/rules_score/private/component.bzl @@ -207,7 +207,6 @@ def _component_impl(ctx): SphinxSourcesInfo( srcs = req_sphinx_depset, deps = sphinx_depset, - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/private/dependability_analysis.bzl b/bazel/rules/rules_score/private/dependability_analysis.bzl index 68637d71..dc5b6287 100644 --- a/bazel/rules/rules_score/private/dependability_analysis.bzl +++ b/bazel/rules/rules_score/private/dependability_analysis.bzl @@ -30,23 +30,21 @@ 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, rst_ancillary_list, lobster_files): +def _collect_analysis_providers(sa, rst_srcs_list, rst_deps_list, lobster_files): """Collect analysis providers from a single sub-analysis target. Updates the provided lists/dicts in-place. Args: - 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_ancillary_list: List of depsets to extend with SphinxSourcesInfo.ancillary. - lobster_files: Dict to update with AnalysisInfo.lobster_files - (canonical name → File). + 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. + 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) - rst_ancillary_list.append(sa[SphinxSourcesInfo].ancillary) if AnalysisInfo in sa: lobster_files.update(sa[AnalysisInfo].lobster_files) @@ -76,7 +74,6 @@ def _dependability_analysis_impl(ctx): rst_srcs_transitive = [dfa_rst_files] rst_deps_transitive = [dfa_rst_files] - rst_ancillary_transitive = [] lobster_files = {} # canonical name → File, merged from all sub-analyses # ------------------------------------------------------------------------- @@ -85,7 +82,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, rst_ancillary_transitive, lobster_files) + _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, lobster_files) # ------------------------------------------------------------------------- # Collect from security_analysis targets @@ -93,7 +90,7 @@ 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, rst_ancillary_transitive, lobster_files) + _collect_analysis_providers(sa, rst_srcs_transitive, rst_deps_transitive, lobster_files) # Architectural design sphinx deps (optional) if ctx.attr.arch_design and SphinxSourcesInfo in ctx.attr.arch_design: @@ -185,7 +182,6 @@ def _dependability_analysis_impl(ctx): SphinxSourcesInfo( srcs = all_rst_srcs, deps = all_rst_deps, - ancillary = depset(transitive = rst_ancillary_transitive), ), ] diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index d97c124b..94835674 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -121,7 +121,7 @@ _INTEGRITY_LEVEL_RANK = {level: rank for rank, level in enumerate(_INTEGRITY_LEV # ============================================================================ def _get_sphinx_files(target): - return target[SphinxSourcesInfo].srcs.to_list() + return target[SphinxSourcesInfo].deps.to_list() def _filter_doc_files(files): """Filter files to only include documentation files. @@ -249,13 +249,17 @@ def _process_artifact_files(ctx, artifact_name, label): output_files = [] index_refs = [] - # Get and filter files + # deps contains all files to symlink (own srcs + transitive children); + # srcs are the toctree entries for this rule only. all_files = _get_sphinx_files(label) doc_files = _filter_doc_files(all_files) if not doc_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) @@ -273,28 +277,13 @@ def _process_artifact_files(ctx, artifact_name, label): ) output_files.append(output_file) - # Add to index if it's a document file - if _is_document_file(artifact_file): + # Add to toctree index only for files directly owned by this rule. + if _is_document_file(artifact_file) and artifact_file.path in srcs_paths: doc_ref = (artifact_name + "/" + relative_path) \ .replace(".rst", "") \ .replace(".md", "") index_refs.append(doc_ref) - # Symlink ancillary files (present for sub-toctrees / .. uml:: resolution, - # but NOT added to the outer toctree index). - if SphinxSourcesInfo in label: - for anc_file in label[SphinxSourcesInfo].ancillary.to_list(): - if anc_file.extension not in ["rst", "md", "puml", "plantuml", "png", "svg", "inc", "json"]: - continue - relative_path = _compute_relative_path(anc_file, _find_common_directory([anc_file])) - output_file = _create_artifact_symlink( - ctx, - artifact_name, - anc_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 3f2d1eae..87fd50d8 100644 --- a/bazel/rules/rules_score/private/fmea.bzl +++ b/bazel/rules/rules_score/private/fmea.bzl @@ -281,12 +281,13 @@ def _fmea_impl(ctx): for f in root_cause_lobster_files: lobster_files["root_causes.lobster"] = f - # detail_rsts are ancillary: they must be present next to fmea.rst for the - # sub-toctree to resolve, but they are NOT top-level toctree entries. + # 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. 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] + 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) @@ -301,7 +302,6 @@ def _fmea_impl(ctx): SphinxSourcesInfo( srcs = all_sphinx_srcs, deps = depset(transitive = sphinx_deps), - ancillary = depset(detail_rsts), ), ] diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index f887e88d..b1eac31e 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -123,7 +123,6 @@ def _requirements_impl(ctx): SphinxSourcesInfo( srcs = sphinx_srcs, deps = depset(transitive = transitive_sphinx), - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/private/unit.bzl b/bazel/rules/rules_score/private/unit.bzl index ee0517cd..c03e2b9d 100644 --- a/bazel/rules/rules_score/private/unit.bzl +++ b/bazel/rules/rules_score/private/unit.bzl @@ -96,7 +96,6 @@ def _unit_impl(ctx): SphinxSourcesInfo( srcs = all_files, deps = depset(transitive = [all_files] + sphinx_design_deps), - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/private/unit_design.bzl b/bazel/rules/rules_score/private/unit_design.bzl index 266e4362..1b0e4bc6 100644 --- a/bazel/rules/rules_score/private/unit_design.bzl +++ b/bazel/rules/rules_score/private/unit_design.bzl @@ -95,7 +95,6 @@ def _unit_design_impl(ctx): SphinxSourcesInfo( srcs = all_source_files, deps = all_source_files, - ancillary = depset(), ), ] diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 44ef0590..18900f72 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -49,11 +49,19 @@ SphinxSourcesInfo = provider( builds, including reStructuredText, Markdown, PlantUML diagrams, and image files. Rules that produce documentation artifacts should provide 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. """, fields = { - "srcs": "Depset of direct source files for Sphinx documentation (.rst, .md, .puml, .plantuml, .svg, .png, etc.)", - "deps": "Depset of transitive Sphinx source files collected from all direct and transitive dependencies.", - "ancillary": "Depset of files that must be physically present in the Sphinx tree (e.g. for sub-toctrees or .. uml:: directives) but are NOT top-level toctree entries.", + "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.", }, )