Architecture
This page is the developer-facing map of rules_score: how the Bazel
rules/macros are wired together, which tool each rule invokes, how the
tools interrelate, and what every tool does. It complements the user-facing
Overview (rule catalogue) and Integration Guide (how to consume
the rules).
Three layers
rules_score is organised in three layers. A user only touches layer 1; the
build wires layers 2 and 3 automatically.
Macros / rules (Starlark,
private/*.bzl) — the public work-product declarations (feature_requirements,architectural_design,fmea,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,SphinxSourcesInfo). The provider graph is the “API” between rules; see Overview for the provider-flow diagram.Tools — the executables each action runs. Some are vendored third-party tools (TRLC, Lobster, the PlantUML parser, Sphinx); some are local helpers under
src/(rst_to_trlc.py,fmea_assembler.py,sphinx_html_merge.py). The FMEA fault-tree processing lives in the Rustpuml_cli(FTA mode, backed by thepuml_ftacrate).
Rule → tool invocation map
The figure below shows, for each rule, the tool(s) it drives during
bazel build / bazel test. Green boxes are local helpers shipped in this
repository; blue boxes are external tools.
Tool inventory
Every tool below is also registered as a ToolQualification.Tool record in
trlc/config/tools.trlc so it can be referenced from use cases, potential
errors and tool requirements (see Tool Qualification). Per-tool READMEs
are rendered under Tool Reference.
Tool |
Binary / label |
Invoked by |
Function |
|---|---|---|---|
Bazel |
build system |
all rules |
Orchestrates the action graph, propagates providers, enforces hermetic
builds, and runs the analysis-time certified-scope and
integrity-level safety checks in |
TRLC |
|
|
Parses and type-checks requirement / FMEA records against the |
rst_to_trlc |
|
|
Converts RST requirement directives ( |
PlantUML Parser |
|
|
Parses |
puml_cli (FTA mode) |
|
|
Analysis only: parses the |
fmea_assembler |
|
|
Assembles the failure-mode-centric |
Lobster |
|
|
The traceability backbone. |
Architecture Verifier |
|
|
Compares the current architecture (components/units collected from the
implementation tree via an aspect, serialised to |
Sphinx (Docs) |
|
|
Two-phase documentation build: phase 1 ( |
Lobster Bazel |
|
|
Scans source files (C/C++, Rust, Python, Starlark, TRLC) for
single-line tracing tags ( |
Manual Analysis |
|
standalone qualification activity |
Captures human review verdicts against an analysis spec, locks them to a
committed lock/results file, and emits the verdict as |
Note
tools.trlc also registers placeholder tools that are not yet wired into
any action: PlantumlLinter, PlantumlFormatter, LibClang (planned
C/C++ design extraction), BazelMetamodel (the FlatBuffers architecture
schema, currently embedded in the parser/verifier), and AIChecker. They
exist so use cases and tool requirements can already reference them.
How the tools interrelate — the traceability data flow
The unifying idea is that every artefact is reduced to a ``.lobster`` file,
and all .lobster files are aggregated and gated by Lobster. Different tools
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/controlmeasures.trlc) →lobster-trlc; FTA (fta.puml) →puml_cli(FTA mode) →root_causes.lobster.Unit tests (gtest) →
gtest_report→<unit>.lobster.
Two-phase Sphinx build
Every sphinx_module call expands into two Bazel targets that run
sequentially:
<name>_needs (phase 1 — needs builder)
<name> (phase 2 — HTML builder)
Phase 1 — <name>_needs
Sphinx is invoked with --builder needs against the static docs/ source
tree (only srcs files — the checked-in .rst/.md files plus the
generated trlc_rst outputs that are listed as label targets in srcs).
Generated/external files from renamed_srcs and docs_library_deps are
not included; their toctree entries produce toc.not_readable warnings
that are suppressed in conf.template.py (see below for why this is safe).
The needs builder scans every document for .. need:: directives
(sphinx-needs native format) and writes them to a needs.json file.
The toc.not_readable suppression in conf.template.py is safe for the
HTML phase because that phase relocates every file into a staging directory, so
it never encounters an unresolvable toctree entry.
The resulting needs.json (empty for modules whose requirements are authored
in TRLC rather than native sphinx-needs format) is wrapped in a
SphinxNeedsInfo provider and propagated transitively so that every
downstream module can consume it.
Phase 2 — <name>
Sphinx is invoked with --builder html against a relocated copy of all
source files (srcs, renamed_srcs, docs_library_deps) symlinked into
a unified staging directory under bazel-bin/.
This is also where .. requirement:definition:: directives (from the trlc
Sphinx extension) are processed and cross-references resolved. The raw
requirement records come from .trlc source files compiled by the
trlc_rst Bazel rule into .rst files that contain the directives. The
chain is:
*.trlc
└─ trlc_rst (Bazel rule, @trlc)
└─ requirements_rst.rst (.. requirement:definition:: <ID> ...)
└─ Sphinx HTML builder (resolves {requirement:downstream-ref})
Before the HTML build starts, sphinx_module_ext.py reads the aggregated
needs_external_needs.json
(written by the Bazel rule from all incoming SphinxNeedsInfo providers) and
populates the needs_external_needs Sphinx configuration key. This tells
sphinx-needs where to find the needs.json of each dependency and what
base URL to use for generated hyperlinks, so a {requirement:downstream-ref}
role in a spec file can link directly to the requirement definition page in the
dependency’s HTML.
After the HTML build, src/sphinx_html_merge.py copies each dependency’s
output directory into <name>/html/<dep-name>/ so the final site is
self-contained.
deps[*].needs.json ──► needs_external_needs.json
│
sources (all relocated) ───────┤
▼
Sphinx HTML builder
│
<name>/_html/ ──► sphinx_html_merge
│
<name>/html/
├── index.html
├── dep1/ ← merged
└── dep2/ ← merged
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, fmea.template.rst and
the lobster configs) — flow through the three in-process tool actions of the
fmea rule 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
consumed by Sphinx.
The fmea rule drives three actions, all reading the input artifacts above:
puml_cli (FTA mode) parses each
fta_*.pumldirectly (no rewriting) and writesroot_causes.lobsterandfta_chains.json(the ordered per-failure-mode chains). 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.fmea_assembler consumes
fta_chains.jsonand parses the FailureMode / ControlMeasure.trlcrecords (with the.rslspec for import resolution) in a single in-processTRLCRSTpass, expandingfmea.template.rstintofmea.rst.lobster-trlc (run twice) turns the FailureMode and ControlMeasure records into
failuremodes.lobster/controlmeasures.lobsterfor the traceability report.
SphinxSourcesInfo carries three depsets:
srcs — files that become top-level toctree entries in the enclosing document section.
fmeaemits exactly one:fmea.rst.deps — all files that must be present in the staging directory; for
fmeathis is justfmea.rst, because the page is self-contained (failure modes and control measures are rendered inline, not pulled in via.. include::).aux_srcs — files to symlink alongside
srcs/depsbut not added to any toctree.fmeauses this for the authoredfta_*.pumldiagrams, whichfmea.rstreferences 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, controlmeasures.lobster, root_causes.lobster)
into the dependability_analysis traceability report.
to the outer index toctree.
fmeauses this for thedetail_*.rstsub-pages, which are referenced from the inner.. toctree::insidefmea.rstrather than from the section index.
Hermetic tool path resolution
Background: Bazel action environment
Paths available at analysis time (Starlark ctx.executable.foo.path,
ctx.executable.foo.short_path) are always relative to the execroot —
the per-action working directory Bazel creates under the output base. Two
variants exist:
file.path—bazel-out/<config-hash>/bin/third_party/docs_runtime/dot. Contains the exec-configuration hash; valid only at action run-time as a path relative tocwd.file.short_path—third_party/docs_runtime/dot(or../external_repo/…). Hash-free; stable across rebuilds; the canonical rlocation key after stripping a leading../.
What the rule passes
For each tool sphinx_module.bzl computes both variants and injects them as
environment variables:
PLANTUML_BIN = ctx.executable._plantuml.path (execroot-relative)
PLANTUML_BIN_RLOC = ctx.executable._plantuml.short_path (rlocation key)
GRAPHVIZ_DOT = ctx.executable._graphviz.path (execroot-relative)
GRAPHVIZ_DOT_RLOC = ctx.executable._graphviz.short_path (rlocation key)
The rlocation keys (*_RLOC) are computed once at analysis time:
_gv_short = ctx.executable._graphviz.short_path
_graphviz_rloc = (
_gv_short[3:] # strip "../"
if _gv_short.startswith("../")
else ctx.workspace_name + "/" + _gv_short
)
This matches the Bazel runfiles convention used internally by the
exec_in_sysroot wrapper itself (rlocation
'<workspace>/third_party/docs_runtime/dot').
How conf.template.py resolves the paths
conf.py is loaded during Sphinx initialisation, before Sphinx performs
any os.chdir(). Bazel guarantees that the action’s cwd equals the
execroot at process start. Therefore a single os.path.abspath() call
converts the execroot-relative *_BIN / *_DOT value to a stable
absolute path for the entire action lifetime:
plantuml_path = os.path.abspath(os.environ["PLANTUML_BIN"])
graphviz_dot = os.path.abspath(os.environ["GRAPHVIZ_DOT"])
Why rlocation alone cannot resolve the binary
A natural question is: why not call
python.runfiles.Runfiles.Create().Rlocation(graphviz_rloc_key) directly and
skip the abspath step?
Two reasons make this impractical:
Wrong ``RUNFILES_DIR``. The Sphinx Python process inherits
RUNFILES_DIRpointing to the sphinx tool’s own runfiles tree (set by therules_pythonpy_binarylauncher). The Graphviz and PlantUML tools are intools=of the action, which makes their files available in the sandbox but does not merge them into the sphinx binary’s runfiles. CallingRunfiles.Create()without an override therefore searches the wrong tree and returnsNonefor both tool keys.The symlink-path problem. As a workaround one could construct
Runfiles.Create({"RUNFILES_DIR": abspath_dot + ".runfiles"})(the companion runfiles directory Bazel creates for every executable). The dot wrapper is a member of its own runfiles tree (exec_in_sysrootadds the generated script toctx.runfiles(files=[out, …])so the smoke test can resolve it viarlocation). However,Runfiles.Rlocation()returns the path inside the symlink forest, not the real binary path. Passing that symlink path to a subprocess means$0is the symlink, so$0.runfiles/does not exist and the wrapper’s runfiles bootstrap falls back toRUNFILES_DIR— which still points to the sphinx binary’s runfiles. The wrapper fails to findSYSROOT_DIRandFAKECHROOT_WRAPPERand exits with an error.
The os.path.abspath() approach avoids both issues: it yields the real
binary path (not a symlink in a runfiles forest), so $0.runfiles/
bootstrapping in the wrapper works correctly.
The exec_in_sysroot wrapper’s own runfiles bootstrap
The generated wrapper produced by exec_in_sysroot is a POSIX-sh script
that contains an inline runfiles initialisation block (no bash and no
runfiles.bash dependency):
if [ ! -d "${RUNFILES_DIR:-/dev/null}" ] && \
[ ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]; then
if [ -f "$0.runfiles_manifest" ]; then
RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"; export RUNFILES_MANIFEST_FILE
elif [ -f "$0.runfiles/MANIFEST" ]; then
RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"; export RUNFILES_MANIFEST_FILE
elif [ -d "$0.runfiles" ]; then
RUNFILES_DIR="$0.runfiles"; export RUNFILES_DIR
fi
fi
Because graphviz_dot is the absolute path to the real binary (not a
symlink), $0 equals that absolute path, and $0.runfiles/ is the actual
companion runfiles directory Bazel created for the binary. The block resolves
RUNFILES_DIR (or RUNFILES_MANIFEST_FILE) from $0.runfiles/, and a
small inline rlocation helper then looks up SYSROOT_DIR and the
fakechroot wrapper — regardless of what RUNFILES_DIR the parent process
(Sphinx/Python) has set in its environment.
PlantUML and the hermetic dot
sphinxcontrib.plantuml invokes PlantUML via
shlex.split(app.config.plantuml). The plantuml config value always
points at the hermetic dot (there is no fallback):
<abs_plantuml_path> -graphvizdot <abs_graphviz_dot>
This tells PlantUML to use the hermetic dot for its internal layout calls
(PlantUML generates a .dot intermediate for class/component/sequence
diagrams and hands it to graphviz for layout).