Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bazel/rules/rules_score/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ architectural_design(
**`bazel build`** — runs `puml_parser` on every `.puml` file, producing:
- a `.fbs.bin` FlatBuffers binary (diagram AST) — consumed by validation/core checks
- a `.lobster` traceability file (Interface elements only) — consumed by LOBSTER
- a `plantuml_links.json` — consumed by the `clickable_plantuml` Sphinx extension
- a `validation.log` from the `architectural-design` validation profile
- a `.idmap.json` sidecar — consumed by the `clickable_plantuml` Sphinx extension
to resolve cross-diagram links based on element *defines/references* roles

Diagrams in `public_api` are classified separately so their lobster items flow
through `public_api_lobster_files` for failure-mode traceability.
Expand Down
2 changes: 1 addition & 1 deletion bazel/rules/rules_score/docs/_assets/tooling_chain.puml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ rectangle "sphinx_module" <<rule>> as sphinx
' ── Tools ─────────────────────────────────────────────────────────────────────
rectangle "**TRLC**\ntrlc parser + trlc_rst\n(.trlc/.rsl -> .rst; TRLCRST lib)" <<tool>> as trlc
rectangle "**rst_to_trlc**\n(.rst -> .trlc)" <<own>> as r2t
rectangle "**PlantUML Parser**\nparser + linker (Rust)\n(.puml -> .fbs.bin + .lobster)" <<tool>> as puml
rectangle "**PlantUML Parser**\npuml_cli (Rust)\n(.puml -> .fbs.bin + .lobster + .idmap.json)" <<tool>> as puml
rectangle "**puml_cli** (FTA mode)\ninline metamodel + extract\n(.puml -> inlined .puml +\nfta_chains.json + root_causes.lobster)" <<own>> as fta
rectangle "**fmea_assembler**\nTRLCRST page build\n(.trlc + chains -> fmea.rst)" <<own>> as asm
rectangle "**Lobster**\nlobster-trlc / -report /\n-ci-report / gtest_report" <<tool>> as lob
Expand Down
20 changes: 15 additions & 5 deletions bazel/rules/rules_score/docs/tooling_architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,14 @@ are rendered under :doc:`tool_reference/index`.
- Converts RST requirement directives (``feat_req``, ``comp_req``, …) into
``.trlc`` records so requirements can be authored in either RST or TRLC.
* - **PlantUML Parser**
- ``@score_tooling//plantuml/parser:parser`` (Rust) + ``:linker``
- ``@score_tooling//plantuml/parser:parser`` (Rust)
- ``architectural_design``, ``unit_design``
- Parses ``.puml`` diagrams into a FlatBuffers AST (``.fbs.bin``, one
``root_type`` per diagram kind) and extracts interface ``.lobster``
items. The **linker** merges the FlatBuffers into ``plantuml_links.json``
for the ``clickable_plantuml`` Sphinx extension. Rejects syntactically
invalid diagrams with a non-zero exit code.
``root_type`` per diagram kind), extracts interface ``.lobster`` items,
and emits ``.idmap.json`` sidecars recording the *defines/references*
roles of each element. The ``clickable_plantuml`` Sphinx extension reads
these sidecars to resolve cross-diagram links without a separate linker
step. Rejects syntactically invalid diagrams with a non-zero exit code.
* - **puml_cli (FTA mode)**
- ``//plantuml/parser/puml_cli`` ``--fta-output-dir`` (Rust; FTA model in
the ``puml_fta`` crate)
Expand All @@ -121,6 +122,15 @@ are rendered under :doc:`tool_reference/index`.
parse: an overview table, one section per failure mode (detail + inline
fault tree + that chain's control measures), and trailing "Unlinked"
sections so nothing is dropped.
* - **safety_analysis_tools**
- ``//bazel/rules/rules_score:safety_analysis_tools``
(``src/safety_analysis_tools.py``, local)
- ``fmea``
- Assembles the failure-mode-centric ``fmea.rst`` from ``fta_chains.json``
plus the FailureMode / ControlMeasure records in one in-process TRLC
parse: an overview table, one section per failure mode (detail + inline
fault tree + that chain's control measures), and trailing "Unlinked"
sections so nothing is dropped.
* - **Lobster**
- ``@lobster//`` : ``lobster-trlc``, ``lobster-report``,
``lobster-ci-report``, ``lobster-html-report``, ``gtest_report``,
Expand Down
264 changes: 171 additions & 93 deletions plantuml/sphinx/clickable_plantuml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ Sphinx extension that makes PlantUML diagrams clickable by injecting hyperlinks

## Sphinx Integration

The extension hooks into the native Sphinx build lifecycle. URLs are computed by
`app.builder.get_relative_uri()`, which works for any builder and
output directory layout.
The extension hooks into the native Sphinx build lifecycle. URL computation
depends on the configured `plantuml_output_format`: in `svg_obj` mode the
rendered SVG lives in `_images/`, so links are made relative to that directory
(`os.path.relpath(target_uri, imagedir)`); for inline `svg`/`png` the link is
relative to the containing HTML page via
`app.builder.get_relative_uri(from_docname, to_docname)`.

```
Sphinx build lifecycle clickable_plantuml hooks
═══════════════════════════════════ ═══════════════════════════════════════

builder-inited ───► on_builder_inited()
│ (one-time setup) Load all *plantuml_links.json files
from srcdir (recursive).
Store {puml_basename → alias_map}
in app.env.
│ (one-time setup) Load all *.idmap.json files from
│ srcdir (recursive).
Build definition index:
{alias|id → [definer source paths]}.
├─ READ PHASE ──────────────────────────────────────────────────────────────
│ for each document:
Expand All @@ -40,10 +43,9 @@ Sphinx build lifecycle clickable_plantuml hooks
│ │
│ doctree-read ───► on_doctree_read()
│ (per document) Traverse the parsed doctree.
│ For every plantuml node that has a
│ filename attribute, record
│ {puml_basename → docname} in app.env.
│ Warn on basename collisions.
│ For every plantuml node, record
│ {normalized_source_path → docname}
│ in app.env (path identity, not basename).
│ env-merge-info ───► on_env_merge_info()
│ (parallel builds only) Merge puml→docname maps gathered
Expand All @@ -55,119 +57,195 @@ Sphinx build lifecycle clickable_plantuml hooks
│ post-transform / resolve
│ │
│ doctree-resolved ───► on_doctree_resolved()
│ (per document) For each plantuml node, look up the
│ alias_map from app.env.
│ Resolve target .puml → docname, then
│ call app.builder.get_relative_uri()
│ to get the correct relative URL.
│ Append url of <alias> is [[url]]
│ directives to node['uml'] before
│ sphinxcontrib-plantuml renders it.
│ (per document) For each plantuml node, load its idmap.
│ For each reference entry, look up the
│ definition index (FQN first, then alias).
│ Apply proximity tiebreak on ambiguity.
│ Build the URL (relative to _images/ in
│ svg_obj mode, else page-relative via
│ get_relative_uri), then append
│ url of <alias> is [[url]] directives to
│ node['uml'] before rendering.
build-finished
```

## How It Works

1. **Link discovery** (`builder-inited`) – Scans for `*plantuml_links.json` files in the Sphinx source directory.
2. **Diagram location mapping** (`doctree-read`) – As Sphinx reads each document, the extension traverses the parsed doctree to record which `docname` contains which `.puml` diagram (keyed by basename). Basename collisions across documents are reported as warnings.
3. **URL resolution & link injection** (`doctree-resolved`) – For each plantuml node, resolves target `.puml` references to the docname that contains the target diagram, generates a relative URL via `app.builder.get_relative_uri()`, and appends `url of <alias> is [[url]]` directives to the PlantUML source before rendering.
4. **Incremental / parallel support** – `env-purge-doc` removes stale entries when a document is re-read; `env-merge-info` merges state from parallel worker processes.
1. **idmap discovery** (`builder-inited`) – Scans for `*.idmap.json` files in
the Sphinx source directory. Each sidecar records *defines* (elements
elaborated in that diagram, i.e. with children/members) and *references*
(leaf mentions and relation endpoints). A global definition index maps
each alias/FQN to the set of diagrams that elaborate it.

2. **Diagram location mapping** (`doctree-read`) – Records which `docname`
contains which `.puml` diagram, keyed by the canonical workspace-relative
path. A node's identity is recovered by normalising its absolute path
(`srcdir` + the node's `incdir` + `filename`) and deriving exactly one
canonical key using the precomputed workspace offset from `builder-inited`.
Matching is exact canonical-key equality (no basename fallback, no suffix
fallback), so same-basename diagrams in different packages remain distinct.

3. **URL resolution & link injection** (`doctree-resolved`) – For each
reference in a diagram's idmap, resolves the unique definer via the index.
When multiple diagrams define the same element, a *proximity tiebreak*
selects the definer sharing the longest common path prefix with the source
diagram. On a genuine tie, no link is emitted (safe over wrong). URLs are
built relative to `_images/` in `svg_obj` mode (else page-relative via
`app.builder.get_relative_uri()`) and percent-encoded before injection.

4. **Incremental / parallel support** – `env-purge-doc` removes stale entries
when a document is re-read; `env-merge-info` merges state from parallel
worker processes.

## Invariants

The extension relies on two invariants held by the idmap producer
(`architectural_design()` / `puml_cli`):

1. **The `--source-name` must be the srcdir-relative workspace path.**
`--source-name` is passed to `puml_cli` and becomes the `source` field in
the idmap. This value is the canonical key used for all matching: a
plantuml node resolves only when its derived canonical key equals the idmap
`source` key exactly (normalized to POSIX form). The value must be stable
across builds and unique — non-unique `source` values raise an
`ExtensionError` rather than silently mislinking. In Bazel: pass
`puml_file.short_path` to satisfy this invariant regardless of how Sphinx
roots `srcdir` or how Bazel symlinks the staged sources.

2. **PlantUML basenames (file stems) must be unique within a single
`architectural_design` target.** Each `.idmap.json` is written as
`<file_stem>.idmap.json` under the target's output directory, so two
diagrams sharing a stem in one target would collide on output (build
error). Same basenames across *different* targets/packages are fine —
exact canonical-key matching via `--source-name` keeps them independent.

## Automatic idmap Generation (Bazel)

`.idmap.json` sidecars are produced by the `architectural_design()` rule.

The rule passes `--source-name <puml_file.short_path>` and
`--idmap-output-dir` to `puml_cli` for every `.puml` file. The
`--source-name` argument **must be the srcdir-relative workspace path** to
satisfy the canonical-key invariant. Passing `short_path` (the workspace
root-relative path for Bazel sources) ensures this requirement is met. The
resulting idmap `source` field is a stable, unique, workspace-relative path
(e.g. `score/mw/com/proxy_detail.puml`), which becomes the diagram's identity
key throughout the extension. Duplicate `source` values across idmaps raise
an error; duplicate basenames (file stems) within one target also raise an
error.

### Role detection algorithm

Given the resolved model of one `.puml` diagram:

1. **defines** – An element is a *define* when any of the following hold:
- At least one other element lists it as its `parent_id` (component diagrams).
- It has member variables or methods (class diagrams).
- The diagram's `@startuml <name>` matches its alias or display name
(component and class diagrams).
- It is a `$TopEvent` node — the tree root whose `connection` is `None`,
never used as a relation source (FTA diagrams).
2. **references** – Elements that link away to another diagram:
- Top-level leaf boxes and relation endpoints (component diagrams).
- All participants (sequence diagrams — no defines in sequence).
- `$TransferInGate` nodes whose alias is a TRLC-style FQN
(`Package.Record`) referencing another diagram's top event (FTA diagrams).
- Internal FTA nodes (`$BasicEvent`, `$IntermediateEvent`, `$AndGate`,
`$OrGate`) are omitted — they do not cross-link to other diagrams.

### Concrete example

## Automatic JSON Generation (Bazel)
```text
' overview.puml — top-level leaves are REFERENCES
@startuml
[Gateway] --> [Proxy]
@enduml
```

`plantuml_links.json` is generated by the `architectural_design()` rule.
```text
' proxy_detail.puml — Proxy has a child → DEFINE
@startuml
package Proxy { [RequestHandler] }
@enduml
```

The `architectural_design()` rule invokes `//tools/plantuml/linker:linker` on all
`.fbs.bin` FlatBuffers files produced by the PlantUML parser. See
[Link Mapping Format](#link-mapping-format) for a detailed
description of which links are emitted.
`proxy_detail.idmap.json`:
```json
{ "source": "score/mw/com/proxy_detail.puml",
"defines": [{ "alias": "Proxy", "id": "Proxy" }],
"references": [{ "alias": "RequestHandler", "id": "Proxy.RequestHandler" }] }
```

### Algorithm
`overview.idmap.json`:
```json
{ "source": "score/overview.puml",
"defines": [],
"references": [{ "alias": "Gateway", "id": "Gateway" },
{ "alias": "Proxy", "id": "Proxy" }] }
```

Given the set of `.fbs.bin` files for one `architectural_design()` target:
Result: `Proxy` in `overview.puml` links to `proxy_detail.puml`.
`Gateway` has no definer → no link.

1. **Build a top-level index** – For each diagram, collect every component whose
`parent_id` is `None` (i.e. it is not nested inside another component).
The index maps `alias → diagram file`.
## idmap Format

2. **Emit links** – For every component in every diagram, look up its alias in
the top-level index. If a *different* diagram defines that alias as a
top-level component, emit a link entry:
`.idmap.json` files are written by the parser and read by this extension.
They are not intended to be authored manually.

```
source_file = diagram that contains the reference
source_id = alias of the component
target_file = diagram that defines it as a top-level component
```
```json
{
"source": "path/to/diagram.puml",
"defines": [
{ "alias": "ComponentName", "id": "fully.qualified.Name" }
],
"references": [
{ "alias": "OtherComponent", "id": "OtherComponent" }
]
}
```

3. **Deduplicate** – Sort and deduplicate so that each `(source_file, source_id)`
pair has exactly one target (first alphabetically). Duplicate `source_id`
entries within the same source diagram are removed because PlantUML's
`url of X is [[…]]` directive supports only one URL per alias.
## End-to-End Clickable Diagram Example

### Concrete Example
This minimal example shows what users should create in docs to get a clickable
diagram:

`docs/arch/overview.puml`
```text
' adas_overview.puml — subsystem context
@startuml
component ADAS
component BrakeController
component LaneKeepAssist
ADAS --> BrakeController
ADAS --> LaneKeepAssist
[Gateway] --> [Proxy]
@enduml
```

`docs/arch/proxy_detail.puml`
```text
' brake_controller.puml — component detail
@startuml
component BrakeController
interface BrakeDemandIF
interface WheelSpeedIF
BrakeController --> BrakeDemandIF
BrakeController <-- WheelSpeedIF
package Proxy {
[RequestHandler]
}
@enduml
```

Generated links — one in each direction:
`docs/arch/overview.rst`
```rst
Overview
========

```json
{
"links": [
{
"source_file": "adas_overview.puml",
"source_id": "BrakeController",
"target_file": "brake_controller.puml"
},
{
"source_file": "brake_controller.puml",
"source_id": "BrakeController",
"target_file": "adas_overview.puml"
}
]
}
.. uml:: overview.puml
```

Clicking `BrakeController` in the overview navigates to its detail diagram;
clicking it in the detail diagram navigates back to the overview.
`docs/arch/proxy_detail.rst`
```rst
Proxy Detail
============

`ADAS` and `LaneKeepAssist` appear as top-level only in `adas_overview.puml` and
have no dedicated detail diagram, so **no links** are emitted for them.
.. uml:: proxy_detail.puml
```

(link-mapping-format)=
## Link Mapping Format
When the idmaps contain:

Place one or more `*plantuml_links.json` filesinside the Sphinx source directory:
- `overview.puml` references `Proxy`
- `proxy_detail.puml` defines `Proxy`

```json
{
"links": [
{
"source_file": "my_diagram.puml",
"source_id": "ComponentA",
"target_file": "other_diagram.puml"
}
]
}
```
the rendered `Proxy` element in `overview.puml` becomes clickable and opens
the page containing `proxy_detail.puml`.
Loading