From 5fcc1fcadff870289f3a60c843166d60b5a0f9f8 Mon Sep 17 00:00:00 2001 From: curious-turtle Date: Fri, 18 Sep 2026 16:23:00 +0530 Subject: [PATCH] Add static_view section to architectural_design Separation of static_view related changes from PR#437 Other changes related to toc tree generation and naming collision avoidance are already implemented in some other way,shape or form so that will need more rework to reimplement. This commit is just to get the static_view changes Added rust validation core Added: Test example to test everything Modified: Docs Modified: Bazel to wire everything --- bazel/rules/rules_score/README.md | 19 ++ .../rules/rules_score/docs/rule_reference.rst | 11 +- .../private/architectural_design.bzl | 38 +++- bazel/rules/rules_score/private/views.bzl | 1 + bazel/rules/rules_score/providers.bzl | 3 +- bazel/rules/rules_score/test/BUILD | 61 ++++++ .../clickable_example/static_view_detail.puml | 20 ++ .../static_view_failure_modes.trlc | 24 +++ .../clickable_example/static_view_fta.puml | 20 ++ .../static_view_overview.puml | 25 +++ .../clickable_example/static_view_static.puml | 23 +++ validation/core/BUILD | 2 + validation/core/README.md | 12 +- .../src/models/component_diagram_models.rs | 37 +++- validation/core/src/models/mod.rs | 2 +- .../core/src/profiles/architectural_design.rs | 21 +- validation/core/src/validators/mod.rs | 2 + .../static_view_consistency_validator.rs | 167 ++++++++++++++++ .../static_view_consistency_validator_test.rs | 182 ++++++++++++++++++ 19 files changed, 649 insertions(+), 21 deletions(-) create mode 100644 bazel/rules/rules_score/test/fixtures/clickable_example/static_view_detail.puml create mode 100644 bazel/rules/rules_score/test/fixtures/clickable_example/static_view_failure_modes.trlc create mode 100644 bazel/rules/rules_score/test/fixtures/clickable_example/static_view_fta.puml create mode 100644 bazel/rules/rules_score/test/fixtures/clickable_example/static_view_overview.puml create mode 100644 bazel/rules/rules_score/test/fixtures/clickable_example/static_view_static.puml create mode 100644 validation/core/src/validators/static_view_consistency_validator.rs create mode 100644 validation/core/src/validators/test/static_view_consistency_validator_test.rs diff --git a/bazel/rules/rules_score/README.md b/bazel/rules/rules_score/README.md index 36b58507..717b48a1 100644 --- a/bazel/rules/rules_score/README.md +++ b/bazel/rules/rules_score/README.md @@ -98,6 +98,25 @@ architectural_design( Diagrams in `public_api` are classified separately so their lobster items flow through `public_api_lobster_files` for failure-mode traceability. +`static_view` is an optional additional section for component diagrams that +present a partial view of the static architecture (e.g. a diagram scoped to a +subsystem). Diagrams passed to `static_view` are parsed like `static`, but are +never used to define the units/components validated against the Bazel +component graph. Instead, every component/unit defined in a `static_view` +diagram must also be defined, under the same parent, in `static`: it may only +contain a subset of the units/components of the matching `static` diagram. +**`bazel build`** fails if a `static_view` diagram introduces a +component/unit that is not present in `static`. + +The `static_view` section can be used for creating additional diagrams that +provide a view onto the architecture which make the design easier to view / understand. +E.g. you can create a diagram which shows a subset of components as showing all +components in one view may be too "busy". It can also be useful when showing the +interfaces between components. Adding all the interfaces in the diagrams in the +`static` view may result in too many interface lines which is not readable. Instead, +a view can be created with a subset of components and only the interfaces between these +chosen components can be shown. + --- ## `unit` diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index ff28409a..961dcbe9 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -390,15 +390,16 @@ Example glossary source (``.rst``): architectural_design ~~~~~~~~~~~~~~~~~~~~ -Bundles static, dynamic, public-API, and internal-API architecture views into a -single target. Provides ``ArchitecturalDesignInfo`` consumed by ``dependable_element`` -and ``fmea``. +Bundles static, dynamic, static-view, public-API, and internal-API architecture +views into a single target. Provides ``ArchitecturalDesignInfo`` consumed by +``dependable_element`` and ``fmea``. .. code-block:: python architectural_design( name = "arch", static = ["docs/static_design.puml"], + static_view = ["docs/subsystem_view.puml"], dynamic = ["docs/sequence.puml"], public_api = ["docs/public_api.puml"], internal_api = ["docs/internal_api.puml"], @@ -432,6 +433,10 @@ and ``fmea``. - label list - no - Internal-API diagram files (``.puml``) describing interfaces exposed between components inside the SEooC; their FlatBuffers output is exposed via ``ArchitecturalDesignInfo.internal_api`` for downstream validation (default ``[]``) + * - ``static_view`` + - label list + - no + - Component diagrams (``.puml``, ``.plantuml``) that present a partial view of the static architecture. These can be used to create smaller diagrams which highlight a subset of all components / units to improve readability / understandability. Components and units defined in a static view must also be defined under the same parent in ``static`` (default ``[]``) * - ``maturity`` - string - no diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index 807c9fec..def1f1cd 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -198,7 +198,7 @@ def _colocate_view_files(ctx, staged_files, view_output_dir): colocated[relative_path] = copy return colocated -def _run_validation(ctx, component_fbs_files, sequence_fbs_files, public_api_fbs_files, internal_api_fbs_files): +def _run_validation(ctx, component_fbs_files, sequence_fbs_files, public_api_fbs_files, internal_api_fbs_files, static_view_fbs_files): """Run the architectural-design validation profile. Args: @@ -207,6 +207,7 @@ def _run_validation(ctx, component_fbs_files, sequence_fbs_files, public_api_fbs sequence_fbs_files: Sequence-diagram FlatBuffer files generated from this target's dynamic inputs. public_api_fbs_files: List of public-API FlatBuffer files generated from this target's public_api inputs. internal_api_fbs_files: List of internal-API FlatBuffer files generated from this target's internal_api inputs. + static_view_fbs_files: Component-diagram FlatBuffer files generated from this target's static_view inputs. Returns: Struct with file and name fields describing the validation log entry. """ @@ -220,8 +221,9 @@ def _run_validation(ctx, component_fbs_files, sequence_fbs_files, public_api_fbs "sequence_diagrams": [f.path for f in sequence_fbs_files], "public_api_diagrams": [f.path for f in public_api_fbs_files], "internal_api_diagrams": [f.path for f in internal_api_fbs_files], + "static_view": [f.path for f in static_view_fbs_files], }, - inputs = component_fbs_files + sequence_fbs_files + public_api_fbs_files + internal_api_fbs_files, + inputs = component_fbs_files + sequence_fbs_files + public_api_fbs_files + internal_api_fbs_files + static_view_fbs_files, mnemonic = "ArchitecturalDesignValidate", maturity = ctx.attr.maturity, log_level = get_log_level(ctx), @@ -247,10 +249,10 @@ def _architectural_design_impl(ctx): # All diagrams of this target share one flat fbs/lobster/idmap namespace # (keyed by ctx.label.name), so stems must be disambiguated across all - # four views together, not per-view. + # views together, not per-view. stems = _disambiguated_stems( ctx, - ctx.files.static + ctx.files.dynamic + ctx.files.public_api + ctx.files.internal_api, + ctx.files.static + ctx.files.dynamic + ctx.files.public_api + ctx.files.internal_api + ctx.files.static_view, ) view_fbs = {} @@ -319,16 +321,17 @@ def _architectural_design_impl(ctx): public_api_fbs = depset(view_fbs["public_api"]) internal_api_fbs = depset(view_fbs["internal_api"]) public_api_lobster = depset(view_lobster["public_api"]) + static_view_fbs = depset(view_fbs["static_view"]) all_source_files = depset(transitive = view_source_files) - # All idmap sidecars (across static/dynamic/public_api/internal_api) are + # All idmap sidecars (across static/dynamic/public_api/internal_api/static_view) are # staged into the sphinx sources so the `clickable_plantuml` extension can # discover them (it scans `srcdir` recursively for `*.idmap.json`) and # resolve cross-diagram links — including component diagrams linking to # the class diagrams that elaborate their public/internal API interfaces. all_idmap_files = depset( - view_idmap["static"] + view_idmap["dynamic"] + view_idmap["public_api"] + view_idmap["internal_api"], + view_idmap["static"] + view_idmap["dynamic"] + view_idmap["public_api"] + view_idmap["internal_api"] + view_idmap["static_view"], ) sphinx_files = depset( @@ -341,6 +344,7 @@ def _architectural_design_impl(ctx): view_fbs["dynamic"], view_fbs["public_api"], view_fbs["internal_api"], + view_fbs["static_view"], ) # `deps` carries everything needed in the Sphinx tree for this rule @@ -360,6 +364,7 @@ def _architectural_design_impl(ctx): dynamic = dynamic_fbs, public_api = public_api_fbs, internal_api = internal_api_fbs, + static_view = static_view_fbs, view_root_indexes = view_root_indexes, name = ctx.label.name, public_api_lobster_files = public_api_lobster, @@ -406,6 +411,16 @@ def _architectural_design_attrs(): "Classified separately so their FlatBuffers outputs are exposed via " + "ArchitecturalDesignInfo.internal_api for downstream validation.", ), + "static_view": attr.label_list( + allow_files = [".puml", ".plantuml"], + mandatory = False, + doc = "Component diagrams that present a partial view of the static architecture. " + + "Parsed identically to `static`, but never used to define the units/components " + + "validated against the Bazel component graph. Instead, every component/unit " + + "defined here must also be defined, under the same parent, in `static`; " + + "the build fails if a `static_view` diagram introduces a component/unit that is " + + "not present in the `static` diagrams.", + ), "maturity": attr.string( default = "release", values = ["release", "development"], @@ -444,6 +459,7 @@ def architectural_design( dynamic = [], public_api = [], internal_api = [], + static_view = [], maturity = "release", **kwargs): """Define architectural design following S-CORE process guidelines. @@ -478,6 +494,15 @@ def architectural_design( static/dynamic diagrams but classified separately so their FlatBuffers outputs are exposed via ArchitecturalDesignInfo. internal_api for downstream validation. + static_view: Optional list of .puml component diagrams that present a + partial view of the static architecture. These are parsed + identically to `static`, but are not used to define the + units/components validated against the Bazel component graph. + Instead, every component/unit defined in a `static_view` diagram + must also be defined, under the same parent, in `static`: it may + only contain a subset of the units/components of the matching + `static` diagram. The build fails if a `static_view` diagram + introduces a component/unit that is not present in `static`. maturity: Maturity level of the architectural design. Use "development" to write validation findings without failing the Bazel action. @@ -510,6 +535,7 @@ def architectural_design( dynamic = dynamic, public_api = public_api, internal_api = internal_api, + static_view = static_view, maturity = maturity, **kwargs ) diff --git a/bazel/rules/rules_score/private/views.bzl b/bazel/rules/rules_score/private/views.bzl index 2a305010..b75856a0 100644 --- a/bazel/rules/rules_score/private/views.bzl +++ b/bazel/rules/rules_score/private/views.bzl @@ -28,4 +28,5 @@ ARCH_VIEWS = [ ("dynamic", "Dynamic Design"), ("public_api", "Public API"), ("internal_api", "Internal API"), + ("static_view", "Static View"), ] diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 7a612d1b..ef2de001 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -204,7 +204,8 @@ ArchitecturalDesignInfo = provider( "dynamic": "Depset of FlatBuffers binaries for dynamic architecture diagrams (sequence diagrams, activity diagrams, etc.)", "public_api": "Depset of FlatBuffers binaries for public API diagrams (class diagrams, etc.)", "internal_api": "Depset of FlatBuffers binaries for internal API diagrams (class diagrams, etc.)", - "view_root_indexes": "Dict mapping view name ('static', 'dynamic', 'public_api', 'internal_api') to that view's single top-level toctree-entry File (see emit_view_navigation's root_index), or None for views with no navigable files.", + "static_view": "Depset of FlatBuffers binaries for static_view component diagrams (partial views of the static architecture, validated for consistency against static).", + "view_root_indexes": "Dict mapping view name ('static', 'dynamic', 'public_api', 'internal_api', 'static_view') to that view's single top-level toctree-entry File (see emit_view_navigation's root_index), or None for views with no navigable files.", "name": "Name of the architectural design target", "public_api_lobster_files": "Depset of .lobster traceability files generated from public_api diagrams.", "validation_logs": "List of validation log entries produced by this architectural design target. Each entry has file and name fields.", diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 5df54825..19870b5e 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -600,6 +600,67 @@ dependable_element( deps = [], ) +# Demonstrates architectural_design's `static_view` attribute: `static_view` +# diagrams are parsed like `static`, but never define the units/components +# validated against the Bazel component graph. Instead, every component/unit +# defined in a `static_view` diagram must also be defined, under the same +# parent, in `static` -- here, static_view_overview.puml (component_sv with a +# single child unit_sv) is a consistent subset of static_view_static.puml +# (component_sv with unit_sv AND unit_sv_extra), so `bazel build` succeeds. +# The static-vs-static_view consistency check itself (including the +# build-failure case when a static_view diagram introduces an entity absent +# from static) is covered by validate_static_view_consistency's Rust unit +# tests in validation/core/src/validators/test/static_view_consistency_validator_test.rs. +# +# public_api is also wired in (static_view_detail.puml, defining +# `package_sv.SvInterface`) so this example also exercises the general +# "every public API interface item must be referenced by a FailureMode" +# traceability requirement (see fmea/dependability_analysis below) for a +# target that also uses `static_view` -- mirroring the fmea/dependability_ +# analysis wiring from the original static_view example fixture. +architectural_design( + name = "arch_design_static_view_example", + public_api = ["fixtures/clickable_example/static_view_detail.puml"], + static = ["fixtures/clickable_example/static_view_static.puml"], + static_view = ["fixtures/clickable_example/static_view_overview.puml"], +) + +# public_api items must be referenced by a FailureMode in the SEooC's own +# safety analysis - same requirement as public_api_example_fmea above. +fmea( + name = "static_view_example_fmea", + arch_design = ":arch_design_static_view_example", + failuremodes = ["fixtures/clickable_example/static_view_failure_modes.trlc"], + root_causes = ["fixtures/clickable_example/static_view_fta.puml"], +) + +dependability_analysis( + name = "static_view_example_dependability_analysis", + arch_design = ":arch_design_static_view_example", + fmea = [":static_view_example_fmea"], +) + +dependable_element( + name = "static_view_example_lib", + architectural_design = [":arch_design_static_view_example"], + assumptions_of_use = [":aous"], + components = [], + dependability_analysis = [":static_view_example_dependability_analysis"], + integrity_level = "B", + # Downgraded to warnings for two reasons: (1) this fixture's `static` + # diagram declares package_sv/component_sv/unit_sv purely to demonstrate + # the static_view attribute and its consistency check, with no matching + # Bazel component/unit targets (same reasoning as unit_example_lib + # above); (2) it intentionally reuses "SvInterface" as both a reference + # and a definition, which the dependable-element validator can't + # distinguish from an accidental duplicate id (same reasoning as + # clickable_example_lib above). + maturity = "development", + requirements = [":feat_req"], + tests = [], + deps = [], +) + # Live example of clickable_plantuml's "unit to class diagram" linking chain: # the *static* architecture (unit_overview.puml, a component diagram) shows # `unit_one` as a leaf unit (no children) - a reference - and diff --git a/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_detail.puml b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_detail.puml new file mode 100644 index 00000000..2a180ad7 --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_detail.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 SvInterface + +package package_sv { + interface "SvInterface" as SvInterface +} + +@enduml diff --git a/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_failure_modes.trlc b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_failure_modes.trlc new file mode 100644 index 00000000..3cf510f8 --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_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 StaticViewExampleFmea + +import ScoreReq + +ScoreReq.FailureMode SvInterfaceFailure { + guidewords = [ScoreReq.Guideword.LossOfFunction] + description = "SvInterface stops responding" + failureeffect = "Callers never receive a response" + version = 1 + safety = ScoreReq.Asil.B + interface = "package_sv.SvInterface" +} diff --git a/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_fta.puml b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_fta.puml new file mode 100644 index 00000000..aaedd8db --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_fta.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' 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 + +$TopEvent("SvInterface stops responding", "StaticViewExampleFmea.SvInterfaceFailure") + +@enduml diff --git a/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_overview.puml b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_overview.puml new file mode 100644 index 00000000..adc16afb --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_overview.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 static_view_overview + +package "Package Sv" as package_sv { + component "Component Sv" as component_sv <> { + component "Unit Sv" as unit_sv <> + } + + interface "SvInterface" as SvInterface + unit_sv -( SvInterface +} + +@enduml diff --git a/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_static.puml b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_static.puml new file mode 100644 index 00000000..76dffda0 --- /dev/null +++ b/bazel/rules/rules_score/test/fixtures/clickable_example/static_view_static.puml @@ -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 +' ******************************************************************************* + +@startuml static_view_static + +package "Package Sv" as package_sv { + component "Component Sv" as component_sv <> { + component "Unit Sv" as unit_sv <> + component "Unit Sv Extra" as unit_sv_extra <> + } +} + +@enduml diff --git a/validation/core/BUILD b/validation/core/BUILD index ef392594..d814867c 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -67,12 +67,14 @@ rust_library( "src/validators/shared/diagram_analysis.rs", "src/validators/shared/helpers.rs", "src/validators/shared/mod.rs", + "src/validators/static_view_consistency_validator.rs", "src/validators/test/class_design_sequence_validator_test.rs", "src/validators/test/component_internal_api_validator_test.rs", "src/validators/test/component_public_api_validator_test.rs", "src/validators/test/component_sequence_validator_test.rs", "src/validators/test/fixtures.rs", "src/validators/test/sequence_internal_api_validator_test.rs", + "src/validators/test/static_view_consistency_validator_test.rs", ], crate_root = "src/lib.rs", visibility = ["//visibility:public"], diff --git a/validation/core/README.md b/validation/core/README.md index 0aff1224..f162e660 100644 --- a/validation/core/README.md +++ b/validation/core/README.md @@ -79,6 +79,7 @@ Profile validators: `architectural-design`: - `validate_component_sequence` +- `validate_static_view_consistency` `dependable-element`: - `validate_bazel_component` @@ -105,10 +106,19 @@ Each profile owns its own input schema. "component_diagrams": ["path/to/component.fbs.bin"], "sequence_diagrams": ["path/to/sequence.fbs.bin"], "internal_api": ["path/to/internal_api.fbs.bin"], - "public_api": ["path/to/public_api.fbs.bin"] + "public_api": ["path/to/public_api.fbs.bin"], + "static_view": ["path/to/static_view_component.fbs.bin"] } ``` +`static_view` are parsed component-diagram outputs representing a partial +"view" onto the architecture in `component_diagrams` (the `static` section). +`validate_static_view_consistency` fails if a `static_view` diagram defines a +component/unit that is not also defined, under the same parent, in the +`static` diagrams. A component/unit may be declared in more than one +`static_view` diagram (e.g. overlapping views); such repetition across +`static_view` diagrams is not treated as a duplicate-entity error. + `unit`: ```json diff --git a/validation/core/src/models/component_diagram_models.rs b/validation/core/src/models/component_diagram_models.rs index 2e06aacd..de6218b0 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -70,7 +70,21 @@ impl ComponentDiagramInputs { &self, result: &mut ValidationResult, ) -> ComponentDiagramArchitecture { - ComponentDiagramArchitecture::from_entities(&self.entities, result) + ComponentDiagramArchitecture::from_entities(&self.entities, result, true) + } + + /// Build a [`ComponentDiagramArchitecture`] index from these diagram + /// inputs without reporting duplicate-entity errors. + /// + /// Used for `static_view` diagrams: multiple `static_view` files may + /// legitimately reference the same entity (e.g. overlapping partial + /// views), so duplicates across those files are not errors. Consistency + /// with the `static` diagram is checked separately. + pub fn to_static_view_architecture( + &self, + result: &mut ValidationResult, + ) -> ComponentDiagramArchitecture { + ComponentDiagramArchitecture::from_entities(&self.entities, result, false) } } @@ -96,14 +110,22 @@ impl ComponentDiagramArchitecture { /// `<>` go into `seooc_set`; /// `<>` go into `comp_set`; /// `<>` go into `unit_set`. - /// Duplicates (same [`EntityKey`]) are reported via `result`. - fn from_entities(entities: &[LogicComponent], result: &mut ValidationResult) -> Self { + /// Duplicates (same [`EntityKey`]) are reported via `result`, unless + /// `report_duplicates` is `false`. + fn from_entities( + entities: &[LogicComponent], + result: &mut ValidationResult, + report_duplicates: bool, + ) -> Self { // Index by raw id for parent resolution; PlantUML nesting uses id, // not alias. let mut id_index: BTreeMap = BTreeMap::new(); for entity in entities { let key = entity.id.to_lowercase(); if let Some(prev) = id_index.insert(key.clone(), entity) { + if !report_duplicates { + continue; + } let kind = entity_kind_name(entity); let alias = entity.match_key(); let parent = @@ -144,9 +166,9 @@ impl ComponentDiagramArchitecture { let filtered_component_count = components.len(); let filtered_unit_count = units.len(); - let seooc_set = Self::build_set(&seoocs, &id_index, result); - let comp_set = Self::build_set(&components, &id_index, result); - let unit_set = Self::build_set(&units, &id_index, result); + let seooc_set = Self::build_set(&seoocs, &id_index, result, report_duplicates); + let comp_set = Self::build_set(&components, &id_index, result, report_duplicates); + let unit_set = Self::build_set(&units, &id_index, result, report_duplicates); Self { seooc_set, @@ -163,6 +185,7 @@ impl ComponentDiagramArchitecture { items: &[&LogicComponent], id_index: &BTreeMap, result: &mut ValidationResult, + report_duplicates: bool, ) -> BTreeMap { let mut set = BTreeMap::new(); for entity in items { @@ -195,7 +218,7 @@ impl ComponentDiagramArchitecture { }; let key = (alias, parent_alias); if let Some(prev) = set.insert(key.clone(), (*entity).clone()) { - if prev.id.eq_ignore_ascii_case(&entity.id) { + if !report_duplicates || prev.id.eq_ignore_ascii_case(&entity.id) { continue; } let kind = entity_kind_name(entity); diff --git a/validation/core/src/models/mod.rs b/validation/core/src/models/mod.rs index 221543e5..1095e703 100644 --- a/validation/core/src/models/mod.rs +++ b/validation/core/src/models/mod.rs @@ -19,7 +19,7 @@ mod component_diagram_models; mod sequence_diagram_models; mod shared; -use shared::EntityKey; +pub use shared::EntityKey; #[cfg(test)] pub use bazel_models::BazelInputEntry; diff --git a/validation/core/src/profiles/architectural_design.rs b/validation/core/src/profiles/architectural_design.rs index d6f032e9..26493dd2 100644 --- a/validation/core/src/profiles/architectural_design.rs +++ b/validation/core/src/profiles/architectural_design.rs @@ -18,7 +18,7 @@ use crate::models::{ use crate::readers::{ClassDiagramReader, ComponentDiagramReader, SequenceDiagramReader}; use crate::validators::{ validate_component_internal_api, validate_component_public_api, validate_component_sequence, - validate_sequence_internal_api, + validate_sequence_internal_api, validate_static_view_consistency, }; use crate::ValidationResult; use serde::Deserialize; @@ -34,6 +34,7 @@ pub struct ArchitecturalDesignInputs { sequence_diagrams: Vec, internal_api_diagrams: Vec, public_api_diagrams: Vec, + static_view: Vec, } fn registered_validators<'a>( @@ -41,6 +42,7 @@ fn registered_validators<'a>( sequence: &'a Option, internal_api: &'a Option, public_api: &'a Option, + static_view: &'a Option, ) -> Vec> { vec![ Box::new(move || { @@ -63,6 +65,10 @@ fn registered_validators<'a>( component.as_ref(), )) }), + Box::new(move || { + let (component, static_view) = (component.as_ref()?, static_view.as_ref()?); + Some(validate_static_view_consistency(component, static_view)) + }), ] } @@ -88,8 +94,19 @@ pub fn run(inputs: &ArchitecturalDesignInputs) -> Result { &mut result, |raw: ClassDiagramInputs, _result| PublicApiIndex::build_index(&raw), )?; + let static_view = read_and_convert::( + inputs.static_view.as_slice(), + &mut result, + |raw: ComponentDiagramInputs, errs| raw.to_static_view_architecture(errs), + )?; - let validators = registered_validators(&component, &sequence, &internal_api, &public_api); + let validators = registered_validators( + &component, + &sequence, + &internal_api, + &public_api, + &static_view, + ); let mut ran_validator = false; for validator in validators { diff --git a/validation/core/src/validators/mod.rs b/validation/core/src/validators/mod.rs index ba23d7ad..22e51f5d 100644 --- a/validation/core/src/validators/mod.rs +++ b/validation/core/src/validators/mod.rs @@ -21,6 +21,7 @@ mod component_public_api_validator; mod component_sequence_validator; mod sequence_internal_api_validator; mod shared; +mod static_view_consistency_validator; #[cfg(test)] #[path = "test/fixtures.rs"] @@ -33,3 +34,4 @@ pub use component_internal_api_validator::validate_component_internal_api; pub use component_public_api_validator::validate_component_public_api; pub use component_sequence_validator::validate_component_sequence; pub use sequence_internal_api_validator::validate_sequence_internal_api; +pub use static_view_consistency_validator::validate_static_view_consistency; diff --git a/validation/core/src/validators/static_view_consistency_validator.rs b/validation/core/src/validators/static_view_consistency_validator.rs new file mode 100644 index 00000000..bf63d3bf --- /dev/null +++ b/validation/core/src/validators/static_view_consistency_validator.rs @@ -0,0 +1,167 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Validation: check that `static_view` component diagrams only reference +//! components/units that are also defined in the `static` component +//! diagrams. +//! +//! `static_view` diagrams are partial "views" onto the full static +//! architecture: any component/unit they define must already be defined in +//! `static`, and a `static_view` diagram may only include a subset of the +//! units/components of the matching `static` component. It is not permitted +//! to introduce components/units in `static_view` that do not exist in +//! `static`. +//! +//! A component/unit may appear in more than one `static_view` diagram (e.g. +//! overlapping views); such repetition across `static_view` diagrams is not +//! checked for duplicates, only consistency with `static` is checked. + +use std::collections::BTreeMap; + +use crate::models::{ComponentDiagramArchitecture, EntityKey, LogicComponent}; +use crate::results::{ErrorBuilder, ErrorCategory}; +use crate::{Diagnostics, ValidationResult}; + +/// Run static-vs-static_view component diagram consistency validation. +pub fn validate_static_view_consistency( + static_diagram: &ComponentDiagramArchitecture, + static_view_diagram: &ComponentDiagramArchitecture, +) -> ValidationResult { + StaticViewConsistencyValidator::new().run(static_diagram, static_view_diagram) +} + +/// Compares a `static` [`ComponentDiagramArchitecture`] against a +/// `static_view` [`ComponentDiagramArchitecture`], reporting any +/// component/unit defined in `static_view` that is not also defined in +/// `static`. A parentless static-view entry may reference a uniquely named +/// static entity from a different architectural scope. +struct StaticViewConsistencyValidator { + result: ValidationResult, +} + +impl StaticViewConsistencyValidator { + fn new() -> Self { + Self { + result: ValidationResult::default(), + } + } + + fn run( + mut self, + static_diagram: &ComponentDiagramArchitecture, + static_view_diagram: &ComponentDiagramArchitecture, + ) -> ValidationResult { + append_debug_log( + &mut self.result.diagnostics, + static_diagram, + static_view_diagram, + ); + self.check_only_known_entities( + &static_diagram.comp_set, + &static_view_diagram.comp_set, + "component", + ); + self.check_only_known_entities( + &static_diagram.unit_set, + &static_view_diagram.unit_set, + "unit", + ); + self.result + } + + /// Reports every entity present in `static_view_set` that is not present + /// in `static_set`. Parent-qualified view entries must match exactly; + /// parentless view entries may match a uniquely named static entity. + fn check_only_known_entities( + &mut self, + static_set: &BTreeMap, + static_view_set: &BTreeMap, + entity_type: &str, + ) { + for (key, entity) in static_view_set { + if !Self::is_known_static_entity(static_set, key) { + let (name, parent) = key; + let parent_str = parent.as_deref().unwrap_or("(top-level)"); + self.result + .add_failure(Self::format_extra(entity_type, name, parent_str, entity)); + } + } + } + + fn is_known_static_entity( + static_set: &BTreeMap, + static_view_key: &EntityKey, + ) -> bool { + if static_set.contains_key(static_view_key) { + return true; + } + + let (name, parent) = static_view_key; + parent.is_none() + && static_set + .keys() + .filter(|(static_name, _)| static_name == name) + .take(2) + .count() + == 1 + } + + fn format_extra( + entity_type: &str, + name: &str, + parent_str: &str, + entity: &LogicComponent, + ) -> String { + let (source_file, source_line) = entity.source_location.display(); + + ErrorBuilder::new(ErrorCategory::Design) + .title(format!( + "{entity_type} \"{name}\" in the static_view diagram is not defined in the static diagram" + )) + .field("alias", format!("\"{name}\"")) + .field("parent", parent_str) + .field("static_view source file", format!("\"{source_file}\"")) + .field("static_view source line", source_line.to_string()) + .fix(format!( + "add {entity_type} \"{name}\" under \"{parent_str}\" to the static diagram, or remove it from the static_view diagram" + )) + .build() + } +} + +fn append_debug_log( + diagnostics: &mut Diagnostics, + static_diagram: &ComponentDiagramArchitecture, + static_view_diagram: &ComponentDiagramArchitecture, +) { + diagnostics.debug(|| "static component set:".to_string()); + for key in static_diagram.comp_set.keys() { + diagnostics.debug(|| format!(" {:?}", key)); + } + diagnostics.debug(|| "static unit set:".to_string()); + for key in static_diagram.unit_set.keys() { + diagnostics.debug(|| format!(" {:?}", key)); + } + diagnostics.debug(|| "static_view component set:".to_string()); + for key in static_view_diagram.comp_set.keys() { + diagnostics.debug(|| format!(" {:?}", key)); + } + diagnostics.debug(|| "static_view unit set:".to_string()); + for key in static_view_diagram.unit_set.keys() { + diagnostics.debug(|| format!(" {:?}", key)); + } +} + +#[cfg(test)] +#[path = "test/static_view_consistency_validator_test.rs"] +mod tests; diff --git a/validation/core/src/validators/test/static_view_consistency_validator_test.rs b/validation/core/src/validators/test/static_view_consistency_validator_test.rs new file mode 100644 index 00000000..ef3d32c4 --- /dev/null +++ b/validation/core/src/validators/test/static_view_consistency_validator_test.rs @@ -0,0 +1,182 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use super::*; +use crate::models::{ComponentDiagramInputs, ComponentType, LogicComponent}; +use crate::validators::fixtures::dummy_source_location; + +fn entity( + id: &str, + alias: Option<&str>, + parent_id: Option<&str>, + stereotype: Option<&str>, +) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: alias.map(|s| s.to_string()), + alias: alias.map(|s| s.to_string()), + parent_id: parent_id.map(|s| s.to_string()), + element_type: ComponentType::Component, + stereotype: stereotype.map(|s| s.to_string()), + relations: Vec::new(), + source_location: dummy_source_location(), + } +} + +fn diagram(entities: Vec) -> ComponentDiagramInputs { + ComponentDiagramInputs { entities } +} + +fn run( + static_entities: Vec, + static_view_entities: Vec, +) -> ValidationResult { + let mut result = ValidationResult::default(); + let static_diagram = diagram(static_entities).to_diagram_architecture(&mut result); + let static_view_diagram = + diagram(static_view_entities).to_static_view_architecture(&mut result); + result.merge(validate_static_view_consistency( + &static_diagram, + &static_view_diagram, + )); + result +} + +#[test] +fn static_view_subset_of_static_passes() { + let static_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + entity("CompA.Unit2", Some("unit_2"), Some("CompA"), Some("unit")), + ]; + let static_view_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + ]; + + let result = run(static_entities, static_view_entities); + assert!( + result.is_empty(), + "Expected pass, got: {:?}", + result.failures + ); +} + +#[test] +fn parentless_static_view_unit_matches_unique_static_unit() { + let static_entities = vec![ + entity("MwCom", Some("mw_com"), None, Some("SEooC")), + entity( + "MwCom.BindingFactories", + Some("binding_factories"), + Some("MwCom"), + Some("unit"), + ), + ]; + let static_view_entities = vec![entity( + "BindingFactories", + Some("binding_factories"), + None, + Some("unit"), + )]; + + let result = run(static_entities, static_view_entities); + assert!( + result.is_empty(), + "Expected parentless static-view unit to match unique static unit, got: {:?}", + result.failures + ); +} + +#[test] +fn parentless_static_view_unit_with_ambiguous_static_name_fails() { + let static_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompB", Some("comp_b"), None, Some("component")), + entity( + "CompA.SharedUnit", + Some("shared_unit"), + Some("CompA"), + Some("unit"), + ), + entity( + "CompB.SharedUnit", + Some("shared_unit"), + Some("CompB"), + Some("unit"), + ), + ]; + let static_view_entities = vec![entity( + "SharedUnit", + Some("shared_unit"), + None, + Some("unit"), + )]; + + let result = run(static_entities, static_view_entities); + assert!(result.failures.iter().any(|message| message.contains( + "Unit \"shared_unit\" in the static_view diagram is not defined in the static diagram" + ))); +} + +#[test] +fn static_view_component_not_in_static_fails() { + let static_entities = vec![entity("CompA", Some("comp_a"), None, Some("component"))]; + let static_view_entities = vec![entity("CompB", Some("comp_b"), None, Some("component"))]; + + let result = run(static_entities, static_view_entities); + assert!(result.failures.iter().any(|message| message.contains( + "Component \"comp_b\" in the static_view diagram is not defined in the static diagram" + ))); +} + +#[test] +fn static_view_unit_not_in_static_fails() { + let static_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + ]; + let static_view_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + entity("CompA.Unit2", Some("unit_2"), Some("CompA"), Some("unit")), + ]; + + let result = run(static_entities, static_view_entities); + assert!(result.failures.iter().any(|message| message.contains( + "Unit \"unit_2\" in the static_view diagram is not defined in the static diagram" + ))); +} + +#[test] +fn static_view_entity_repeated_across_views_is_not_a_duplicate() { + // Simulates the same entity being declared in two separate static_view + // diagram files, which are merged before consistency checking. + let static_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + ]; + let static_view_entities = vec![ + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + entity("CompA", Some("comp_a"), None, Some("component")), + entity("CompA.Unit1", Some("unit_1"), Some("CompA"), Some("unit")), + ]; + + let result = run(static_entities, static_view_entities); + assert!( + result.is_empty(), + "Expected no duplicate-entity error across static_view diagrams, got: {:?}", + result.failures + ); +}