Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ Returns `true` when `qualifiedName` matches one of `scope.Subjects` per that sub
or is an exact match of one of `scope.ExplicitMembers` (a bracket-filter-matched definition or
usage). Used by every strategy to decide whether a candidate element belongs in a scoped diagram.

###### `MatchesUnlimitedSubject(string qualifiedName, ExposedScope scope)`

A narrower sibling of `IsInSubjectScope`, added for `InterconnectionViewLayoutStrategy`'s per-branch
depth-limited recursion (see
`docs/design/sysml2-tools-core/layout/internal/interconnection-view-layout-strategy.md`'s
*Per-Branch Depth-Limited Recursion* section). Returns `true` only when `qualifiedName` matches one
of `scope.Subjects` (by the same equals-or-subtree-prefix / direct-child rule `IsInSubjectScope`
uses per recursion kind) **and** that specific matched subject's own `ExposeRecursionKind` is
`MembershipRecursive` or `NamespaceRecursive` — i.e. only ever `true` for a match against an
unlimited-depth (`expose X::**;` / `expose X::*::**;`) subject. It never returns `true` via
`scope.ExplicitMembers` (a bracket-filter match is always exact-only, regardless of any subject's
recursion kind elsewhere in the same scope) and never returns `true` merely because *some other*,
unrelated subject in `scope.Subjects` happens to be recursive — only the specific subject that
`qualifiedName` itself matches is considered. Unlike `IsInSubjectScope` (which answers "is this
qualified name in scope at all, considering every subject's own recursion kind together"), this
method deliberately does **not** re-derive its answer from `IsInSubjectScope`'s combined `bool`
result: the caller needs to know not just *whether* `qualifiedName` is in scope, but specifically
*which* subject matched it and *that* subject's own recursion kind — information `IsInSubjectScope`
does not expose. When `qualifiedName` matches more than one subject with different recursion kinds
(possible when two overlapping `expose` statements both name an ancestor of the same feature), this
method returns `true` if *any* matched subject is recursive — most-permissive-wins, the same
inclusive-OR semantics `IsInSubjectScope` itself already uses across subjects.

###### `IsRootRelevantToScope(string candidateQualifiedName, ExposedScope scope)`

Returns `true` when `candidateQualifiedName` (a candidate single-root diagram root, e.g. the
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,29 @@ sections:
usages, not just definitions, and a definitions-only candidate set could never match one.
tests:
- ResolveExposedScope_BracketFilterMetaclassKind_MatchesUsageLevelCandidate

- id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-UnlimitedSubjectMatch
title: >-
ExposeScopeResolver.MatchesUnlimitedSubject shall return true for a qualified name that
matches a scope subject (by IsInSubjectScope's own equals-or-subtree-prefix or
direct-child rule per that subject's recursion kind) whose own ExposeRecursionKind is
MembershipRecursive or NamespaceRecursive, and false for a match against any other
recursion kind, a match found only via ExplicitMembers, or an unrelated qualified name.
justification: |
InterconnectionViewLayoutStrategy's per-branch depth-limited recursion (see that unit's
ExposeScopingPerBranchRecursionIndependence requirement) needs to know, for a specific
matched feature, not just whether it is in scope at all (IsInSubjectScope's combined
answer across every subject) but specifically whether the subject that matched it is
itself recursive — information IsInSubjectScope does not expose, since it deliberately
answers only a single combined in/out-of-scope boolean. Without this narrower predicate,
a per-branch recursion decision could only be approximated by re-deriving from
IsInSubjectScope, which cannot distinguish "matched a recursive subject" from "matched a
non-recursive subject", the exact distinction per-branch recursion depends on.
tests:
- MatchesUnlimitedSubject_MembershipRecursiveSelf_ReturnsTrue
- MatchesUnlimitedSubject_MembershipRecursiveSubtree_ReturnsTrue
- MatchesUnlimitedSubject_NamespaceRecursiveSubtree_ReturnsTrue
- MatchesUnlimitedSubject_MembershipExact_ReturnsFalse
- MatchesUnlimitedSubject_NamespaceDirectChildren_ReturnsFalse
- MatchesUnlimitedSubject_ExplicitMembersOnly_ReturnsFalse
- MatchesUnlimitedSubject_UnrelatedName_ReturnsFalse
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,58 @@ sections:
- InterconnectionView_BuildLayout_ExposeSinglePart_NarrowsToThatPart
- InterconnectionView_BuildLayout_ExposeMultipleParts_UnionsBothSubtrees

- id: SysML2Tools-Core-Layout-Internal-InterconnectionViewLayoutStrategy-ExposeScopingRecursionDepthLimit
title: >-
For a resolved expose scope subject with no unlimited-depth recursion (MembershipExact
or NamespaceDirectChildren), InterconnectionViewLayoutStrategy shall limit interior
expansion of the branch matched by that subject to its own direct part children,
rendering any deeper container part in that branch as an intrinsic-sized leaf box
instead of recursing into its own interior.
justification: |
A non-recursive `expose` form (`expose X;` / `expose X::*;`) asks for only the named
scope's own direct children — not its entire composition subtree. Before this
depth-limiting existed, the resolved scope's filter was applied only at depth 0 and every
included part's interior was then expanded unconditionally regardless of whether the
`expose` statement itself was recursive, silently over-rendering one extra level of nested
parts the user never asked for. This decision is made independently per composition
branch (see the design documentation's "Per-Branch Depth-Limited Recursion" section, and
the ExposeScopingPerBranchRecursionIndependence requirement below for the mixed-subject
case): a feature matching only non-recursive subjects has its own branch depth-limited,
regardless of whether some *other* subject in the same scope is recursive. A branch whose
feature matches at least one recursive subject, or the whole diagram when there is no
scope at all (including the `--auto` view), continues to recurse fully at every depth,
unchanged.
tests:
- InterconnectionView_BuildLayout_ExposeNonRecursiveSubjects_DoesNotRecurseIntoNestedPartsInterior
- InterconnectionView_BuildLayout_ExposeRootOnly_NestedSubsystemInDifferentNamespace_RendersItsOwnUnits
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_ContainerFeature_RendersAsLeaf
- InterconnectionView_BuildLayout_NullViewNode_PicksHeuristicRootUnchanged

- id: SysML2Tools-Core-Layout-Internal-InterconnectionViewLayoutStrategy-PerBranchRecursionIndependence
title: >-
When a resolved expose scope combines a subject with unlimited-depth recursion
(MembershipRecursive or NamespaceRecursive) and a subject with no recursion
(MembershipExact or NamespaceDirectChildren), InterconnectionViewLayoutStrategy shall
decide interior-expansion depth independently for each top-level branch — fully
recursing the branch matched by the recursive subject while depth-limiting the branch
matched only by the non-recursive subject — rather than applying one diagram-wide
decision to every branch.
justification: |
An earlier implementation of the ExposeScopingRecursionDepthLimit requirement computed a
single diagram-wide `unlimitedRecursion` boolean from the resolved scope's `Subjects` as a
whole, so a scope such as `expose SystemDef::**; expose OtherDef;` applied unlimited
recursion to the *entire* diagram, including parts reached only via the non-recursive
`OtherDef` subject — over-collapsing a branch the user explicitly asked to keep
depth-limited. This requirement replaces that diagram-wide simplification with a
per-branch decision (see `ExposeScopeResolver.MatchesUnlimitedSubject` and the design
documentation's "Per-Branch Depth-Limited Recursion" section): each top-level feature's
own matched subject (not the scope's `Subjects` collection as a whole) determines that
feature's own branch recursion depth, and that decision is threaded unchanged to every
descendant of the branch, never re-derived by qualified-name matching past depth 0 (since
composition-path depth and namespace-declaration depth are independent axes in SysML v2).
tests:
- InterconnectionView_BuildLayout_ExposeMixedRecursion_OnlyRecursiveBranchRecurses

- id: SysML2Tools-Core-Layout-Internal-InterconnectionViewLayoutStrategy-NoExposeFallback
title: >-
When a view's ViewContext carries no resolved Expose edge — including a null ViewNode
Expand Down Expand Up @@ -257,8 +309,8 @@ sections:
When no candidate root definition is scope-relevant but the resolved expose scope
directly includes one or more top-level part feature usages, InterconnectionViewLayoutStrategy
shall render those features as boxless nodes side by side instead of an empty canvas,
recursing into each feature's own interior exactly as a normal nested container part
would.
applying the same depth-limited interior expansion (see the
ExposeScopingRecursionDepthLimit requirement) as the normal container-rooted path.
justification: |
Per SysML v2 spec §8.3.26.11 and §9.2.20.2.6, an InterconnectionView's exposed content
need not be a single definition — a scope such as `expose PublishingSubsystem::*;` where
Expand All @@ -269,11 +321,14 @@ sections:
side-by-side placement (reusing the same `LayeredPlacement.PlaceWithPorts` containment
packing every other case already uses) and reusing the same container-vs-leaf recursion
`CollectParts` uses for every other nested part (via the shared `BuildPartItem` helper)
avoids duplicating any layout or recursion logic for this fallback.
avoids duplicating any layout or recursion logic for this fallback — including whether
that recursion is depth-limited: a top-level feature exposed by a purely non-recursive
scope renders as a leaf with no nested interior drawn, exactly like a depth-limited nested
container part would.
tests:
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_NoRootDef_RendersTopLevelFeatureWithoutFrame
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_TwoTopLevelParts_ArrangesSideBySideNoFrame
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_TopLevelFeatureIsContainer_RecursesInterior
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_ContainerFeature_RendersAsLeaf
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_NestedTopLevelFeatureExcluded_NotDuplicated

- id: SysML2Tools-Core-Layout-Internal-InterconnectionViewLayoutStrategy-ScopedTopLevelFeatureFallbackConnections
Expand Down Expand Up @@ -304,3 +359,26 @@ sections:
tests:
- InterconnectionView_BuildLayout_ExposeNamespaceDirectChildren_NoMatchingFeature_ReturnsMinimalCanvas
- InterconnectionView_BuildLayout_EmptyWorkspace_ReturnsMinimalCanvas

- id: SysML2Tools-Core-Layout-Internal-InterconnectionViewLayoutStrategy-TopLevelOwnerScopedConnections
title: >-
When the no-single-root scoped fallback collects top-level parts from two or more
different containing definitions that happen to share a simple part name,
InterconnectionViewLayoutStrategy shall resolve each definition's own connections only
against that definition's own top-level parts, never against a same-named top-level part
collected from a different containing definition.
justification: |
A connection's endpoints are only meaningful relative to its own containing definition's
direct children. Before this fix, `ResolveTopLevelConnections` built one flat
`Dictionary<string, int>` simple-name index from every top-level part collected across
the entire workspace scan; when two different containing definitions each declared a
same-named part (e.g. both `part logger : Logger;`), `TryAdd` silently kept only the
first occurrence, so a connection declared inside the *other* definition resolved against
the wrong definition's part — producing a bogus edge, or silently dropping the intended
one. Grouping collected top-level parts by their own `OwnerQualifiedName` (tracked by
`CollectTopLevelScopedParts` from each matched feature's own containing definition, not
re-derived by simple-name matching) and resolving each definition's own connections only
against its own owner-restricted index eliminates this cross-owner name collision, without
changing the unrelated single-root `BuildPartIndex`/`ResolveConnections` path at all.
tests:
- InterconnectionView_BuildLayout_TopLevelNameCollisionAcrossOwners_ResolvesOwnConnection
22 changes: 22 additions & 0 deletions docs/user_guide/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,28 @@ single `part def` qualifies as "the" root — those feature usages render direct
with no enclosing frame around them. A scope that matches neither a root definition nor any
top-level `part` feature usage still renders the empty diagram described above.

Also specifically for the Interconnection View: when a nested part's own type is itself a
container (a `part def` with its own nested parts), how deep the diagram recurses into that
part's own interior depends on whether that specific part's own branch is matched by a recursive
`expose` subject — decided **independently for each top-level part**, not once for the whole
diagram. A part matched by at least one recursive form (`expose X::**;` or `expose X::*::**;`),
or any part at all when there is no `expose` statement, has its own branch recurse fully — every
nested container's own interior is shown within that branch, at any depth. A part matched **only**
by non-recursive forms (`expose X;` and/or `expose X::*;`) has its own branch limited to that
part's own direct part children: a deeper nested part still renders as its own box within that
branch, but its own interior is not drawn. Because this decision is per-branch, a single view can
mix both outcomes: `expose SystemA::**; expose SystemB;` fully recurses into `SystemA`'s own
composed structure while stopping `SystemB`'s branch at its own direct children, in the very same
diagram — a sibling part matched by a different subject is never affected by another part's own
recursion kind. For example, `expose System; expose System::*;` shows `System`'s direct part
children as boxes, but does not expand into any of those parts' own nested structure, even if
their types have one — whereas `expose System::**;` shows every level.

When the no-single-root scoped fallback (above) collects top-level parts from more than one
containing definition, a connection declared inside one of those definitions always resolves
against that definition's own top-level parts only — even if a different exposed definition
happens to declare a same-named part.

Named `view Name { ... }` usages (not just `view def` declarations) are also now recognized as
their own renderable declarations: a workspace containing both `view def` declarations and named
`view` usages surfaces both kinds as views the `render` command discovers and renders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
##### Verification Approach

`ExposeScopeResolver` is verified through direct unit tests in `ExposeScopeResolverTests` that
call `ResolveExposedScope`, `IsInSubjectScope`, `IsRootRelevantToScope`, and
`IsMoreSpecificCandidate` directly with synthetic `SysmlWorkspace`/`SysmlViewNode` inputs and
call `ResolveExposedScope`, `IsInSubjectScope`, `MatchesUnlimitedSubject`, `IsRootRelevantToScope`,
and `IsMoreSpecificCandidate` directly with synthetic `SysmlWorkspace`/`SysmlViewNode` inputs and
assert on the returned `ExposedScope` (Phase 2a: `Subjects`/`ExplicitMembers`/`Failures`,
replacing the earlier flat qualified-name-list shape) or boolean result. No mocking is required;
every method is a pure function over its parameters.
Expand Down Expand Up @@ -62,6 +62,15 @@ configuration are required beyond a standard .NET SDK installation.
- (Phase 2a) `IsInSubjectScope` returns `true` for an exact qualified-name match against an
`ExplicitMembers` entry, and `false` for one of that entry's own descendants — an
`ExplicitMembers` match is exact-only, not a subtree match.
- `MatchesUnlimitedSubject` returns `true` for a qualified name matching a `Subjects` entry (exact
or subtree) whose own recursion kind is `MembershipRecursive`, and likewise for a subtree match
against a `NamespaceRecursive` entry.
- `MatchesUnlimitedSubject` returns `false` for a qualified name matching a `Subjects` entry whose
own recursion kind is `MembershipExact` or `NamespaceDirectChildren` — matching a non-recursive
subject never signals unlimited recursion, even though the qualified name is otherwise in scope.
- `MatchesUnlimitedSubject` returns `false` for a qualified name that matches only an
`ExplicitMembers` entry (never via explicit members, regardless of any subject's recursion kind
elsewhere in the same scope) and for an unrelated qualified name.
- `IsRootRelevantToScope` returns `true` when the candidate equals a `Subjects` entry.
- `IsRootRelevantToScope` returns `true` when the candidate is nested within a `Subjects`
entry.
Expand Down Expand Up @@ -141,6 +150,21 @@ configuration are required beyond a standard .NET SDK installation.
Exact qualified-name match against an `ExplicitMembers` entry is in scope
- `IsInSubjectScope_ExplicitMemberDescendant_ReturnsFalse`:
A descendant of an `ExplicitMembers` entry is not automatically in scope (exact match only)
- `MatchesUnlimitedSubject_MembershipRecursiveSelf_ReturnsTrue`:
Exact match against a `MembershipRecursive` subject matches the unlimited-recursion predicate
- `MatchesUnlimitedSubject_MembershipRecursiveSubtree_ReturnsTrue`:
Subtree match against a `MembershipRecursive` subject matches the unlimited-recursion predicate
- `MatchesUnlimitedSubject_NamespaceRecursiveSubtree_ReturnsTrue`:
Subtree match against a `NamespaceRecursive` subject matches the unlimited-recursion predicate
- `MatchesUnlimitedSubject_MembershipExact_ReturnsFalse`:
Match against a `MembershipExact` subject does not match the unlimited-recursion predicate
- `MatchesUnlimitedSubject_NamespaceDirectChildren_ReturnsFalse`:
Match against a `NamespaceDirectChildren` subject does not match the unlimited-recursion
predicate
- `MatchesUnlimitedSubject_ExplicitMembersOnly_ReturnsFalse`:
A match found only via `ExplicitMembers` never matches the unlimited-recursion predicate
- `MatchesUnlimitedSubject_UnrelatedName_ReturnsFalse`:
An unrelated qualified name does not match the unlimited-recursion predicate
- `IsRootRelevantToScope_CandidateEqualsSubject_ReturnsTrue`:
Candidate equal to a `Subjects` entry is relevant
- `IsRootRelevantToScope_CandidateNestedInSubject_ReturnsTrue`:
Expand Down
Loading
Loading