diff --git a/.github/skills/score-architecture/SKILL.md b/.github/skills/score-architecture/SKILL.md index 911cdd24..07dffbcd 100644 --- a/.github/skills/score-architecture/SKILL.md +++ b/.github/skills/score-architecture/SKILL.md @@ -330,7 +330,7 @@ One target bundles every diagram kind (from [`examples/seooc/design/BUILD`](../. ```starlark architectural_design( name = "sample_seooc_design", - static = ["static_design.puml", "index.md"], + static = ["static_design.puml", "overview_design.puml", "index.md"], dynamic = ["dynamic_design.puml"], public_api = ["public_api.puml", "public_api.rst"], internal_api = ["internal_api.puml"], @@ -339,6 +339,24 @@ architectural_design( ) ``` +`static` accepts more than one `.puml` file. They are merged by entity id (the full parent-alias +dot-path) into a single architecture: re-declaring the same entity (same id) in more than one +file is allowed and merges its relations, as long as `stereotype`/element type agree everywhere +it's declared — this is how `overview_design.puml` above can bare-declare the SEooC and its +top-level component while `static_design.puml` elaborates their internals, without duplicating +every nested unit and relation in both files. A parent whose children are split across files with +no single file containing all of them is an error. Re-nesting an entity under a different parent +across files is *not* caught here (different parent ⇒ different id ⇒ a different entity) — that +class of mistake instead surfaces as a Bazel ↔ diagram mismatch (extra/missing entity) in the +`bazel_component` check below. + +Each `.puml` file is parsed and resolved on its own, *before* the id-based merge above runs — a +relation may only reference an alias declared in that same file. Referencing an alias that's only +declared in another `static` file fails at PlantUML parse time (`Element Resolver: +UnresolvedReference: `), not as a Design validation error. So keep each file +self-contained: if a detail file wires up an entity's interfaces, (re-)declare those interfaces +in that same file rather than assuming they're visible from the overview file. + `static`/`dynamic` accept `.puml`, `.plantuml`, `.png`, `.svg`, `.rst`, `.md`. To combine a diagram with prose, add both the RST/Markdown wrapper *and* the referenced `.puml` to the same list (as `public_api.puml` + `public_api.rst` above, which overrides `public_api.puml`'s diff --git a/bazel/rules/rules_score/docs/index.rst b/bazel/rules/rules_score/docs/index.rst index e34565a3..cc365063 100644 --- a/bazel/rules/rules_score/docs/index.rst +++ b/bazel/rules/rules_score/docs/index.rst @@ -44,6 +44,7 @@ Rules SCORE for Bazel tool_reference/specs/bazel_component tool_reference/specs/class_design_implementation + tool_reference/specs/component_model tool_reference/specs/component_internal_api tool_reference/specs/component_public_api tool_reference/specs/component_sequence diff --git a/bazel/rules/rules_score/docs/overview.rst b/bazel/rules/rules_score/docs/overview.rst index 6c702222..27cc14b5 100644 --- a/bazel/rules/rules_score/docs/overview.rst +++ b/bazel/rules/rules_score/docs/overview.rst @@ -117,6 +117,10 @@ Bazel/C++ implementation: - **Bazel ↔ static design** — every ``component``/``unit`` target must appear in the static PlantUML diagram and vice versa (:doc:`spec `). +- **Static design merge** — ``static`` may list more than one PlantUML file + (e.g. a boundary overview plus detail diagrams); they are merged by entity + id before the checks below run + (:doc:`spec `). - **Static ↔ public/internal API** — interfaces referenced in the static design must be declared by the public/internal API class diagrams (:doc:`public API spec `, diff --git a/bazel/rules/rules_score/docs/rule_reference.rst b/bazel/rules/rules_score/docs/rule_reference.rst index ff28409a..d305ba0b 100644 --- a/bazel/rules/rules_score/docs/rule_reference.rst +++ b/bazel/rules/rules_score/docs/rule_reference.rst @@ -419,7 +419,7 @@ and ``fmea``. * - ``static`` - label list - no - - Static-view files (``.puml``, ``.rst``, ``.md``, ``.svg``, ``.png``) (default ``[]``) + - Static-view files (``.puml``, ``.rst``, ``.md``, ``.svg``, ``.png``) (default ``[]``). Multiple ``.puml`` files are allowed — e.g. a boundary overview diagram alongside a detailed one — and are merged into a single architecture, see `Multiple static PlantUML files`_ below. * - ``dynamic`` - label list - no @@ -443,6 +443,47 @@ and ``fmea``. **Generated targets:** ```` (provides ``ArchitecturalDesignInfo``; no standalone test — consistency is validated as part of ``bazel test //pkg:my_element``) +.. _multiple-static-puml-files: + +Multiple static PlantUML files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``static`` accepts more than one ``.puml`` file. This is intended for splitting +a large architecture into a boundary **overview** diagram (the SEooC and its +public interfaces only) and one or more **detail** diagrams that elaborate the +internals, without repeating every relation and nested entity in both places. + +The files are parsed independently and then merged by entity id (the full +dot-path of parent aliases). Rules for the merge: + +* Re-declaring the *same* entity (identical id) in more than one file is + allowed as long as its ``stereotype`` and element type agree across files; + their relations are unioned (duplicate relations are de-duplicated). This + is what lets an overview file bare-declare a component or unit that a + detail file elaborates further. +* Re-declaring the same alias with a conflicting ``stereotype`` or element + type across files is an error. +* A parent whose children are declared across more than one file is only + allowed if a single file contains the full set of children (i.e. one file + is the "home" file and the others only add a benign subset); if the + children are genuinely split with no file containing all of them, this is + reported as an error. +* Re-nesting an entity under a *different* parent in another file is **not** + detected as a same-entity conflict, because the id encodes the parent + chain — a different parent means a different id, hence a different + entity. Such "wrong nesting" mistakes are instead caught by the existing + Bazel-vs-diagram comparison (an entity nested under the wrong parent shows + up as an extra/missing entry there). + +Each ``.puml`` file is parsed and resolved **independently**, before the +merge step above ever runs. A relation (``-->``, ``..>``, etc.) may only +reference aliases declared in the *same* file — an alias declared only in +another ``static`` file is not visible yet at that point. Referencing such +an alias fails at PlantUML parse time with an ``Element Resolver: +UnresolvedReference: `` error, not as a Design validation error. Keep +every file self-contained: if a detail file wires up an entity's +interfaces, declare (or re-declare) those interfaces in that same file. + .. _rule-unit-design: unit_design diff --git a/bazel/rules/rules_score/docs/user_guide/architectural_design.rst b/bazel/rules/rules_score/docs/user_guide/architectural_design.rst index 3729dc3c..0e0756c8 100644 --- a/bazel/rules/rules_score/docs/user_guide/architectural_design.rst +++ b/bazel/rules/rules_score/docs/user_guide/architectural_design.rst @@ -314,6 +314,70 @@ When an element needs an explicitly named, standalone binding point — for exam @enduml +.. _overview-and-detail-diagrams: + +Splitting Into an Overview and Detail Diagrams +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For a large architecture, pass more than one file to ``static``: one shared +**overview** diagram that bare-declares the SEooC boundary and its top-level +components, plus one or more **detail** diagrams that elaborate a component's +internals. Each file is parsed independently and then merged by entity id, so +the same component can appear in both without repeating its full contents: + +.. code-block:: text + + ' overview_design.puml + @startuml overview_design + + package "Safety Software SEooC Example" as safety_software_seooc_example <> { + component "ComponentExample" as component_example <> + } + + interface "SampleLibraryAPI" as SampleLibraryAPI + safety_software_seooc_example )-d- SampleLibraryAPI + + @enduml + +.. code-block:: text + + ' static_design.puml + @startuml static_design + + package "Safety Software SEooC Example" as safety_software_seooc_example <> { + component "ComponentExample" as component_example <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + } + + @enduml + +.. code-block:: starlark + + architectural_design( + name = "my_arch", + static = ["overview_design.puml", "static_design.puml"], + ) + +See ``examples/seooc/design`` for the full working pair. + +**Limitations:** + +- Exactly one file must be the "home" for a given parent's full set of + children — one diagram declares *all* of a parent's children; the others + may only bare-declare that parent (no children) or repeat that same full + set. Two files each declaring a different, incomplete subset of the same + parent's children is rejected as a merge error — in practice, use one + shared overview file plus one or more detail files that each own a + disjoint part of the hierarchy, not several files partially detailing the + same component. +- A relation may only reference aliases declared in the *same* file — an + overview cannot wire up a relation to an alias that only a detail file + declares. Keep each file self-contained. + +See :ref:`multiple-static-puml-files` for the complete merge rules. + Bazel ~~~~~~ @@ -330,6 +394,10 @@ architectural_design dynamic = ["sequence_design.puml"], ) +``static`` accepts more than one ``.puml`` file — see +:ref:`overview-and-detail-diagrams` above for splitting a large architecture +into an overview plus detail diagrams, and their limitations. + unit ^^^^^ diff --git a/bazel/rules/rules_score/examples/seooc/design/BUILD b/bazel/rules/rules_score/examples/seooc/design/BUILD index de5076a6..74f88bd2 100644 --- a/bazel/rules/rules_score/examples/seooc/design/BUILD +++ b/bazel/rules/rules_score/examples/seooc/design/BUILD @@ -30,6 +30,7 @@ architectural_design( ], static = [ "static_design.puml", + "overview_design.puml", "index.md", ], visibility = ["//visibility:public"], diff --git a/bazel/rules/rules_score/examples/seooc/design/overview_design.puml b/bazel/rules/rules_score/examples/seooc/design/overview_design.puml new file mode 100644 index 00000000..bf79fda8 --- /dev/null +++ b/bazel/rules/rules_score/examples/seooc/design/overview_design.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' 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 overview_design + +' Boundary overview: the SEooC and its public API only. Internals are +' elaborated in static_design.puml; every entity here is also declared there. + +package "Safety Software SEooC Example" as safety_software_seooc_example <> { + component "ComponentExample" as component_example <> +} + +interface "SampleLibraryAPI" as SampleLibraryAPI + +safety_software_seooc_example )-d- SampleLibraryAPI + +@enduml diff --git a/plantuml/sphinx/clickable_plantuml/README.md b/plantuml/sphinx/clickable_plantuml/README.md index 51f15dc8..a564691c 100644 --- a/plantuml/sphinx/clickable_plantuml/README.md +++ b/plantuml/sphinx/clickable_plantuml/README.md @@ -99,7 +99,8 @@ Sphinx build lifecycle clickable_plantuml hooks │ (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. + │ Apply proximity, then descendant-count + │ tiebreak on ambiguity. │ Build the URL (relative to _images/ in │ svg_obj mode, else page-relative via │ get_relative_uri), then append @@ -133,9 +134,16 @@ Sphinx build lifecycle clickable_plantuml hooks 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. + diagram (this is the case for a reference resolved across multiple + `static` files merged into one architecture, see the `component_model` + validator spec). If proximity still ties (e.g. candidates in the same + directory), a *descendant-count tiebreak* prefers the candidate whose + idmap elaborates more nested entries under the referenced id — this can + change which file a link points to compared to earlier releases, where a + proximity tie always meant no link. On a genuine tie after both stages, 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 diff --git a/plantuml/sphinx/clickable_plantuml/clickable_plantuml.py b/plantuml/sphinx/clickable_plantuml/clickable_plantuml.py index 155595ec..c15e96b0 100644 --- a/plantuml/sphinx/clickable_plantuml/clickable_plantuml.py +++ b/plantuml/sphinx/clickable_plantuml/clickable_plantuml.py @@ -33,8 +33,12 @@ 3. If exactly one definer: emit the link. 4. If multiple definers: pick the one sharing the longest common workspace- relative path prefix with the source diagram (proximity tiebreak). - On a tie: log a warning and emit no link (safe over wrong). -5. Never link a diagram to itself. +5. If proximity still ties (e.g. same directory): pick the one with the most + idmap entries nested under the referenced id (descendant-count tiebreak) — + the file that elaborates more of that id's decomposition is assumed to be + the more useful navigation target. +6. If still tied: log a warning and emit no link (safe over wrong). +7. Never link a diagram to itself. """ from __future__ import annotations @@ -44,6 +48,7 @@ import os import re import urllib.parse +from collections.abc import Callable from pathlib import Path, PurePosixPath from typing import Any @@ -122,32 +127,47 @@ def _common_prefix_length(path_a: str, path_b: str) -> int: return count -def _proximity_tiebreak(source: str, candidates: list[str]) -> str | None: - """Pick the candidate with the longest common prefix with *source*. +def _tied_top(candidates: list[str], score: Callable[[str], int]) -> tuple[int | None, list[str]]: + """Return the max score and every candidate achieving it (``(None, [])`` if empty). + + Shared by both tiebreak stages in :func:`_resolve_definer`. + """ + best_score: int | None = None + best: list[str] = [] + for candidate in candidates: + candidate_score = score(candidate) + if best_score is None or candidate_score > best_score: + best_score = candidate_score + best = [candidate] + elif candidate_score == best_score: + best.append(candidate) + return best_score, best + + +def _proximity_score(source: str, candidate: str) -> int: + """Common-prefix-length score used by the proximity tiebreak stage. All inputs are canonical workspace-relative POSIX keys (guaranteed by the exact-matching in P0-1); the assertions guard that invariant so a staging - path can never sneak into the comparison. Returns ``None`` when two or - more candidates score equally (tie → no link). + path can never sneak into the comparison. """ _assert_canonical_source_key(source) - best_candidate: str | None = None - best_score = -1 - has_tie = False + _assert_canonical_source_key(candidate) + return _common_prefix_length(source, candidate) - for candidate in candidates: - _assert_canonical_source_key(candidate) - score = _common_prefix_length(source, candidate) - if score > best_score: - best_score = score - best_candidate = candidate - has_tie = False - elif score == best_score: - has_tie = True - - if has_tie or best_candidate is None: - return None - return best_candidate + +def _descendant_count(idmap_by_source: dict[str, Any], source_key: str, fqn: str) -> int: + """Count *source_key*'s idmap entries nested under *fqn* (id starts with ``f"{fqn}."``). + + Second tiebreak stage in :func:`_resolve_definer`: prefers the file that + elaborates more of that id's decomposition. + """ + idmap = idmap_by_source.get(source_key) + if idmap is None: + return 0 + prefix = f"{fqn}." + entries = idmap.get("defines", []) + idmap.get("references", []) + return sum(1 for entry in entries if entry.get("id", "").startswith(prefix)) def _resolve_definer( @@ -155,6 +175,7 @@ def _resolve_definer( fqn: str, source_key: str, definition_index: dict[str, list[str]], + idmap_by_source: dict[str, Any] | None = None, ) -> str | None: """Return the definer source key for one reference, or ``None``. @@ -167,9 +188,9 @@ def _resolve_definer( while a distinct diagram elaborates it under a shared alias). * A diagram never links to itself (self-links are dropped from both lookups). - * A single remaining candidate wins outright; multiple candidates go - through the proximity tiebreak, and a genuine tie logs a warning and - returns ``None`` (safe over wrong). + * A single remaining candidate wins outright. Multiple candidates go + through the proximity tiebreak, then the descendant-count tiebreak; + a genuine tie after both logs a warning and returns ``None``. """ _assert_canonical_source_key(source_key) @@ -184,7 +205,17 @@ def _candidates_for(key: str) -> list[str]: return None if len(candidates) == 1: return candidates[0] - target = _proximity_tiebreak(source_key, candidates) + + proximity_group = _tied_top(candidates, lambda c: _proximity_score(source_key, c))[1] + target: str | None = proximity_group[0] if len(proximity_group) == 1 else None + + if target is None and idmap_by_source: + descendant_score, descendant_group = _tied_top( + proximity_group, lambda c: _descendant_count(idmap_by_source, c, fqn) + ) + if len(descendant_group) == 1 and descendant_score > 0: + target = descendant_group[0] + if target is None: logger.warning( "clickable_plantuml: ambiguous definition for '%s' in '%s' — tied candidates %s; no link emitted", @@ -617,7 +648,7 @@ def on_doctree_resolved(app: Sphinx, doctree: nodes.document, docname: str) -> N if not alias or alias in seen_aliases_in_node: continue - target_source = _resolve_definer(alias, fqn, source_key, definition_index) + target_source = _resolve_definer(alias, fqn, source_key, definition_index, idmap_by_source) if target_source is None: continue diff --git a/plantuml/sphinx/clickable_plantuml/tests/test_clickable_plantuml.py b/plantuml/sphinx/clickable_plantuml/tests/test_clickable_plantuml.py index 3cee52e0..7c948152 100644 --- a/plantuml/sphinx/clickable_plantuml/tests/test_clickable_plantuml.py +++ b/plantuml/sphinx/clickable_plantuml/tests/test_clickable_plantuml.py @@ -456,6 +456,61 @@ def test_resolve_definer_tie_returns_none_and_warns( assert "ambiguous definition" in caplog.text +def test_resolve_definer_same_directory_tie_broken_by_descendant_count() -> None: + # Same-directory candidates tie on proximity; descendant count breaks the tie. + definition_index = { + "pkg.component_example": ["design/static_design.puml", "design/internal_api.puml"], + } + idmap_by_source = { + "design/static_design.puml": { + "defines": [ + {"alias": "component_example", "id": "pkg.component_example"}, + ], + "references": [ + {"alias": "unit_1", "id": "pkg.component_example.unit_1"}, + {"alias": "unit_2", "id": "pkg.component_example.unit_2"}, + {"alias": "sub_component_example", "id": "pkg.component_example.sub_component_example"}, + {"alias": "InternalInterface", "id": "pkg.component_example.InternalInterface"}, + ], + }, + "design/internal_api.puml": { + "defines": [ + {"alias": "component_example", "id": "pkg.component_example"}, + {"alias": "InternalInterface", "id": "pkg.component_example.InternalInterface"}, + ], + "references": [], + }, + } + + target = _resolve_definer( + "component_example", + "pkg.component_example", + "design/overview_design.puml", + definition_index, + idmap_by_source, + ) + + assert target == "design/static_design.puml" + + +def test_resolve_definer_same_directory_tie_with_equal_descendant_counts_still_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + definition_index = { + "pkg.Proxy": ["design/one.puml", "design/two.puml"], + } + idmap_by_source = { + "design/one.puml": {"defines": [{"alias": "Proxy", "id": "pkg.Proxy"}], "references": []}, + "design/two.puml": {"defines": [{"alias": "Proxy", "id": "pkg.Proxy"}], "references": []}, + } + + caplog.set_level(logging.WARNING) + target = _resolve_definer("Proxy", "pkg.Proxy", "design/overview.puml", definition_index, idmap_by_source) + + assert target is None + assert "ambiguous definition" in caplog.text + + def test_common_prefix_length_requires_canonical_keys() -> None: with pytest.raises(ValueError, match="non-canonical source key"): _common_prefix_length("/abs/a.puml", "pkg/b.puml") diff --git a/validation/core/BUILD b/validation/core/BUILD index ef392594..160b0aba 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -26,6 +26,7 @@ filegroup( "docs/specifications/class_design_implementation.md", "docs/specifications/class_design_sequence.md", "docs/specifications/component_internal_api.md", + "docs/specifications/component_model.md", "docs/specifications/component_public_api.md", "docs/specifications/component_sequence.md", "docs/specifications/sequence_internal_api.md", @@ -40,6 +41,7 @@ rust_library( "src/models/bazel_models.rs", "src/models/class_diagram_models.rs", "src/models/component_diagram_models.rs", + "src/models/component_diagram_models/component_diagram_merge.rs", "src/models/mod.rs", "src/models/sequence_diagram_models.rs", "src/models/shared.rs", diff --git a/validation/core/docs/requirements/tool_requirements.trlc b/validation/core/docs/requirements/tool_requirements.trlc index 80b93a37..43fa3c04 100644 --- a/validation/core/docs/requirements/tool_requirements.trlc +++ b/validation/core/docs/requirements/tool_requirements.trlc @@ -84,6 +84,29 @@ section "Tool Requirements" { } + section "Component Model Validator" { + + ToolQualification.ToolRequirement ComponentModelCrossFileDeclarationConsistency { + description = '''When an entity with the same id is declared + in more than one `static` PlantUML file, the validator shall + report an error if the declarations disagree on stereotype or + element type, and shall otherwise merge their relations into + a single entity.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + ToolQualification.ToolRequirement ComponentModelSingleHomeDecomposition { + description = '''The validator shall report an error when an + entity's children are declared across more than one `static` + PlantUML file and no single file declares the full set of + children declared for that entity anywhere.''' + derived_from = [UseCases.Validate_Architecture_Specification_Documents] + satisfied_by = Verifier + } + + } + section "Component Sequence Validator" { ToolQualification.ToolRequirement ComponentSequenceAliasConsistency { diff --git a/validation/core/docs/specifications/bazel_component.md b/validation/core/docs/specifications/bazel_component.md index 827fca4f..2ea8a2df 100644 --- a/validation/core/docs/specifications/bazel_component.md +++ b/validation/core/docs/specifications/bazel_component.md @@ -22,6 +22,13 @@ It shall make sure that the same architectural elements exist on both sides and ## What is Validated +`architectural_design.static` may list more than one PlantUML file (e.g. a +boundary overview diagram plus one or more detail diagrams); these are merged +into a single component diagram model before this validator runs, so the +checks below operate on that merged view regardless of how many static files +contributed to it. See the `architectural_design` rule reference for the +cross-file merge rules. + All comparisons are case-insensitive: both Bazel target short names and PlantUML aliases/IDs are normalized to lowercase before matching, so a Bazel target `Component_X` matches a PlantUML entity `as COMPONENT_X`. Names are diff --git a/validation/core/docs/specifications/component_model.md b/validation/core/docs/specifications/component_model.md new file mode 100644 index 00000000..f7ad37c5 --- /dev/null +++ b/validation/core/docs/specifications/component_model.md @@ -0,0 +1,101 @@ + + +# Component Model Specification + +## Purpose + +`architectural_design.static` may list more than one PlantUML file (e.g. a +boundary overview diagram plus one or more detail diagrams). This validator +builds the merged `ComponentDiagramArchitecture` that every other +component-diagram validator (`bazel_component`, `component_internal_api`, +`component_public_api`, `component_sequence`, `sequence_internal_api`) +operates on, and enforces that the merge itself is unambiguous. + +## What is Validated + +Entities are matched across files by id — the full dot-path of parent +aliases — not by bare alias, so two entities that share an alias under +different parents are never conflated. + +### Cross-File Declaration Consistency + +Re-declaring the same id in more than one `static` file is allowed as long as +every declaration agrees on stereotype and element type; their relations are +merged (duplicate relations, compared structurally rather than by source +location, are not repeated). This is what lets an overview file bare-declare +an entity that a detail file elaborates further. + +*(Requirement: {requirement:downstream-ref}`Tools.ComponentModelCrossFileDeclarationConsistency`)* + +```text +[Design] Unit "shared_thing" is re-declared with a conflicting stereotype in another component diagram file. +``` + +```text +[Design] Component "shared_thing" is re-declared with a conflicting element type in another component diagram file. +``` + +Declarations of the same entity are compared by name/alias/id only through +the id itself — since two declarations sharing the exact same id are +guaranteed to share the same alias and immediate parent, disagreement is only +possible on stereotype or element type. Re-nesting an entity under a +*different* parent in another file is not detected here: a different parent +means a different id, hence a different entity. That mistake instead surfaces +as an extra/missing entity in the `bazel_component` check. + +### Single-Home Decomposition + +A parent whose children (nested components/units) are declared across more +than one file is only allowed if a single file contains the full set of +children declared for that parent anywhere — i.e. one file is the "home" file +and the others only re-declare a benign subset (or none). If the children are +genuinely split, with no file containing all of them, this is an error. + +*(Requirement: {requirement:downstream-ref}`Tools.ComponentModelSingleHomeDecomposition`)* + +```text +[Design] Entity "component_a" has children declared across more than one component diagram file, with no single file containing all of them. +``` + +Interfaces are exempt from this check: they aren't compared against the +Bazel build graph, so an overview file may declare a subset of a parent's +interfaces (e.g. its public ones) while a detail file declares others (e.g. +its internal ones), without that counting as a split decomposition. + +### Determinism + +Errors are ordered by source location (file, then line), not by which +declaration happened to be visited first while merging — so the reported +error is identical regardless of the order files are listed in `static`. + +### Not Validated Here + +Each `.puml` file is parsed and resolved independently, before the id-based +merge above ever runs. A relation may only reference an alias declared in +that same file: referencing an alias that's only declared in another +`static` file fails at PlantUML parse time (`Element Resolver: +UnresolvedReference: `), not as a Design validation error from this +validator. + +## Failure Cases + +| Failure case | Validation rule | +|---|---| +| Same id declared in two files with conflicting stereotype or element type | Cross-File Declaration Consistency | +| A parent's children declared across files with no single file containing all of them | Single-Home Decomposition | + +## Debug Output + +The validator emits debug output containing the total number of SEooC +packages, components, and units after the cross-file merge. diff --git a/validation/core/integration_test/README.md b/validation/core/integration_test/README.md index 156aedfd..4a5d7666 100644 --- a/validation/core/integration_test/README.md +++ b/validation/core/integration_test/README.md @@ -24,11 +24,13 @@ integration_test/ ├── BUILD # shared Rust test framework library ├── puml_fixture.bzl # Starlark rule: provider → category dirs ├── bazel_component/ # BazelComponent suite, cases, and test binary -├── component_class/ # ComponentClass suite, cases, and test binary +├── component_model/ # ComponentModel suite, cases, and test binary ├── component_sequence/ # ComponentSequence suite, cases, and test binary ├── component_internal_api/ # ComponentInternalApi cases +├── component_public_api/ # ComponentPublicApi cases ├── sequence_internal_api/ # SequenceInternalApi cases ├── class_design_implementation/ # ClassDesignImplementation suite, cases, and test binary +├── class_design_sequence/ # ClassDesignSequence suite, cases, and test binary ├── src/ # Rust crate sources for shared test_framework │ ├── lib.rs # re-exports from test_framework │ └── test_framework.rs # shared helpers (CLI runner, assertions) @@ -229,11 +231,14 @@ bazel test //validation/core/integration_test/... Run a single suite: ```bash -bazel test //validation/core/integration_test/bazel_component:integration_test -bazel test //validation/core/integration_test/component_sequence:integration_test +bazel test //validation/core/integration_test/bazel_component:bazel_component_integration_test +bazel test //validation/core/integration_test/component_model:component_model_integration_test +bazel test //validation/core/integration_test/component_sequence:component_sequence_integration_test bazel test //validation/core/integration_test/component_internal_api:component_internal_api_integration_test +bazel test //validation/core/integration_test/component_public_api:component_public_api_integration_test bazel test //validation/core/integration_test/sequence_internal_api:sequence_internal_api_integration_test -bazel test //validation/core/integration_test/class_design_implementation:integration_test +bazel test //validation/core/integration_test/class_design_implementation:class_design_implementation_integration_test +bazel test //validation/core/integration_test/class_design_sequence:class_design_sequence_integration_test ``` ## Adding a new test case @@ -252,10 +257,10 @@ bazel test //validation/core/integration_test/class_design_implementation:integr 4. Create a `BUILD` file following the pattern of an existing case in the same suite. -5. Add the new `case_data` target to the matching filegroup in - [`BUILD`](BUILD) (`bazel_component_test_data`, - `component_sequence_test_data`, `component_internal_api_test_data`, `sequence_internal_api_test_data`, - or `class_design_implementation_test_data`). +5. Add the new `case_data` target to the matching filegroup in the suite's + own `BUILD` file (e.g. `component_model_test_data` in + [`component_model/BUILD`](component_model/BUILD)) — each suite keeps its + `_test_data` filegroup alongside its `rust_test` target. 6. Add a `#[test]` function in the matching suite file, such as `bazel_component_suite.rs` or `class_design_implementation_suite.rs`. diff --git a/validation/core/integration_test/bazel_component/BUILD b/validation/core/integration_test/bazel_component/BUILD index 1c0f02d6..b763206a 100644 --- a/validation/core/integration_test/bazel_component/BUILD +++ b/validation/core/integration_test/bazel_component/BUILD @@ -25,9 +25,12 @@ filegroup( "//validation/core/integration_test/bazel_component/negative_extra_unit:case_data", "//validation/core/integration_test/bazel_component/negative_missing_component:case_data", "//validation/core/integration_test/bazel_component/negative_missing_unit:case_data", + "//validation/core/integration_test/bazel_component/negative_multi_file_extra_component:case_data", + "//validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting:case_data", "//validation/core/integration_test/bazel_component/negative_wrong_stereotype:case_data", "//validation/core/integration_test/bazel_component/positive_case_insensitive:case_data", "//validation/core/integration_test/bazel_component/positive_component:case_data", + "//validation/core/integration_test/bazel_component/positive_multi_file_component:case_data", ], ) diff --git a/validation/core/integration_test/bazel_component/bazel_component_suite.rs b/validation/core/integration_test/bazel_component/bazel_component_suite.rs index 91c69987..5507c13c 100644 --- a/validation/core/integration_test/bazel_component/bazel_component_suite.rs +++ b/validation/core/integration_test/bazel_component/bazel_component_suite.rs @@ -55,6 +55,21 @@ fn positive_component_suite_case() { assert_case("positive_component"); } +#[test] +fn positive_multi_file_component_suite_case() { + assert_case("positive_multi_file_component"); +} + +#[test] +fn negative_multi_file_extra_component_suite_case() { + assert_case("negative_multi_file_extra_component"); +} + +#[test] +fn negative_multi_file_wrong_nesting_suite_case() { + assert_case("negative_multi_file_wrong_nesting"); +} + #[test] fn positive_case_insensitive_suite_case() { assert_case("positive_case_insensitive"); diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/BUILD b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/BUILD new file mode 100644 index 00000000..a509b043 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + static = [ + "component_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/architecture.json b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/architecture.json new file mode 100644 index 00000000..5e8c61fc --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/architecture.json @@ -0,0 +1,17 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + "@//bazel/rules/rules_score/examples/seooc:component_example" + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2" + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/component_diagram.puml b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/component_diagram.puml new file mode 100644 index 00000000..e8d3ca7b --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/component_diagram.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 component_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + + component "Extra Component" as extra_component <> +} + +@enduml diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/expected.yaml b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/expected.yaml new file mode 100644 index 00000000..5ef52c78 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/expected.yaml @@ -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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Naming] Component "extra_component" from the PlantUML component diagram not found in Bazel. + Alias : "extra_component" + Parent : safety_software_seooc_example + Component source file : "validation/core/integration_test/bazel_component/negative_multi_file_extra_component/component_diagram.puml" + Component source line : 22 + Fix : Add the corresponding Bazel component definition for "extra_component", or remove it from the PlantUML component diagram. diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/overview_diagram.puml b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/overview_diagram.puml new file mode 100644 index 00000000..c55da185 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_extra_component/overview_diagram.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Boundary overview: benignly re-declares the SEooC and its top-level +' component_example, without touching extra_component. Proves that merging +' this file in doesn't suppress (or duplicate) the pre-existing +' extra-component-vs-Bazel detection for extra_component, which is declared +' solely in component_diagram.puml. + +@startuml overview_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> +} + +@enduml diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/BUILD b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/BUILD new file mode 100644 index 00000000..12565374 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/architecture.json b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/architecture.json new file mode 100644 index 00000000..5e8c61fc --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/architecture.json @@ -0,0 +1,17 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + "@//bazel/rules/rules_score/examples/seooc:component_example" + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2" + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/detail_diagram.puml b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/detail_diagram.puml new file mode 100644 index 00000000..3f3624f0 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/detail_diagram.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 detail_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/expected.yaml b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/expected.yaml new file mode 100644 index 00000000..93a80896 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/expected.yaml @@ -0,0 +1,26 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: + # Asserts exactly one error fires: the wrongly-nested "unit_1" (a direct + # child of the package) not found in Bazel. Guards against a spurious + # split-decomposition error also firing for the correctly-nested "unit_1" + # under "component_example" that detail_diagram.puml declares. + - "FAILED (1 error(s)):" + - | + [Naming] Unit "unit_1" from the PlantUML component diagram not found in Bazel. + Alias : "unit_1" + Parent : safety_software_seooc_example + Component source file : "validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/overview_diagram.puml" + Component source line : 25 + Fix : Add the corresponding Bazel unit definition for "unit_1", or remove it from the PlantUML component diagram. diff --git a/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/overview_diagram.puml b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/overview_diagram.puml new file mode 100644 index 00000000..07b35d09 --- /dev/null +++ b/validation/core/integration_test/bazel_component/negative_multi_file_wrong_nesting/overview_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Deliberately wrong: "unit_1" is re-declared here as a direct child of the +' SEooC package instead of nested under component_example, where +' detail_diagram.puml (and the Bazel graph in architecture.json) puts it. +' Since the id encodes the full parent chain, this produces a second, +' differently-parented "unit_1" entity rather than merging with the correct +' one -- which Bazel comparison then reports as not found. + +@startuml overview_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> + component "Unit 1" as unit_1 <> +} + +@enduml diff --git a/validation/core/integration_test/bazel_component/positive_multi_file_component/BUILD b/validation/core/integration_test/bazel_component/positive_multi_file_component/BUILD new file mode 100644 index 00000000..a509b043 --- /dev/null +++ b/validation/core/integration_test/bazel_component/positive_multi_file_component/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + static = [ + "component_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "architecture.json", + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/bazel_component/positive_multi_file_component/architecture.json b/validation/core/integration_test/bazel_component/positive_multi_file_component/architecture.json new file mode 100644 index 00000000..5e8c61fc --- /dev/null +++ b/validation/core/integration_test/bazel_component/positive_multi_file_component/architecture.json @@ -0,0 +1,17 @@ +{ + "components": { + "safety_software_seooc_example": { + "units": [], + "components": [ + "@//bazel/rules/rules_score/examples/seooc:component_example" + ] + }, + "@//bazel/rules/rules_score/examples/seooc:component_example": { + "units": [ + "@//bazel/rules/rules_score/examples/seooc/unit_1:unit_1", + "@//bazel/rules/rules_score/examples/seooc/unit_2:unit_2" + ], + "components": [] + } + } +} diff --git a/validation/core/integration_test/bazel_component/positive_multi_file_component/component_diagram.puml b/validation/core/integration_test/bazel_component/positive_multi_file_component/component_diagram.puml new file mode 100644 index 00000000..cb51efe9 --- /dev/null +++ b/validation/core/integration_test/bazel_component/positive_multi_file_component/component_diagram.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 component_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/bazel_component/positive_multi_file_component/expected.yaml b/validation/core/integration_test/bazel_component/positive_multi_file_component/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/bazel_component/positive_multi_file_component/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/bazel_component/positive_multi_file_component/overview_diagram.puml b/validation/core/integration_test/bazel_component/positive_multi_file_component/overview_diagram.puml new file mode 100644 index 00000000..eb2539a9 --- /dev/null +++ b/validation/core/integration_test/bazel_component/positive_multi_file_component/overview_diagram.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 +' ******************************************************************************* + +' Boundary overview: re-declares the SEooC and its top-level component, +' without their nested detail, which lives solely in component_diagram.puml. + +@startuml overview_diagram + +package "Sample Seooc" as safety_software_seooc_example <> { + component "Component Example" as component_example <> +} + +@enduml diff --git a/validation/core/integration_test/component_internal_api/BUILD b/validation/core/integration_test/component_internal_api/BUILD index 8d1d86e1..7b4e9588 100644 --- a/validation/core/integration_test/component_internal_api/BUILD +++ b/validation/core/integration_test/component_internal_api/BUILD @@ -16,10 +16,6 @@ load("@rules_rust//rust:defs.bzl", "rust_test") filegroup( name = "component_internal_api_test_data", srcs = [ - "//validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded:case_data", - "//validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded:case_data", - "//validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded:case_data", - "//validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded:case_data", "//validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api:case_data", "//validation/core/integration_test/component_internal_api/negative_interface_missing_from_internal_api_with_suggestion:case_data", "//validation/core/integration_test/component_internal_api/negative_interfaces_missing_from_internal_api_with_suggestions:case_data", diff --git a/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs b/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs index 73423330..bc3db758 100644 --- a/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs +++ b/validation/core/integration_test/component_internal_api/component_internal_api_suite.rs @@ -66,26 +66,6 @@ fn negative_interfaces_missing_from_internal_api_with_suggestions_suite_case() { assert_case("negative_interfaces_missing_from_internal_api_with_suggestions"); } -#[test] -fn negative_duplicate_unit_alias_casefolded_suite_case() { - assert_case("negative_duplicate_unit_alias_casefolded"); -} - -#[test] -fn negative_duplicate_component_alias_casefolded_suite_case() { - assert_case("negative_duplicate_component_alias_casefolded"); -} - -#[test] -fn negative_duplicate_interface_alias_casefolded_suite_case() { - assert_case("negative_duplicate_interface_alias_casefolded"); -} - -#[test] -fn negative_duplicate_dependable_element_alias_casefolded_suite_case() { - assert_case("negative_duplicate_dependable_element_alias_casefolded"); -} - #[test] fn positive_interface_match_suite_case() { assert_case("positive_interface_match"); diff --git a/validation/core/integration_test/component_model/BUILD b/validation/core/integration_test/component_model/BUILD new file mode 100644 index 00000000..d6700cc5 --- /dev/null +++ b/validation/core/integration_test/component_model/BUILD @@ -0,0 +1,46 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("@rules_rust//rust:defs.bzl", "rust_test") + +filegroup( + name = "component_model_test_data", + srcs = [ + "//validation/core/integration_test/component_model/negative_children_split_across_files:case_data", + "//validation/core/integration_test/component_model/negative_conflicting_element_type:case_data", + "//validation/core/integration_test/component_model/negative_conflicting_stereotype:case_data", + "//validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded:case_data", + "//validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded:case_data", + "//validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded:case_data", + "//validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded:case_data", + "//validation/core/integration_test/component_model/positive_overview_nested_public_interface:case_data", + "//validation/core/integration_test/component_model/positive_overview_partial_subset:case_data", + "//validation/core/integration_test/component_model/positive_overview_subset:case_data", + "//validation/core/integration_test/component_model/positive_three_file_merge:case_data", + ], +) + +rust_test( + name = "component_model_integration_test", + srcs = ["component_model_suite.rs"], + crate_root = "component_model_suite.rs", + data = [ + ":component_model_test_data", + ], + deps = [ + "//validation/core:validation_cli", + "//validation/core/integration_test:test_framework", + "@crates//:serde", + "@crates//:serde_json", + ], +) diff --git a/validation/core/integration_test/component_model/component_model_suite.rs b/validation/core/integration_test/component_model/component_model_suite.rs new file mode 100644 index 00000000..2fad033a --- /dev/null +++ b/validation/core/integration_test/component_model/component_model_suite.rs @@ -0,0 +1,100 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Suite covering `ComponentDiagramArchitecture`'s entity model: duplicate/ +//! casefold-shadow detection and the multi-file `static` merge. + +use test_framework::{ + assert_cli_result, collect_case_fbs_files, load_expected_yaml_fixture, run_validation_profile, + CliRunResult, +}; + +const SUITE_DIR: &str = "component_model"; + +fn run_case_from_cli(case_dir: &str, component_fbs_paths: &[String]) -> CliRunResult { + run_validation_profile( + &format!("component_model_{case_dir}"), + "architectural-design", + serde_json::json!({ + "component_diagrams": component_fbs_paths, + }), + ) +} + +fn assert_case(case_dir: &str) { + let expected = load_expected_yaml_fixture(SUITE_DIR, case_dir); + let component_fbs_paths = collect_case_fbs_files(SUITE_DIR, case_dir, "component"); + + let result = if !component_fbs_paths.is_empty() { + run_case_from_cli(case_dir, &component_fbs_paths) + } else { + panic!("missing generated FBS fixtures for {case_dir}: expected component/*.fbs.bin"); + }; + + assert_cli_result(case_dir, &expected, &result); +} + +#[test] +fn negative_duplicate_unit_alias_casefolded_suite_case() { + assert_case("negative_duplicate_unit_alias_casefolded"); +} + +#[test] +fn negative_duplicate_component_alias_casefolded_suite_case() { + assert_case("negative_duplicate_component_alias_casefolded"); +} + +#[test] +fn negative_duplicate_interface_alias_casefolded_suite_case() { + assert_case("negative_duplicate_interface_alias_casefolded"); +} + +#[test] +fn negative_duplicate_dependable_element_alias_casefolded_suite_case() { + assert_case("negative_duplicate_dependable_element_alias_casefolded"); +} + +#[test] +fn positive_overview_subset_suite_case() { + assert_case("positive_overview_subset"); +} + +#[test] +fn negative_conflicting_stereotype_suite_case() { + assert_case("negative_conflicting_stereotype"); +} + +#[test] +fn negative_children_split_across_files_suite_case() { + assert_case("negative_children_split_across_files"); +} + +#[test] +fn negative_conflicting_element_type_suite_case() { + assert_case("negative_conflicting_element_type"); +} + +#[test] +fn positive_overview_partial_subset_suite_case() { + assert_case("positive_overview_partial_subset"); +} + +#[test] +fn positive_three_file_merge_suite_case() { + assert_case("positive_three_file_merge"); +} + +#[test] +fn positive_overview_nested_public_interface_suite_case() { + assert_case("positive_overview_nested_public_interface"); +} diff --git a/validation/core/integration_test/component_model/negative_children_split_across_files/BUILD b/validation/core/integration_test/component_model/negative_children_split_across_files/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_children_split_across_files/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/internal_api_diagram.puml b/validation/core/integration_test/component_model/negative_children_split_across_files/detail_diagram.puml similarity index 76% rename from validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/internal_api_diagram.puml rename to validation/core/integration_test/component_model/negative_children_split_across_files/detail_diagram.puml index 1e662a82..3612cb2b 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/internal_api_diagram.puml +++ b/validation/core/integration_test/component_model/negative_children_split_across_files/detail_diagram.puml @@ -11,11 +11,11 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml internal_api_diagram +@startuml detail_diagram -package package_a { - interface "Internal Interface" as iface_a { - + GetData() +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> } } diff --git a/validation/core/integration_test/component_model/negative_children_split_across_files/expected.yaml b/validation/core/integration_test/component_model/negative_children_split_across_files/expected.yaml new file mode 100644 index 00000000..7ce7f521 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_children_split_across_files/expected.yaml @@ -0,0 +1,15 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Design] Entity "component_a" has children declared across more than one component diagram file, with no single file containing all of them. diff --git a/validation/core/integration_test/component_model/negative_children_split_across_files/overview_diagram.puml b/validation/core/integration_test/component_model/negative_children_split_across_files/overview_diagram.puml new file mode 100644 index 00000000..a3039bdc --- /dev/null +++ b/validation/core/integration_test/component_model/negative_children_split_across_files/overview_diagram.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Deliberately wrong: this file adds a *different* child ("unit_2") for +' component_a instead of merely re-declaring the child(ren) that +' detail_diagram.puml already declares -- component_a's decomposition ends up +' split across both files with no single file describing it in full. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/negative_conflicting_element_type/BUILD b/validation/core/integration_test/component_model/negative_conflicting_element_type/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_element_type/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_model/negative_conflicting_element_type/detail_diagram.puml b/validation/core/integration_test/component_model/negative_conflicting_element_type/detail_diagram.puml new file mode 100644 index 00000000..e66617c8 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_element_type/detail_diagram.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' 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 detail_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Shared Thing" as shared_thing <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/negative_conflicting_element_type/expected.yaml b/validation/core/integration_test/component_model/negative_conflicting_element_type/expected.yaml new file mode 100644 index 00000000..3f94a08d --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_element_type/expected.yaml @@ -0,0 +1,15 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Design] Component "shared_thing" is re-declared with a conflicting element type in another component diagram file. diff --git a/validation/core/integration_test/component_model/negative_conflicting_element_type/overview_diagram.puml b/validation/core/integration_test/component_model/negative_conflicting_element_type/overview_diagram.puml new file mode 100644 index 00000000..5ce73269 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_element_type/overview_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Deliberately wrong: "shared_thing" is re-declared here as an <> +' *interface* element instead of the <> *component* element it is +' declared as in detail_diagram.puml. The stereotype text matches on purpose, +' so this exercises the element-type conflict path rather than the +' stereotype conflict path. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + interface "Shared Thing" as shared_thing <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/negative_conflicting_stereotype/BUILD b/validation/core/integration_test/component_model/negative_conflicting_stereotype/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_stereotype/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/internal_api_diagram.puml b/validation/core/integration_test/component_model/negative_conflicting_stereotype/detail_diagram.puml similarity index 75% rename from validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/internal_api_diagram.puml rename to validation/core/integration_test/component_model/negative_conflicting_stereotype/detail_diagram.puml index f86b6817..cdc5a8a0 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/internal_api_diagram.puml +++ b/validation/core/integration_test/component_model/negative_conflicting_stereotype/detail_diagram.puml @@ -11,11 +11,11 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml internal_api_diagram +@startuml detail_diagram -package package_a { - interface "InternalInterface" as InternalInterface { - + GetData() +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Shared Thing" as shared_thing <> } } diff --git a/validation/core/integration_test/component_model/negative_conflicting_stereotype/expected.yaml b/validation/core/integration_test/component_model/negative_conflicting_stereotype/expected.yaml new file mode 100644 index 00000000..2b883b79 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_stereotype/expected.yaml @@ -0,0 +1,15 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: false +error_contains: | + [Design] Unit "shared_thing" is re-declared with a conflicting stereotype in another component diagram file. diff --git a/validation/core/integration_test/component_model/negative_conflicting_stereotype/overview_diagram.puml b/validation/core/integration_test/component_model/negative_conflicting_stereotype/overview_diagram.puml new file mode 100644 index 00000000..58608bc5 --- /dev/null +++ b/validation/core/integration_test/component_model/negative_conflicting_stereotype/overview_diagram.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 +' ******************************************************************************* + +' Deliberately wrong: "shared_thing" is re-declared here as a <> +' instead of the <> it is declared as in detail_diagram.puml. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Shared Thing" as shared_thing <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/BUILD b/validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/BUILD similarity index 95% rename from validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/BUILD rename to validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/BUILD index 26b6054f..b4e3d73d 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/BUILD +++ b/validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/BUILD @@ -16,7 +16,6 @@ load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixtur architectural_design( name = "design", - internal_api = ["internal_api_diagram.puml"], maturity = "development", static = ["component_diagram.puml"], visibility = ["//visibility:private"], diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/component_diagram.puml b/validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/component_diagram.puml rename to validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/component_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/expected.yaml b/validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/expected.yaml similarity index 85% rename from validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/expected.yaml rename to validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/expected.yaml index 5bf3a78f..5d01566d 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/expected.yaml +++ b/validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/expected.yaml @@ -15,8 +15,8 @@ error_contains: | [Design] Component "component_a" is defined more than once in the component diagram. Component : "component_a" Parent : package_a - Component source file : "validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/component_diagram.puml" + Component source file : "validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/component_diagram.puml" Component source line : 17 - Duplicate source file : "validation/core/integration_test/component_internal_api/negative_duplicate_component_alias_casefolded/component_diagram.puml" + Duplicate source file : "validation/core/integration_test/component_model/negative_duplicate_component_alias_casefolded/component_diagram.puml" Duplicate source line : 18 Fix : Keep only one component "component_a" under "package_a", or rename one of the duplicate entities. diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/BUILD b/validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/BUILD similarity index 95% rename from validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/BUILD rename to validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/BUILD index 26b6054f..b4e3d73d 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/BUILD +++ b/validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/BUILD @@ -16,7 +16,6 @@ load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixtur architectural_design( name = "design", - internal_api = ["internal_api_diagram.puml"], maturity = "development", static = ["component_diagram.puml"], visibility = ["//visibility:private"], diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml b/validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml rename to validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/expected.yaml b/validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/expected.yaml similarity index 84% rename from validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/expected.yaml rename to validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/expected.yaml index b4d6499d..95bbfb52 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/expected.yaml +++ b/validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/expected.yaml @@ -15,8 +15,8 @@ error_contains: | [Design] Dependable element "seooc_a" is defined more than once in the component diagram. Dependable element : "seooc_a" Parent : - Component source file : "validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml" + Component source file : "validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml" Component source line : 16 - Duplicate source file : "validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml" + Duplicate source file : "validation/core/integration_test/component_model/negative_duplicate_dependable_element_alias_casefolded/component_diagram.puml" Duplicate source line : 20 Fix : Keep only one dependable element "seooc_a" under "", or rename one of the duplicate entities. diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/BUILD b/validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/BUILD similarity index 95% rename from validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/BUILD rename to validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/BUILD index 26b6054f..b4e3d73d 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/BUILD +++ b/validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/BUILD @@ -16,7 +16,6 @@ load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixtur architectural_design( name = "design", - internal_api = ["internal_api_diagram.puml"], maturity = "development", static = ["component_diagram.puml"], visibility = ["//visibility:private"], diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/component_diagram.puml b/validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/component_diagram.puml rename to validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/component_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/expected.yaml b/validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/expected.yaml similarity index 85% rename from validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/expected.yaml rename to validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/expected.yaml index fc64da52..1198a851 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/expected.yaml +++ b/validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/expected.yaml @@ -15,8 +15,8 @@ error_contains: | [Design] Interface "iface_a" is defined more than once in the component diagram. Interface : "iface_a" Parent : package_a - Component source file : "validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/component_diagram.puml" + Component source file : "validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/component_diagram.puml" Component source line : 17 - Duplicate source file : "validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/component_diagram.puml" + Duplicate source file : "validation/core/integration_test/component_model/negative_duplicate_interface_alias_casefolded/component_diagram.puml" Duplicate source line : 18 Fix : Keep only one interface "iface_a" under "package_a", or rename one of the duplicate entities. diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/BUILD b/validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/BUILD similarity index 95% rename from validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/BUILD rename to validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/BUILD index 26b6054f..b4e3d73d 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/BUILD +++ b/validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/BUILD @@ -16,7 +16,6 @@ load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixtur architectural_design( name = "design", - internal_api = ["internal_api_diagram.puml"], maturity = "development", static = ["component_diagram.puml"], visibility = ["//visibility:private"], diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/component_diagram.puml b/validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/component_diagram.puml similarity index 100% rename from validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/component_diagram.puml rename to validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/component_diagram.puml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/expected.yaml b/validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/expected.yaml similarity index 86% rename from validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/expected.yaml rename to validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/expected.yaml index 27c7b9dc..df91df4f 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/expected.yaml +++ b/validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/expected.yaml @@ -15,8 +15,8 @@ error_contains: | [Design] Unit "unit_1" is defined more than once in the component diagram. Unit : "unit_1" Parent : component_a - Component source file : "validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/component_diagram.puml" + Component source file : "validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/component_diagram.puml" Component source line : 18 - Duplicate source file : "validation/core/integration_test/component_internal_api/negative_duplicate_unit_alias_casefolded/component_diagram.puml" + Duplicate source file : "validation/core/integration_test/component_model/negative_duplicate_unit_alias_casefolded/component_diagram.puml" Duplicate source line : 19 Fix : Keep only one unit "unit_1" under "component_a", or rename one of the duplicate entities. diff --git a/validation/core/integration_test/component_model/positive_overview_nested_public_interface/BUILD b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_model/positive_overview_nested_public_interface/detail_diagram.puml b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/detail_diagram.puml new file mode 100644 index 00000000..a8d7ee92 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/detail_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Detail: nests an *internal* interface under component_a. overview_diagram.puml +' nests a different, *public* interface under the same component_a. Neither +' file contains the other's interface, but interfaces are not "children" for +' the purpose of the single-home decomposition check, so this must not be +' reported as a split decomposition. + +@startuml detail_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + interface "Internal Interface" as iface_int + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_overview_nested_public_interface/expected.yaml b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_model/positive_overview_nested_public_interface/overview_diagram.puml b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/overview_diagram.puml new file mode 100644 index 00000000..76def82e --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_nested_public_interface/overview_diagram.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 +' ******************************************************************************* + +' Overview: nests a *public* interface under component_a, distinct from the +' *internal* interface detail_diagram.puml nests under the same component_a. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + interface "Public Interface" as iface_pub + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_overview_partial_subset/BUILD b/validation/core/integration_test/component_model/positive_overview_partial_subset/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_partial_subset/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_model/positive_overview_partial_subset/detail_diagram.puml b/validation/core/integration_test/component_model/positive_overview_partial_subset/detail_diagram.puml new file mode 100644 index 00000000..5eebe2cb --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_partial_subset/detail_diagram.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 detail_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_overview_partial_subset/expected.yaml b/validation/core/integration_test/component_model/positive_overview_partial_subset/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_partial_subset/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_model/positive_overview_partial_subset/overview_diagram.puml b/validation/core/integration_test/component_model/positive_overview_partial_subset/overview_diagram.puml new file mode 100644 index 00000000..32c28003 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_partial_subset/overview_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Overview: re-declares component_a with only a strict, non-empty subset of +' the children declared in detail_diagram.puml (unit_1 only, out of +' {unit_1, unit_2}). Since detail_diagram.puml contains the full set, this +' is a benign "home file + partial overview" split, not a real decomposition +' conflict. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_overview_subset/BUILD b/validation/core/integration_test/component_model/positive_overview_subset/BUILD new file mode 100644 index 00000000..d12fcf60 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_subset/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_model/positive_overview_subset/detail_diagram.puml b/validation/core/integration_test/component_model/positive_overview_subset/detail_diagram.puml new file mode 100644 index 00000000..9ab79e44 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_subset/detail_diagram.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 detail_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + } + + interface "Internal Interface" as iface_a + unit_1 -( iface_a +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_overview_subset/expected.yaml b/validation/core/integration_test/component_model/positive_overview_subset/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_subset/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_model/positive_overview_subset/overview_diagram.puml b/validation/core/integration_test/component_model/positive_overview_subset/overview_diagram.puml new file mode 100644 index 00000000..9f462041 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_overview_subset/overview_diagram.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 +' ******************************************************************************* + +' Boundary overview: re-declares package_a and its top-level component_a +' without their nested detail, which lives solely in detail_diagram.puml. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_three_file_merge/BUILD b/validation/core/integration_test/component_model/positive_three_file_merge/BUILD new file mode 100644 index 00000000..88519344 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_three_file_merge/BUILD @@ -0,0 +1,41 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + maturity = "development", + static = [ + "overview_diagram.puml", + "detail_diagram.puml", + "full_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_model/positive_three_file_merge/detail_diagram.puml b/validation/core/integration_test/component_model/positive_three_file_merge/detail_diagram.puml new file mode 100644 index 00000000..50cec7f9 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_three_file_merge/detail_diagram.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 +' ******************************************************************************* + +' Partial detail: re-declares component_a with only unit_1, a strict subset +' of the full decomposition declared in full_diagram.puml. + +@startuml detail_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_three_file_merge/expected.yaml b/validation/core/integration_test/component_model/positive_three_file_merge/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_model/positive_three_file_merge/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_model/positive_three_file_merge/full_diagram.puml b/validation/core/integration_test/component_model/positive_three_file_merge/full_diagram.puml new file mode 100644 index 00000000..da234fba --- /dev/null +++ b/validation/core/integration_test/component_model/positive_three_file_merge/full_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Home file: the only file containing the full decomposition of component_a +' (unit_1 and unit_2). Together with overview_diagram.puml (no children) and +' detail_diagram.puml (unit_1 only, a subset), this stresses the +' single-home-file check across three files instead of just two. + +@startuml full_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_model/positive_three_file_merge/overview_diagram.puml b/validation/core/integration_test/component_model/positive_three_file_merge/overview_diagram.puml new file mode 100644 index 00000000..3ef62feb --- /dev/null +++ b/validation/core/integration_test/component_model/positive_three_file_merge/overview_diagram.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 +' ******************************************************************************* + +' Boundary overview: bare-declares package_a and its top-level component_a, +' with no children at all. + +@startuml overview_diagram + +package "Package A" as package_a <> { + component "Component A" as component_a <> +} + +@enduml diff --git a/validation/core/integration_test/component_public_api/BUILD b/validation/core/integration_test/component_public_api/BUILD index 979d7bdc..084936a5 100644 --- a/validation/core/integration_test/component_public_api/BUILD +++ b/validation/core/integration_test/component_public_api/BUILD @@ -22,6 +22,7 @@ filegroup( "//validation/core/integration_test/component_public_api/negative_public_api_missing_with_suggestion:case_data", "//validation/core/integration_test/component_public_api/negative_public_api_missing_with_suggestions:case_data", "//validation/core/integration_test/component_public_api/negative_public_api_wrong_type:case_data", + "//validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge:case_data", "//validation/core/integration_test/component_public_api/positive_public_api_match:case_data", ], ) diff --git a/validation/core/integration_test/component_public_api/component_public_api_suite.rs b/validation/core/integration_test/component_public_api/component_public_api_suite.rs index 908d5aea..9f78c924 100644 --- a/validation/core/integration_test/component_public_api/component_public_api_suite.rs +++ b/validation/core/integration_test/component_public_api/component_public_api_suite.rs @@ -54,6 +54,11 @@ fn positive_public_api_match_suite_case() { assert_case("positive_public_api_match"); } +#[test] +fn positive_overview_seooc_relation_merge_suite_case() { + assert_case("positive_overview_seooc_relation_merge"); +} + #[test] fn negative_public_api_missing_suite_case() { assert_case("negative_public_api_missing"); diff --git a/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/BUILD b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/BUILD new file mode 100644 index 00000000..9b03d374 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + public_api = ["public_api.puml"], + static = [ + "static_design.puml", + "overview_design.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:private"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//validation/core/integration_test:__subpackages__"], +) diff --git a/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/expected.yaml b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/overview_design.puml b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/overview_design.puml new file mode 100644 index 00000000..82a42414 --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/overview_design.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 +' ******************************************************************************* + +' Boundary overview: bare re-declaration of the SEooC and its top-level +' component, without repeating the connection to SampleLibraryAPI. Proves +' that the SEooC-to-public-API relation declared only in static_design.puml +' still survives the merge and satisfies the public API validator. + +@startuml overview_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> +} + +@enduml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/internal_api_diagram.puml b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/public_api.puml similarity index 82% rename from validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/internal_api_diagram.puml rename to validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/public_api.puml index 0d11d24d..7fba8dbe 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_dependable_element_alias_casefolded/internal_api_diagram.puml +++ b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/public_api.puml @@ -11,12 +11,10 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml internal_api_diagram +@startuml public_api -package seooc_a { - interface "Internal Interface" as iface_a { - + GetData() - } +interface "Sample Library API" as SampleLibraryAPI <> { + +GetNumber(): int } @enduml diff --git a/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/static_design.puml b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/static_design.puml new file mode 100644 index 00000000..1df6cece --- /dev/null +++ b/validation/core/integration_test/component_public_api/positive_overview_seooc_relation_merge/static_design.puml @@ -0,0 +1,26 @@ +' ******************************************************************************* +' 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_design + +package "Sample Seooc" as sample_seooc <> { + component "Component Example" as component_example <> { + component "Unit 1" as unit_1 <> + } +} + +interface "SampleLibraryAPI" as SampleLibraryAPI + +sample_seooc )-d- SampleLibraryAPI + +@enduml diff --git a/validation/core/integration_test/component_sequence/BUILD b/validation/core/integration_test/component_sequence/BUILD index 05088387..b0ef8c45 100644 --- a/validation/core/integration_test/component_sequence/BUILD +++ b/validation/core/integration_test/component_sequence/BUILD @@ -25,6 +25,7 @@ filegroup( "//validation/core/integration_test/component_sequence/positive_exact_match:case_data", "//validation/core/integration_test/component_sequence/positive_external_callee_in_sequence_return:case_data", "//validation/core/integration_test/component_sequence/positive_external_caller_in_sequence_connection:case_data", + "//validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings:case_data", ], ) diff --git a/validation/core/integration_test/component_sequence/component_sequence_suite.rs b/validation/core/integration_test/component_sequence/component_sequence_suite.rs index 7abd4db4..8407382e 100644 --- a/validation/core/integration_test/component_sequence/component_sequence_suite.rs +++ b/validation/core/integration_test/component_sequence/component_sequence_suite.rs @@ -56,6 +56,11 @@ fn positive_exact_match_suite_case() { assert_case("positive_exact_match"); } +#[test] +fn positive_overview_preserves_unit_bindings_suite_case() { + assert_case("positive_overview_preserves_unit_bindings"); +} + #[test] fn negative_missing_participant_suite_case() { assert_case("negative_missing_participant"); diff --git a/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/BUILD b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/BUILD new file mode 100644 index 00000000..e45ea888 --- /dev/null +++ b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/BUILD @@ -0,0 +1,40 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//bazel/rules/rules_score:rules_score.bzl", "architectural_design") +load("//validation/core/integration_test:puml_fixture.bzl", "provider_fbs_fixture_bundle") + +architectural_design( + name = "design", + dynamic = ["sequence_diagram.puml"], + static = [ + "detail_diagram.puml", + "overview_diagram.puml", + ], + visibility = ["//visibility:private"], +) + +provider_fbs_fixture_bundle( + name = "fbs", + visibility = ["//visibility:public"], + deps = [":design"], +) + +filegroup( + name = "case_data", + srcs = [ + "expected.yaml", + ":fbs", + ], + visibility = ["//visibility:public"], +) diff --git a/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/detail_diagram.puml b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/detail_diagram.puml new file mode 100644 index 00000000..96c77e6e --- /dev/null +++ b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/detail_diagram.puml @@ -0,0 +1,27 @@ +' ******************************************************************************* +' 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 detail_diagram + +package "Package A" as package_a { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } + + interface "InternalInterface" as InternalInterface + unit_1 -( InternalInterface + unit_2 )- InternalInterface +} + +@enduml diff --git a/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/expected.yaml b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/expected.yaml new file mode 100644 index 00000000..898ecba3 --- /dev/null +++ b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/expected.yaml @@ -0,0 +1,13 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +should_pass: true diff --git a/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/overview_diagram.puml b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/overview_diagram.puml new file mode 100644 index 00000000..f677dfe6 --- /dev/null +++ b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/overview_diagram.puml @@ -0,0 +1,28 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + +' Boundary overview: bare re-declaration of package_a/component_a and its +' units, without repeating the InternalInterface bindings. Proves that the +' required/provided interface bindings declared only in detail_diagram.puml +' still survive the merge and satisfy the sequence validator. + +@startuml overview_diagram + +package "Package A" as package_a { + component "Component A" as component_a <> { + component "Unit 1" as unit_1 <> + component "Unit 2" as unit_2 <> + } +} + +@enduml diff --git a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/internal_api_diagram.puml b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/sequence_diagram.puml similarity index 78% rename from validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/internal_api_diagram.puml rename to validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/sequence_diagram.puml index 1e662a82..fcca6a92 100644 --- a/validation/core/integration_test/component_internal_api/negative_duplicate_interface_alias_casefolded/internal_api_diagram.puml +++ b/validation/core/integration_test/component_sequence/positive_overview_preserves_unit_bindings/sequence_diagram.puml @@ -11,12 +11,12 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml internal_api_diagram +@startuml sequence_diagram -package package_a { - interface "Internal Interface" as iface_a { - + GetData() - } -} +participant "Unit 1" as unit_1 <> +participant "Unit 2" as unit_2 <> + +unit_1 -> unit_2 : SendSignal +unit_2 --> unit_1 : Ack @enduml diff --git a/validation/core/src/models/component_diagram_models.rs b/validation/core/src/models/component_diagram_models.rs index 2e06aacd..45b932b9 100644 --- a/validation/core/src/models/component_diagram_models.rs +++ b/validation/core/src/models/component_diagram_models.rs @@ -11,6 +11,8 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +mod component_diagram_merge; + use std::collections::BTreeMap; use super::EntityKey; @@ -85,9 +87,6 @@ pub struct ComponentDiagramArchitecture { pub unit_set: BTreeMap, /// Full raw entity list, kept for debug output. pub entities: Vec, - pub filtered_seooc_count: usize, - pub filtered_component_count: usize, - pub filtered_unit_count: usize, } impl ComponentDiagramArchitecture { @@ -96,7 +95,9 @@ impl ComponentDiagramArchitecture { /// `<>` go into `seooc_set`; /// `<>` go into `comp_set`; /// `<>` go into `unit_set`. - /// Duplicates (same [`EntityKey`]) are reported via `result`. + /// Duplicates (same [`EntityKey`]) are reported via `result`, except + /// benign re-declarations of the exact same entity across multiple + /// `static` files (see [`Self::build_set`]), which are merged instead. fn from_entities(entities: &[LogicComponent], result: &mut ValidationResult) -> Self { // Index by raw id for parent resolution; PlantUML nesting uses id, // not alias. @@ -104,6 +105,21 @@ impl ComponentDiagramArchitecture { for entity in entities { let key = entity.id.to_lowercase(); if let Some(prev) = id_index.insert(key.clone(), entity) { + // Same id: benign re-declaration (merged in build_set) or a + // conflicting one; a different id colliding after lowercasing + // is a genuine duplicate-alias error instead. + if prev.id == entity.id { + if let Some(field) = + component_diagram_merge::conflicting_declaration_field(prev, entity) + { + result.add_failure( + component_diagram_merge::format_conflicting_declaration_error( + prev, entity, field, + ), + ); + } + continue; + } let kind = entity_kind_name(entity); let alias = entity.match_key(); let parent = @@ -129,6 +145,8 @@ impl ComponentDiagramArchitecture { } } + component_diagram_merge::check_single_home_decomposition(entities, result); + let seoocs: Vec<&LogicComponent> = entities .iter() .filter(|entity| entity.is_seooc_package()) @@ -140,10 +158,6 @@ impl ComponentDiagramArchitecture { let units: Vec<&LogicComponent> = entities.iter().filter(|entity| entity.is_unit()).collect(); - let filtered_seooc_count = seoocs.len(); - 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); @@ -153,9 +167,6 @@ impl ComponentDiagramArchitecture { comp_set, unit_set, entities: entities.to_vec(), - filtered_seooc_count, - filtered_component_count, - filtered_unit_count, } } @@ -164,7 +175,7 @@ impl ComponentDiagramArchitecture { id_index: &BTreeMap, result: &mut ValidationResult, ) -> BTreeMap { - let mut set = BTreeMap::new(); + let mut set: BTreeMap = BTreeMap::new(); for entity in items { let alias = entity.match_key(); let parent_alias = match &entity.parent_id { @@ -194,6 +205,13 @@ impl ComponentDiagramArchitecture { None => None, }; let key = (alias, parent_alias); + // Benign re-declaration: merge relations instead of overwriting. + if let Some(existing) = set.get_mut(&key) { + if existing.id == entity.id { + component_diagram_merge::merge_relations(existing, entity); + continue; + } + } if let Some(prev) = set.insert(key.clone(), (*entity).clone()) { if prev.id.eq_ignore_ascii_case(&entity.id) { continue; diff --git a/validation/core/src/models/component_diagram_models/component_diagram_merge.rs b/validation/core/src/models/component_diagram_models/component_diagram_merge.rs new file mode 100644 index 00000000..ff6b33a5 --- /dev/null +++ b/validation/core/src/models/component_diagram_models/component_diagram_merge.rs @@ -0,0 +1,629 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Cross-file `static` merge policy for [`super::ComponentDiagramArchitecture`]: +//! benign re-declaration merging, conflicting re-declaration detection, and +//! single-home decomposition. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; + +use super::{entity_kind_name, LogicComponent, LogicComponentExt}; +use crate::{ErrorBuilder, ErrorCategory, ValidationResult}; + +/// Merges relations from a repeated declaration into the already-indexed entity. +/// +/// Compares structurally, ignoring `source_location` (unlike `PartialEq`), so +/// a relation re-declared in another file isn't treated as new. +pub(super) fn merge_relations(existing: &mut LogicComponent, incoming: &LogicComponent) { + for relation in &incoming.relations { + let already_present = existing.relations.iter().any(|r| { + r.target == relation.target + && r.relation_type == relation.relation_type + && r.source_role == relation.source_role + && r.annotation == relation.annotation + }); + if !already_present { + existing.relations.push(relation.clone()); + } + } +} + +/// Returns the first field on which two same-id declarations disagree, or +/// `None` if they may be merged. +/// +/// `alias`/`parent` aren't compared: the same `id` already implies both match. +pub(super) fn conflicting_declaration_field( + prev: &LogicComponent, + entity: &LogicComponent, +) -> Option<&'static str> { + if prev.name != entity.name { + return Some("display name"); + } + if prev.stereotype != entity.stereotype { + return Some("stereotype"); + } + if prev.element_type != entity.element_type { + return Some("element type"); + } + None +} + +pub(super) fn format_conflicting_declaration_error( + prev: &LogicComponent, + entity: &LogicComponent, + field: &'static str, +) -> String { + // Order by source location so the message is stable regardless of file order. + let (first, second) = ordered_declarations(prev, entity); + let kind = entity_kind_name(first); + let alias = first.match_key(); + let (source_file, source_line) = first.source_location.display(); + let (conflicting_file, conflicting_line) = second.source_location.display(); + ErrorBuilder::new(ErrorCategory::Design) + .title(format!( + "{kind} \"{alias}\" is re-declared with a conflicting {field} in another component diagram file" + )) + .field(kind, format!("\"{alias}\"")) + .field("conflicting field", field) + .field("component source file", format!("\"{source_file}\"")) + .field("component source line", source_line.to_string()) + .field("conflicting source file", format!("\"{conflicting_file}\"")) + .field("conflicting source line", conflicting_line.to_string()) + .fix(format!( + "make every declaration of \"{alias}\" across all static diagrams agree on {field}, or rename one of the conflicting entities" + )) + .build() +} + +/// Fails when an entity's children are declared across more than one file +/// with no single file containing the full set. +/// +/// Interfaces are exempt since they aren't compared against the Bazel build graph. +pub(super) fn check_single_home_decomposition( + entities: &[LogicComponent], + result: &mut ValidationResult, +) { + // parent id (lowercased) -> file -> child id (lowercased) -> sample entity + let mut children_by_parent: BTreeMap< + String, + BTreeMap>, + > = BTreeMap::new(); + for entity in entities { + if entity.is_interface() { + continue; + } + let Some(parent_id) = &entity.parent_id else { + continue; + }; + let (file, _) = entity.source_location.display(); + children_by_parent + .entry(parent_id.to_lowercase()) + .or_default() + .entry(file) + .or_default() + .insert(entity.id.to_lowercase(), entity); + } + + for (parent_key, by_file) in &children_by_parent { + if by_file.len() <= 1 { + continue; + } + + let all_child_count = by_file + .values() + .flat_map(|children| children.keys()) + .collect::>() + .len(); + let has_single_home_file = by_file + .values() + .any(|children| children.len() == all_child_count); + if has_single_home_file { + continue; + } + + let parent_alias = entities + .iter() + .find(|entity| entity.id.to_lowercase() == *parent_key) + .map(LogicComponentExt::match_key) + .unwrap_or_else(|| parent_key.clone()); + + // Report per file so a benign subset re-declaration isn't shown as a duplicate. + let mut error = ErrorBuilder::new(ErrorCategory::Design) + .title(format!( + "entity \"{parent_alias}\" has children declared across more than one component diagram file, with no single file containing all of them" + )) + .field("entity", format!("\"{parent_alias}\"")); + for (file, children) in by_file { + let children_display = children + .values() + .map(|child| format!("\"{}\"", child.match_key())) + .collect::>() + .join(", "); + error = error.field(format!("children in \"{file}\""), children_display); + } + result.add_failure( + error + .fix(format!( + "declare the full decomposition of \"{parent_alias}\" in a single file; other files may re-declare \"{parent_alias}\" with a subset of its already-declared children (or none), but must not add children missing from every other file" + )) + .build(), + ); + } +} + +/// Orders two declarations by source location, independent of insertion order. +fn ordered_declarations<'a>( + left: &'a LogicComponent, + right: &'a LogicComponent, +) -> (&'a LogicComponent, &'a LogicComponent) { + let left_display = left.source_location.display(); + let right_display = right.source_location.display(); + + if (left_display.0.as_str(), left_display.1) <= (right_display.0.as_str(), right_display.1) { + (left, right) + } else { + (right, left) + } +} + +#[cfg(test)] +mod tests { + use super::super::{ + ComponentDiagramInputs, ComponentRelationType, ComponentType, EndpointRole, LogicRelation, + }; + use super::*; + use crate::validators::fixtures::dummy_source_location; + use crate::ValidationResult; + + fn relation(target: &str) -> LogicRelation { + LogicRelation { + target: target.to_string(), + annotation: None, + relation_type: ComponentRelationType::Association, + source_role: EndpointRole::None, + source_location: dummy_source_location(), + } + } + + fn entity_in_file( + id: &str, + alias: Option<&str>, + parent_id: Option<&str>, + element_type: ComponentType, + stereotype: Option<&str>, + relations: Vec, + file: &str, + ) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: alias.map(str::to_string), + alias: alias.map(str::to_string), + parent_id: parent_id.map(str::to_string), + element_type, + stereotype: stereotype.map(str::to_string), + relations, + source_location: source_location::SourceLocation::new(file, 1), + } + } + + #[test] + fn merges_benign_redeclaration_of_same_entity_across_files() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + vec![relation("iface_x")], + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + vec![relation("iface_y")], + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result.is_empty(), + "expected no failures, got {:?}", + result.failures + ); + let merged = architecture + .comp_set + .get(&("comp_a".to_string(), None)) + .expect("expected merged component entry"); + let mut targets: Vec<&str> = merged + .relations + .iter() + .map(|relation| relation.target.as_str()) + .collect(); + targets.sort_unstable(); + assert_eq!(targets, vec!["iface_x", "iface_y"]); + } + + #[test] + fn merges_relation_repeated_across_files_only_once() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + vec![relation("iface_x")], + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + vec![relation("iface_x")], + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result.is_empty(), + "expected no failures, got {:?}", + result.failures + ); + let merged = architecture + .comp_set + .get(&("comp_a".to_string(), None)) + .expect("expected merged component entry"); + assert_eq!(merged.relations.len(), 1); + } + + #[test] + fn reports_conflicting_element_type() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Interface, + Some("component"), + Vec::new(), + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result + .failures + .iter() + .any(|message| message.contains("is re-declared with a conflicting element type")), + "Expected conflicting element type error, got: {:?}", + result.failures + ); + } + + #[test] + fn reports_conflicting_stereotype() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("unit"), + Vec::new(), + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result + .failures + .iter() + .any(|message| message.contains("is re-declared with a conflicting stereotype")), + "Expected conflicting stereotype error, got: {:?}", + result.failures + ); + } + + #[test] + fn reports_conflicting_display_name() { + let mut first = entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ); + first.name = Some("Component A".to_string()); + let mut second = entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "overview.puml", + ); + second.name = Some("Component A (renamed)".to_string()); + + let inputs = ComponentDiagramInputs { + entities: vec![first, second], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result + .failures + .iter() + .any(|message| message.contains("is re-declared with a conflicting display name")), + "Expected conflicting display name error, got: {:?}", + result.failures + ); + } + + #[test] + fn reports_children_declared_across_multiple_files() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "overview.puml", + ), + entity_in_file( + "comp_a.unit_1", + Some("unit_1"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a.unit_2", + Some("unit_2"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result.failures.iter().any(|message| message + .contains("has children declared across more than one component diagram file")), + "Expected single-home decomposition error, got: {:?}", + result.failures + ); + } + + #[test] + fn overview_equal_children_does_not_split_decomposition() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "overview.puml", + ), + entity_in_file( + "comp_a.unit_1", + Some("unit_1"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a.unit_1", + Some("unit_1"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result.is_empty(), + "expected no failures for a benign identical re-declaration, got: {:?}", + result.failures + ); + } + + #[test] + fn overview_strict_subset_of_detail_children_does_not_split_decomposition() { + let inputs = ComponentDiagramInputs { + entities: vec![ + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "overview.puml", + ), + entity_in_file( + "comp_a.unit_1", + Some("unit_1"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a.unit_2", + Some("unit_2"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "detail.puml", + ), + entity_in_file( + "comp_a.unit_1", + Some("unit_1"), + Some("comp_a"), + ComponentType::Component, + Some("unit"), + Vec::new(), + "overview.puml", + ), + ], + }; + + let mut result = ValidationResult::default(); + let _architecture = inputs.to_diagram_architecture(&mut result); + + assert!( + result.is_empty(), + "expected no failures for a strict-subset re-declaration, got: {:?}", + result.failures + ); + } + + #[test] + fn conflicting_declaration_error_is_order_independent() { + let first = entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("component"), + Vec::new(), + "detail.puml", + ); + let second = entity_in_file( + "comp_a", + Some("comp_a"), + None, + ComponentType::Component, + Some("unit"), + Vec::new(), + "overview.puml", + ); + + let mut forward_result = ValidationResult::default(); + let _forward_architecture = ComponentDiagramInputs { + entities: vec![first.clone(), second.clone()], + } + .to_diagram_architecture(&mut forward_result); + + let mut reversed_result = ValidationResult::default(); + let _reversed_architecture = ComponentDiagramInputs { + entities: vec![second, first], + } + .to_diagram_architecture(&mut reversed_result); + + assert_eq!( + forward_result.failures, reversed_result.failures, + "expected the conflicting-declaration error to be order-independent" + ); + } +} diff --git a/validation/core/src/validators/bazel_component_validator.rs b/validation/core/src/validators/bazel_component_validator.rs index 8ce0c00d..486af300 100644 --- a/validation/core/src/validators/bazel_component_validator.rs +++ b/validation/core/src/validators/bazel_component_validator.rs @@ -266,9 +266,9 @@ fn append_debug_log( diagnostics.debug(|| { format!( "Filtered to {} SEooC packages, {} components and {} units", - diagram.filtered_seooc_count, - diagram.filtered_component_count, - diagram.filtered_unit_count + diagram.seooc_set.len(), + diagram.comp_set.len(), + diagram.unit_set.len() ) }); diagnostics.debug(|| "PlantUML SEooC set:".to_string()); diff --git a/validation/core/src/validators/component_internal_api_validator.rs b/validation/core/src/validators/component_internal_api_validator.rs index f74d84fd..ddcdf0fe 100644 --- a/validation/core/src/validators/component_internal_api_validator.rs +++ b/validation/core/src/validators/component_internal_api_validator.rs @@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use super::shared::{best_string_suggestion, format_name_list}; +use super::shared::{best_string_suggestion, earliest_source_by_id, format_name_list}; use crate::models::{ComponentDiagramArchitecture, InternalApiIndex, LogicComponentExt}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -102,12 +102,13 @@ fn append_debug_log( fn collect_component_internal_interface_sources( component_diagram: &ComponentDiagramArchitecture, ) -> BTreeMap { - component_diagram - .entities - .iter() - .filter(|entity| entity.is_interface() && entity.parent_id.is_some()) - .map(|entity| (entity.id.clone(), entity.source_location.clone())) - .collect() + earliest_source_by_id( + component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface() && entity.parent_id.is_some()) + .map(|entity| (entity.id.clone(), entity.source_location.clone())), + ) } fn collect_internal_api_interface_ids(internal_api_diagram: &InternalApiIndex) -> BTreeSet { diff --git a/validation/core/src/validators/component_public_api_validator.rs b/validation/core/src/validators/component_public_api_validator.rs index 22ff2244..087ef049 100644 --- a/validation/core/src/validators/component_public_api_validator.rs +++ b/validation/core/src/validators/component_public_api_validator.rs @@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use super::shared::{best_string_suggestion, format_name_list}; +use super::shared::{best_string_suggestion, earliest_source_by_id, format_name_list}; use crate::models::{ComponentDiagramArchitecture, LogicComponentExt, PublicApiIndex}; use crate::results::{ErrorBuilder, ErrorCategory}; use crate::{Diagnostics, ValidationResult}; @@ -127,12 +127,13 @@ fn append_debug_log( fn collect_component_public_api_sources( component_diagram: &ComponentDiagramArchitecture, ) -> BTreeMap { - component_diagram - .entities - .iter() - .filter(|entity| entity.is_interface() && entity.parent_id.is_none()) - .map(|entity| (entity.id.clone(), entity.source_location.clone())) - .collect() + earliest_source_by_id( + component_diagram + .entities + .iter() + .filter(|entity| entity.is_interface() && entity.parent_id.is_none()) + .map(|entity| (entity.id.clone(), entity.source_location.clone())), + ) } fn collect_seooc_related_public_api_ids( diff --git a/validation/core/src/validators/shared/diagram_analysis.rs b/validation/core/src/validators/shared/diagram_analysis.rs index 14bc5a52..06b169d4 100644 --- a/validation/core/src/validators/shared/diagram_analysis.rs +++ b/validation/core/src/validators/shared/diagram_analysis.rs @@ -59,11 +59,9 @@ pub(in crate::validators) fn build_unit_bindings( .collect(); let mut unit_bindings = BTreeMap::new(); - for entity in component_diagram - .entities - .iter() - .filter(|entity| entity.is_unit()) - { + // `unit_set` is already merged and keyed by (alias, parent), so units + // sharing an alias under different parents stay distinct. + for entity in component_diagram.unit_set.values() { let Some(alias) = entity.alias.clone() else { continue; }; @@ -132,3 +130,116 @@ pub(in crate::validators) fn build_observed_call_contexts<'a>( }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{ComponentDiagramInputs, ComponentType, LogicComponent, LogicRelation}; + use crate::validators::fixtures::dummy_source_location; + use crate::ValidationResult; + + fn entity( + id: &str, + alias: &str, + parent_id: Option<&str>, + element_type: ComponentType, + stereotype: Option<&str>, + relations: Vec, + ) -> LogicComponent { + LogicComponent { + id: id.to_string(), + name: Some(alias.to_string()), + alias: Some(alias.to_string()), + parent_id: parent_id.map(str::to_string), + element_type, + stereotype: stereotype.map(str::to_string), + relations, + source_location: dummy_source_location(), + } + } + + fn interface_binding(target: &str, source_role: EndpointRole) -> LogicRelation { + LogicRelation { + target: target.to_string(), + annotation: None, + relation_type: ComponentRelationType::InterfaceBinding, + source_role, + source_location: dummy_source_location(), + } + } + + #[test] + fn units_sharing_an_alias_under_different_parents_are_not_merged() { + let entities = vec![ + entity( + "comp_a", + "comp_a", + None, + ComponentType::Component, + Some("component"), + Vec::new(), + ), + entity( + "comp_b", + "comp_b", + None, + ComponentType::Component, + Some("component"), + Vec::new(), + ), + entity( + "iface_a", + "iface_a", + None, + ComponentType::Interface, + None, + Vec::new(), + ), + entity( + "iface_b", + "iface_b", + None, + ComponentType::Interface, + None, + Vec::new(), + ), + entity( + "comp_a.unit_x", + "unit_x", + Some("comp_a"), + ComponentType::Component, + Some("unit"), + vec![interface_binding("iface_a", EndpointRole::Provided)], + ), + entity( + "comp_b.unit_x", + "unit_x", + Some("comp_b"), + ComponentType::Component, + Some("unit"), + vec![interface_binding("iface_b", EndpointRole::Required)], + ), + ]; + + let mut result = ValidationResult::default(); + let architecture = ComponentDiagramInputs { entities }.to_diagram_architecture(&mut result); + assert!( + result.is_empty(), + "expected no failures, got {:?}", + result.failures + ); + + // Different parents -> distinct `unit_set` entries. + assert_eq!(architecture.unit_set.len(), 2); + + let bindings = build_unit_bindings(&architecture); + let unit_x = bindings.get("unit_x").expect("expected a unit_x entry"); + // Bare-alias collision resolves to one entry; it must reflect only one unit's interfaces. + assert!( + unit_x.all_interfaces == BTreeSet::from(["iface_a".to_string()]) + || unit_x.all_interfaces == BTreeSet::from(["iface_b".to_string()]), + "expected exactly one unit's interfaces, got {:?}", + unit_x.all_interfaces + ); + } +} diff --git a/validation/core/src/validators/shared/helpers.rs b/validation/core/src/validators/shared/helpers.rs index 789e1f08..d2f49644 100644 --- a/validation/core/src/validators/shared/helpers.rs +++ b/validation/core/src/validators/shared/helpers.rs @@ -13,8 +13,9 @@ //! Helper functions shared by validators. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; +use source_location::SourceLocation; use strsim::jaro_winkler; pub(in crate::validators) const DEFAULT_SUGGESTION_THRESHOLD: f64 = 0.75; @@ -31,6 +32,26 @@ pub(in crate::validators) fn format_name_list(names: &BTreeSet) -> Strin .join(", ") } +/// Reduces `(id, source_location)` pairs to one entry per `id`, keeping the +/// earliest location (by file, then line) so the result doesn't depend on +/// iteration order. +pub(in crate::validators) fn earliest_source_by_id( + entries: impl IntoIterator, +) -> BTreeMap { + let mut result: BTreeMap = BTreeMap::new(); + for (id, location) in entries { + result + .entry(id) + .and_modify(|existing| { + if location.display() < existing.display() { + *existing = location.clone(); + } + }) + .or_insert(location); + } + result +} + pub(in crate::validators) fn format_sequence_call( caller_unit: &str, callee_unit: &str, @@ -77,7 +98,8 @@ pub(in crate::validators) fn best_string_suggestion<'a>( #[cfg(test)] mod tests { - use super::{best_string_suggestion, DEFAULT_SUGGESTION_THRESHOLD}; + use super::{best_string_suggestion, earliest_source_by_id, DEFAULT_SUGGESTION_THRESHOLD}; + use source_location::SourceLocation; use strsim::jaro_winkler; #[test] @@ -103,4 +125,38 @@ mod tests { assert_eq!(best_string_suggestion("abc", ["xyz"]), None); } + + #[test] + fn earliest_source_by_id_keeps_lexicographically_earlier_file() { + let result = earliest_source_by_id([ + ("id_a".to_string(), SourceLocation::new("overview.puml", 1)), + ("id_a".to_string(), SourceLocation::new("detail.puml", 1)), + ]); + + assert_eq!(result["id_a"].display(), ("detail.puml".to_string(), 1)); + } + + #[test] + fn earliest_source_by_id_keeps_lower_line_in_same_file() { + let result = earliest_source_by_id([ + ("id_a".to_string(), SourceLocation::new("detail.puml", 10)), + ("id_a".to_string(), SourceLocation::new("detail.puml", 3)), + ]); + + assert_eq!(result["id_a"].display(), ("detail.puml".to_string(), 3)); + } + + #[test] + fn earliest_source_by_id_is_order_independent() { + let forward = earliest_source_by_id([ + ("id_a".to_string(), SourceLocation::new("detail.puml", 1)), + ("id_a".to_string(), SourceLocation::new("overview.puml", 1)), + ]); + let reversed = earliest_source_by_id([ + ("id_a".to_string(), SourceLocation::new("overview.puml", 1)), + ("id_a".to_string(), SourceLocation::new("detail.puml", 1)), + ]); + + assert_eq!(forward["id_a"].display(), reversed["id_a"].display()); + } } diff --git a/validation/core/src/validators/shared/mod.rs b/validation/core/src/validators/shared/mod.rs index f67af557..c0c5ab82 100644 --- a/validation/core/src/validators/shared/mod.rs +++ b/validation/core/src/validators/shared/mod.rs @@ -21,6 +21,6 @@ pub(in crate::validators) use diagram_analysis::{ UnitInterfaces, }; pub(in crate::validators) use helpers::{ - best_string_suggestion, extract_method_name, format_name_list, format_sequence_call, - intersect_interfaces, + best_string_suggestion, earliest_source_by_id, extract_method_name, format_name_list, + format_sequence_call, intersect_interfaces, };