From 16f5b32b21e268ed2d8e54707595a181abb51cf0 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 17:05:02 -0400 Subject: [PATCH 01/14] Fix expose recursion semantics and bump DemaConsulting.Rendering to 0.1.0-beta.7 - Add ExposeRecursionKind (MembershipExact, MembershipRecursive, NamespaceDirectChildren, NamespaceRecursive) to ExposeMember, correctly derived from the ::** grammar token for MembershipExpose and NamespaceExpose forms in AstBuilder (previously discarded/miscalculated). - Redesign ExposeScopeResolver subject matching to honor exact-match, direct-children-only, and prefix/recursive semantics per recursion kind instead of always treating expose targets as fully recursive subtrees. - Update GeneralViewLayoutStrategy and DynamicViewSynthesizer consumers, and ~44 test call sites, plus new ExposeScopeResolverTests coverage. - Update design/reqstream/verification docs and user guide to reflect the corrected behavior. - Bump DemaConsulting.Rendering.Layout/Svg/Skia to 0.1.0-beta.7 and pin SkiaSharp.NativeAssets to 4.150.0 to resolve a transitive version floor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 7 +- .../filtering/filter-expression-evaluator.md | 2 +- .../layout/internal/expose-scope-resolver.md | 102 ++++--- .../internal/dynamic-view-synthesizer.md | 11 +- .../semantic/model/ast-builder.md | 49 +++- .../semantic/model/sysml-node.md | 45 +-- .../internal/expose-scope-resolver.yaml | 60 +++- .../internal/dynamic-view-synthesizer.yaml | 18 +- .../semantic/model/ast-builder.yaml | 34 ++- .../semantic/model/sysml-node.yaml | 16 +- docs/user_guide/introduction.md | 105 ++++--- .../layout/internal/expose-scope-resolver.md | 77 ++++-- .../semantic/model/ast-builder.md | 20 +- .../semantic/model/sysml-node.md | 17 +- .../DemaConsulting.SysML2Tools.Core.csproj | 2 +- .../Layout/Internal/ExposeScopeResolver.cs | 120 ++++++-- .../Internal/DynamicViewSynthesizer.cs | 16 +- .../Semantic/Model/AstBuilder.cs | 39 ++- .../Semantic/Model/SysmlNode.cs | 51 +++- .../DemaConsulting.SysML2Tools.Tool.csproj | 4 +- .../DemaConsulting.SysML2Tools.Tests.csproj | 10 +- .../ActionFlowViewLayoutStrategyTests.cs | 12 +- .../BrowserAndGridViewLayoutStrategyTests.cs | 14 +- .../Layout/ExposeScopeResolverTests.cs | 259 ++++++++++++++++-- .../Layout/GeneralViewLayoutStrategyTests.cs | 4 +- .../InterconnectionViewLayoutStrategyTests.cs | 16 +- .../Layout/SequenceViewLayoutStrategyTests.cs | 14 +- .../StateTransitionViewLayoutStrategyTests.cs | 12 +- .../Rendering/DynamicViewSynthesizerTests.cs | 1 + .../Semantic/AstBuilderMetadataTests.cs | 164 +++++++++++ .../Semantic/WorkspaceLoaderTests.cs | 1 + 31 files changed, 1017 insertions(+), 285 deletions(-) diff --git a/README.md b/README.md index 8ce5c06..3ab5e09 100644 --- a/README.md +++ b/README.md @@ -289,8 +289,11 @@ sysml2tools help [lint|render|query []|export] | `--view ` names a view that does not exist | Error: lists available view names and exits non-zero | For every layout strategy (not just the General View), a view's body `expose <...>;` statements -now scope the rendered diagram to the union of the exposed names' containment subtrees, instead -of always rendering the full workspace; a view with no `expose` statement continues to render +now scope the rendered diagram to the union of the exposed entries' resolved scopes — an exact +match, direct children only, or the entire containment subtree, depending on each entry's SysML +v2 `expose` grammar form and recursion setting (`expose X;`/`expose X::**;`/`expose +X::*;`/`expose X::*::**;`) — instead of always rendering the full workspace; a view with no +`expose` statement continues to render the full workspace. `render asTreeDiagram;` and `render asInterconnectionDiagram;` now select the Browser View and Interconnection View layout strategies respectively, taking precedence over the name/supertype heuristic; other `render` targets (`asElementTable`, `asTextualNotation`, an diff --git a/docs/design/sysml2-tools-core/filtering/filter-expression-evaluator.md b/docs/design/sysml2-tools-core/filtering/filter-expression-evaluator.md index 78bdc7a..c9343ec 100644 --- a/docs/design/sysml2-tools-core/filtering/filter-expression-evaluator.md +++ b/docs/design/sysml2-tools-core/filtering/filter-expression-evaluator.md @@ -234,7 +234,7 @@ lexer/recursive-descent-parser internals could violate that contract despite `Pa `BracketFilterExpressionText` with `FilterExpressionParser.Parse`, evaluates successful ASTs with `FilterExpressionEvaluator.Evaluate` against that entry's own containment-subtree candidate set, adds matches to the resolved `ExposedScope.ExplicitMembers`, and falls back to - whole-subtree inclusion (`PrefixSubjects`) plus a recorded `BracketFilterFailure` on parse or + whole-subtree inclusion (`Subjects`) plus a recorded `BracketFilterFailure` on parse or evaluation failure. No change was required in this unit to support this second caller. - `FilterExpressionParserTests` and `FilterExpressionEvaluatorTests` exercise the parser, pretty-printer, and evaluator directly. diff --git a/docs/design/sysml2-tools-core/layout/internal/expose-scope-resolver.md b/docs/design/sysml2-tools-core/layout/internal/expose-scope-resolver.md index 50990fe..c150715 100644 --- a/docs/design/sysml2-tools-core/layout/internal/expose-scope-resolver.md +++ b/docs/design/sysml2-tools-core/layout/internal/expose-scope-resolver.md @@ -12,17 +12,21 @@ each maintaining its own copy, so every view kind honors `expose` scoping identi ##### Data Model `ExposeScopeResolver` is a static class with no instance state; input arrives through method -parameters, and (Phase 2a) resolution output is returned as two internal record types: - -- `ExposedScope(IReadOnlyList PrefixSubjects, IReadOnlyList ExplicitMembers)` — - the resolved scope. `PrefixSubjects` are exposed subject qualified names whose entire - containment subtree is in scope (the pre-Phase-2a whole-subtree behavior, used for `expose` - entries with no bracket filter and as the fallback for a bracket-filtered entry that failed to - parse or evaluate). `ExplicitMembers` are individual qualified names — of a `SysmlDefinitionNode` - **or a named `SysmlFeatureNode`** (Phase 2d — widened from definitions-only, see below) — - matched by a successfully-evaluated bracket filter — exact matches only, not their own nested - members unless those also match. A settable `Failures` init-property (default empty) carries any - bracket filters that failed to parse or evaluate. +parameters, and resolution output is returned as three internal record types: + +- `ExposedScope(IReadOnlyList Subjects, IReadOnlyList ExplicitMembers)` — + the resolved scope. `Subjects` are exposed subject qualified names, each paired with the + `ExposeRecursionKind` that governs how it matches a candidate qualified name (see + `IsInSubjectScope` below) — derived from each `expose` entry's own SysML v2 grammar form + (MembershipExpose/NamespaceExpose) and recursion setting (a trailing `::**`), and forced to + whichever *Recursive variant matches the entry's form for the fallback path (a bracket-filtered + entry that failed to parse or evaluate). `ExplicitMembers` are individual qualified names — of a + `SysmlDefinitionNode` **or a named `SysmlFeatureNode`** (Phase 2d — widened from + definitions-only, see below) — matched by a successfully-evaluated bracket filter — exact + matches only, not their own nested members unless those also match. A settable `Failures` + init-property (default empty) carries any bracket filters that failed to parse or evaluate. +- `ExposeSubject(string QualifiedName, ExposeRecursionKind Recursion)` — one resolved `expose` + subject qualified name paired with the recursion kind governing how it matches candidates. - `BracketFilterFailure(string ExpressionText, string? Reason)` — one failed bracket-filter expression's raw source text plus a short human-readable reason, feeding `LayoutWarnings.ForUnevaluatedExposeBracketFilter`. @@ -31,19 +35,19 @@ parameters, and (Phase 2a) resolution output is returned as two internal record ###### `ResolveExposedScope(SysmlWorkspace workspace, SysmlViewNode? viewNode)` -Resolves the qualified-name containment-subtree scope a view's `expose` statements restrict the -diagram to — the only content-scoping mechanism a view has (`render ;` names a -rendering style/format, e.g. `asTreeDiagram`/`asElementTable`, per the SysML v2 grammar, never -content, so `RenderTargetName` never affects this decision). Returns `null` — meaning "render -everything", the pre-scoping behavior — whenever the view has no resolved `Expose`-kind -`ResolvedEdges` entries: covering a `null` `viewNode` (the `--auto` synthesized view, which never -carries expose/render/filter data), a view with no `expose` statement, and a view whose every -`expose` entry failed to resolve, uniformly. Otherwise, for each resolved `Expose` edge, re-pairs -the edge's target with the `ExposeMember` it originated from (see Design below) and: - -- when that entry carries no bracket-filter expression, adds the target (and, for a usage target, - its resolved type — see the usage-to-type fallback below) to `PrefixSubjects`, unchanged from - Phase 1; +Resolves the qualified-name scope a view's `expose` statements restrict the diagram to — the only +content-scoping mechanism a view has (`render ;` names a rendering style/format, e.g. +`asTreeDiagram`/`asElementTable`, per the SysML v2 grammar, never content, so `RenderTargetName` +never affects this decision). Returns `null` — meaning "render everything", the pre-scoping +behavior — whenever the view has no resolved `Expose`-kind `ResolvedEdges` entries: covering a +`null` `viewNode` (the `--auto` synthesized view, which never carries expose/render/filter data), a +view with no `expose` statement, and a view whose every `expose` entry failed to resolve, +uniformly. Otherwise, for each resolved `Expose` edge, re-pairs the edge's target with the +`ExposeMember` it originated from (see Design below) and: + +- when that entry carries no bracket-filter expression, calls `AddSubject` with the entry's own + `ExposeRecursionKind` — adding the target (and, for a usage target, its resolved type — see the + usage-to-type fallback below) as an `ExposeSubject` classified by that kind; - when that entry's bracket-filter expression parses and evaluates successfully (Phase 2a), computes the candidate set as every `workspace.Declarations` key that is the target itself or lies in its containment subtree (`qn == target || qn.StartsWith(target + "::")`) *and* is a @@ -52,27 +56,39 @@ the edge's target with the `ExposeMember` it originated from (see Design below) usage-level candidate too) — mirroring `GeneralViewLayoutStrategy.CollectDefinitions`'s candidate-set restriction — evaluates with `FilterExpressionEvaluator.Evaluate` against that candidate set unchanged, and adds the matched subset to `ExplicitMembers`; -- when that entry's bracket-filter expression fails to parse or evaluate, falls back to - whole-subtree inclusion (`PrefixSubjects`, same as the unfiltered case) and records a +- when that entry's bracket-filter expression fails to parse or evaluate, falls back to calling + `AddSubject` with whichever *Recursive variant matches the entry's form (never the narrower + Exact/DirectChildren kind, regardless of the entry's own classification) and records a `BracketFilterFailure` with the raw expression text and a short reason, so the caller can degrade gracefully instead of silently losing the exposed path's content. When an exposed target resolves to a `SysmlFeatureNode` (a usage, e.g. `part myVehicle : Vehicle;`) rather than a `SysmlDefinitionNode`, the usage's own containment subtree is typically empty — the real content lives under its type's subtree. To avoid silently -scoping to nothing, whole-subtree inclusion also resolves the usage's own `Typing`-kind -`ResolvedEdges` entry (if any) and adds that type's qualified name to `PrefixSubjects` too, so both -the usage and its type's subtree are included. This expansion only applies to whole-subtree -inclusion — a successfully-evaluated bracket filter's `ExplicitMembers` already name the exact -matched definitions/usages directly. +scoping to nothing, `AddSubject` also resolves the usage's own `Typing`-kind `ResolvedEdges` entry +(if any) and adds that type's qualified name as an `ExposeSubject`, **using the same recursion +kind** as the usage's own subject — so a non-recursive (`MembershipExact`) usage expose adds the +type exact-match only, not its subtree, while a recursive usage expose adds the type with +whole-subtree matching, same as before this fix. This expansion only applies to `Subjects` — a +successfully-evaluated bracket filter's `ExplicitMembers` already name the exact matched +definitions/usages directly. ###### `IsInSubjectScope(string qualifiedName, ExposedScope scope)` -Returns `true` when `qualifiedName` equals one of `scope.PrefixSubjects` or lies within one of -their containment subtrees (a `"{subject}::"` prefix match, the same qualified-name-prefix idiom -`StdlibFilter.IsStdlibElement` already uses for stdlib-prefix matching), 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. +Returns `true` when `qualifiedName` matches one of `scope.Subjects` per that subject's own +`ExposeRecursionKind`: + +- `MembershipRecursive`/`NamespaceRecursive` — `qualifiedName` equals the subject or lies within + its containment subtree (a `"{subject}::"` prefix match, the same qualified-name-prefix idiom + `StdlibFilter.IsStdlibElement` already uses for stdlib-prefix matching) — the unchanged + whole-subtree behavior from before this fix. +- `MembershipExact` — `qualifiedName` equals the subject exactly, and nothing else. +- `NamespaceDirectChildren` — `qualifiedName` is a direct (one-level) child of the subject only, + via a private `IsDirectChildOf` helper (`StartsWith(subject + "::")` **and** the remainder + contains no further `"::"`) — not the subject itself, not a deeper descendant. + +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. ###### `IsRootRelevantToScope(string candidateQualifiedName, ExposedScope scope)` @@ -80,11 +96,15 @@ Returns `true` when `candidateQualifiedName` (a candidate single-root diagram ro definition `InterconnectionViewLayoutStrategy`, `StateTransitionViewLayoutStrategy`, `ActionFlowViewLayoutStrategy`, or `SequenceViewLayoutStrategy` would otherwise pick by its own heuristic) is related to the resolved `expose` scope in `scope`, in either containment -direction — checked against both `scope.PrefixSubjects` and `scope.ExplicitMembers`: the candidate -itself is an exposed subject or matched member, the candidate lies within an exposed subject's -containment subtree, or an exposed subject/matched member lies within the candidate's own -containment subtree (the common "expose an inner state/action/part/lifeline of the root" case). -This method identifies the *set* of scope-relevant candidates only — because SysML v2 definitions +direction — checked against both `scope.Subjects.Select(s => s.QualifiedName)` and +`scope.ExplicitMembers`: the candidate itself is an exposed subject or matched member, the +candidate lies within an exposed subject's containment subtree, or an exposed subject/matched +member lies within the candidate's own containment subtree (the common "expose an inner +state/action/part/lifeline of the root" case). This anchor-qualified-name containment check is +unaffected by each subject's `ExposeRecursionKind` — root-candidate relevance only needs to find +the right root to render, independent of whether the final in-scope set (decided separately by +`IsInSubjectScope`) is the full subtree, a single exact member, or one level of children. This +method identifies the *set* of scope-relevant candidates only — because SysML v2 definitions may nest, an ancestor and one of its nested descendant definitions can both be relevant to the same resolved scope. Disambiguating among multiple relevant candidates is delegated to `IsMoreSpecificCandidate`, not decided by this method. diff --git a/docs/design/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.md b/docs/design/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.md index 5e61ac0..64491fc 100644 --- a/docs/design/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.md +++ b/docs/design/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.md @@ -70,13 +70,16 @@ Performs four steps, in order, short-circuiting to a diagnostic on the first fai the rare case where it is already present (e.g., a repeat `Synthesize` call against the same workspace instance for the same target). - `RenderTargetName` set to the resolved token from step 1. - - A single-entry `ExposeMembers` list (`[new ExposeMember(targetQualifiedName, null)]`) and + - A single-entry `ExposeMembers` list (`[new ExposeMember(targetQualifiedName, null, + ExposeRecursionKind.MembershipRecursive)]`) and matching single-entry `ResolvedEdges` list (`[new SysmlEdge(viewQualifiedName, targetQualifiedName, SysmlEdgeKind.Expose)]`) — the same two properties a real, parsed - `view def V { expose Target; }` produces via `ReferenceResolver`, so + `view def V { expose Target::**; }` produces via `ReferenceResolver`, so `ExposeScopeResolver.ResolveExposedScope` (which reads only these two properties, with no - notion of provenance) scopes the rendered diagram to exactly the requested target. This is - the key difference from `DiagramRenderer.SynthesizeAutoView`, whose synthesized node carries + notion of provenance) scopes the rendered diagram to the requested target's whole + containment subtree (matching the pre-fix behavior for dynamic/ad-hoc views, which are + intended to show the full context around a requested element rather than the element + alone). This is the key difference from `DiagramRenderer.SynthesizeAutoView`, whose synthesized node carries no `ResolvedEdges` at all — that absence is what makes `ExposeScopeResolver` return a `null` scope (render everything); a dynamic view instead always resolves to a definite, non-null scope. diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index c94341c..a5fd937 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -213,29 +213,52 @@ whitespace so the Filtering subsystem can re-lex it faithfully rather than recei `ExtractExposedNames(IEnumerable bodyItems)` collects one `ExposeMember` per `expose ;` member in source order, reusing the shared `ExtractImportTarget` helper (see below) against each `expose` member's wrapped `namespaceImport()`/`membershipImport()` — the -identical grammar shape `import` uses. When an `expose` member uses the dominant corpus form -`qualifiedName::**[]`, the same helper also returns the bracketed filter expression's -original source text, which `ExtractExposedNames` pairs together with that same entry's qualified -name into a single `ExposeMember(QualifiedName, BracketFilterExpressionText)` record — rather than +identical grammar shape `import` uses. It captures `isNamespaceForm = expose.namespaceExpose() is +not null` (which grammar alternative — `namespaceExpose`/`membershipExpose` — matched) *before* +calling `ExtractImportTarget`, since the call site already knows this from its own branch, and +takes the helper's 4th tuple element (`isRecursive`) to classify the entry's `ExposeRecursionKind` +via the four-way truth table: bracket-filtered entries (`hasBracketFilter`) always map to whichever +*Recursive variant matches `isNamespaceForm`; non-bracket-filtered entries map +`(isNamespaceForm, isRecursive)` to `MembershipExact`/`MembershipRecursive`/ +`NamespaceDirectChildren`/`NamespaceRecursive` directly. When an `expose` member uses the dominant +corpus form `qualifiedName::**[]`, the same helper also returns the bracketed filter +expression's original source text, which `ExtractExposedNames` pairs together with that same +entry's qualified name and recursion kind into a single +`ExposeMember(QualifiedName, BracketFilterExpressionText, RecursionKind)` record — rather than appending the qualified name and the bracket-filter text to two separate, unpaired flattened lists (the earlier Phase 1 shape) — so a view with more than one `expose` member never loses track of which bracket filter belongs to which exposed path. `ExposeScopeResolver` (Phase 2a) depends on -this pairing to evaluate each entry's bracket filter against that entry's own containment subtree. +this pairing to evaluate each entry's bracket filter against that entry's own containment subtree, +and (per this fix) reads each entry's `RecursionKind` to decide between exact/direct-children/ +whole-subtree matching. `ExtractImportTarget(NamespaceImportContext?, MembershipImportContext?)` is a shared helper -extracted from `VisitImportRule`'s previously inline logic, returning the extracted -qualified/dotted name text and whether the reference is a wildcard, for either the -namespace-import form (`qualifiedName::*`, always a wildcard), the membership-import form -(`qualifiedName`, optionally `::**`), or the bracketed-filter form nested inside a -namespace-import (`qualifiedName::**[]`) — the dominant `expose` shape in the real -OMG corpus (e.g. `expose vehicle::**[@Safety];`). The grammar nests the qualified name two levels -deeper for that third form: `namespaceImport -> filterPackage -> filterPackageImportDeclaration -> +extracted from `VisitImportRule`'s previously inline logic, returning a 4-tuple: the extracted +qualified/dotted name text, whether the reference is a wildcard (`IsWildcard`, unchanged — still +consumed verbatim by `VisitImportRule` for plain `import`, whose behavior this fix does not +alter), the bracket-filter expression text (if any), and (added by this fix) `IsRecursive` — +whether the entry is recursive, derived from a trailing `::**` per branch: for the direct +namespace-import form (`qualifiedName::*`), `namespaceImport.STAR_STAR()` (previously ignored — +the pre-fix bug hard-coded this branch's recursion bit away); for either bracketed-filter +sub-form, always `true` (bracket-filtered forms are treated as always recursive by design, since +the grammar only reaches the bracket-filter alternative via `::**[filterExpr]` — the +recursive/wildcard token always precedes the bracket); for the membership-import form +(`qualifiedName` optionally `::**`), the same `STAR_STAR()` check `IsWildcard` already computed +for that branch (already correct, just +previously unexposed as its own named field). This covers either the namespace-import form +(`qualifiedName::*`, always a wildcard), the membership-import form (`qualifiedName`, optionally +`::**`), or the bracketed-filter form nested inside a namespace-import +(`qualifiedName::**[]`) — the dominant `expose` shape in the real OMG corpus (e.g. +`expose vehicle::**[@Safety];`). The grammar nests the qualified name two levels deeper for that +third form: `namespaceImport -> filterPackage -> filterPackageImportDeclaration -> (membershipImport | namespaceImportDirect)`. `ExtractImportTarget` descends into `filterPackage().filterPackageImportDeclaration()` and extracts the qualified name from whichever of `membershipImport()`/`namespaceImportDirect()` is present there, rather than only checking the direct `qualifiedName()` child (which is null for this alternative). `VisitImportRule` and `ExtractExposedNames` both call this one helper rather than duplicating the extraction logic, per -the Copy-Paste Programming anti-pattern guidance in coding-principles.md. +the Copy-Paste Programming anti-pattern guidance in coding-principles.md; `VisitImportRule` +ignores the 4th (`IsRecursive`) element via a discard, since plain `import` has no analogous +recursion-kind concept and its behavior is unchanged by this fix. `VisitRequirementUsage` and `VisitConcernUsage` push a namespace scope (when named) and call `CollectChildren(body?.requirementBodyItem() ?? [])` in addition to their existing name/qualified- diff --git a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md index a2cfcbd..ba2c909 100644 --- a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md @@ -147,24 +147,33 @@ There are no behavioral methods beyond the inherited `object` members. `SysmlImp rendered scope. Reserved for a possible future capability that selects among rendering-style strategies — see the project ROADMAP. - `ExposeMembers` — each `expose [::**[]];` member in a `view` usage's body, in - source order, paired as an `ExposeMember(string QualifiedName, string? BracketFilterExpressionText)` - record — the qualified-name reference text plus that specific entry's own bracket-filter - expression text, or null when the entry carries none. Empty when no `expose` members are - present (and always empty for a `view def` definition — `expose` is only valid grammar inside a - `view` usage's body). Extracted by `AstBuilder.ExtractExposedNames`, sharing the same - `ExtractImportTarget` helper `VisitImportRule` uses for plain `import`. `GetExposedNames()` - projects each entry's `QualifiedName` as a computed convenience list (a method rather than a - property, since a property returning a freshly-projected collection trips SonarAnalyzer S2365). - Each entry's `QualifiedName` is independently resolved by `ReferenceResolver` into a - `SysmlEdgeKind.Expose` edge, or an unresolved-reference diagnostic (and no edge) for that - entry. `GeneralViewLayoutStrategy` (via the shared `ExposeScopeResolver`) uses this to scope a - rendered diagram, and (Phase 2a) re-pairs each resolved edge back to its originating - `ExposeMember` to evaluate that entry's own `BracketFilterExpressionText`, if any, against that - entry's own containment subtree. Phase 1 originally captured each entry's bracket-filter text on - a separate, unpaired, flattened `ExposeBracketFilterTexts` list alongside an equally flattened - `ExposedNames` list, making it impossible to tell which exposed path a given bracket filter - belonged to when a view declared more than one `expose` member; `ExposeMembers` fixes this - defect by pairing the two together at capture time. + source order, paired as an + `ExposeMember(string QualifiedName, string? BracketFilterExpressionText, ExposeRecursionKind RecursionKind)` + record — the qualified-name reference text, that specific entry's own bracket-filter expression + text (or null when the entry carries none), and an `ExposeRecursionKind` classifying which SysML + v2 `expose` grammar form (MembershipExpose/NamespaceExpose) and recursion setting (a trailing + `::**`) produced the entry: `MembershipExact` (`expose X;`), `MembershipRecursive` + (`expose X::**;`), `NamespaceDirectChildren` (`expose X::*;`), or `NamespaceRecursive` + (`expose X::*::**;`) — a bracket-filtered entry is always classified as one of the two + *Recursive variants (matching its form), since `ExposeScopeResolver` only consults this + classification for its unfiltered/fallback whole-subtree-vs-narrow behavior, never for a + successfully-evaluated bracket filter's own already-exact `ExplicitMembers`. Empty when no + `expose` members are present (and always empty for a `view def` definition — `expose` is only + valid grammar inside a `view` usage's body). Extracted by `AstBuilder.ExtractExposedNames`, + sharing the same `ExtractImportTarget` helper `VisitImportRule` uses for plain `import`. + `GetExposedNames()` projects each entry's `QualifiedName` as a computed convenience list (a + method rather than a property, since a property returning a freshly-projected collection trips + SonarAnalyzer S2365). Each entry's `QualifiedName` is independently resolved by + `ReferenceResolver` into a `SysmlEdgeKind.Expose` edge, or an unresolved-reference diagnostic + (and no edge) for that entry. `GeneralViewLayoutStrategy` (via the shared `ExposeScopeResolver`) + uses this to scope a rendered diagram, and (Phase 2a) re-pairs each resolved edge back to its + originating `ExposeMember` to evaluate that entry's own `BracketFilterExpressionText`, if any, + against that entry's own containment subtree, and to read its `RecursionKind` to decide between + exact/direct-children/whole-subtree matching. Phase 1 originally captured each entry's + bracket-filter text on a separate, unpaired, flattened `ExposeBracketFilterTexts` list alongside + an equally flattened `ExposedNames` list, making it impossible to tell which exposed path a given + bracket filter belonged to when a view declared more than one `expose` member; `ExposeMembers` + fixes this defect by pairing the two (and now, the recursion kind) together at capture time. - `FilterExpressionText` — the raw source text of the view's `filter [];` member's bracketed expression, or null when absent. Captured verbatim by `AstBuilder` (using the original token spacing, not `RuleContext.GetText()`'s whitespace-stripped form) and never diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml index 1853b60..325a7d9 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml @@ -52,18 +52,70 @@ sections: - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-SubjectScopeMembership title: >- ExposeScopeResolver.IsInSubjectScope shall return true for a qualified name that - exactly matches a subject or lies within a subject's containment subtree (a - "{subject}::" prefix match), and false otherwise. + matches a subject per that subject's own ExposeRecursionKind — an exact match for + MembershipExact, a direct (one-level) child match for NamespaceDirectChildren, or an + exact match or containment-subtree match (a "{subject}::" prefix match) for + MembershipRecursive/NamespaceRecursive — and false otherwise. justification: | Every strategy needs a single, consistent rule for "is this element within the exposed - scope" that treats an exposed target's entire containment subtree — not just the target - itself — as included, matching how SysML v2 namespaces nest qualified names. + scope" that honors each `expose` entry's own SysML v2 grammar form and recursion + setting (`expose X;`/`expose X::**;`/`expose X::*;`/`expose X::*::**;`), rather than + unconditionally treating an exposed target's entire containment subtree as included + regardless of whether the entry actually requested recursion. tests: - IsInSubjectScope_ExactMatch_ReturnsTrue - IsInSubjectScope_SubtreeMatch_ReturnsTrue - IsInSubjectScope_PrefixWithoutSeparator_ReturnsFalse - IsInSubjectScope_UnrelatedName_ReturnsFalse + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-MembershipExactNarrowScope + title: >- + ExposeScopeResolver.ResolveExposedScope shall resolve a non-recursive MembershipExpose + entry (expose X;) to a scope containing only X itself (and, for a usage target, its + resolved type, itself only) — not X's containment subtree. + justification: | + Per formal-26-03-02.md §8.3.26.2-4, a bare `expose X;` (MembershipExpose with no + trailing `::**`) is non-recursive: only X is exposed, not its nested members. Treating a + bare `expose X;` as whole-subtree inclusion (the pre-fix behavior) silently over-scoped + every non-recursive membership expose to its entire subtree, which is a correctness bug, + not a rendering nicety. + tests: + - ResolveExposedScope_MembershipExact_ScopeIsExactOnly + - ResolveExposedScope_MembershipExactUsage_TypeFallbackIsAlsoExactOnly + + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-NamespaceDirectChildrenNarrowScope + title: >- + ExposeScopeResolver.ResolveExposedScope shall resolve a non-recursive NamespaceExpose + entry (expose X::*;) to a scope containing only X's direct (one-level) children — not X + itself and not deeper descendants. + justification: | + Per formal-26-03-02.md §8.3.26.2-4, a non-recursive `expose X::*;` (NamespaceExpose with + no trailing `::**`) exposes only X's direct members, not X itself and not their own + nested members. Treating this form as whole-subtree inclusion (the pre-fix behavior) was + incorrect on two counts: it wrongly included X itself, and it wrongly included + grandchildren and deeper descendants. + tests: + - ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnly + + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-RecursiveWholeSubtreeUnchanged + title: >- + ExposeScopeResolver.ResolveExposedScope shall continue to resolve a recursive + MembershipExpose entry (expose X::**;) or recursive NamespaceExpose entry + (expose X::*::**;) to a scope containing X and its entire containment subtree, and a + bracket-filtered entry that fails to parse or evaluate shall continue to fall back to + this same whole-subtree behavior regardless of the entry's own recursion kind. + justification: | + Recursive `expose` forms (`::**`) are the one case where the pre-fix whole-subtree + behavior was already correct; this requirement is a regression guard confirming the fix + for the non-recursive forms above did not also narrow the recursive forms or the + bracket-filter-failure fallback, which must still degrade to the full subtree rather + than the narrower Exact/DirectChildren behavior (never silently losing content on a + bracket-filter failure). + tests: + - ResolveExposedScope_MembershipRecursive_ScopeIsWholeSubtree + - ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree + - ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-RootRelevance title: >- ExposeScopeResolver.IsRootRelevantToScope shall return true for a candidate root diff --git a/docs/reqstream/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.yaml b/docs/reqstream/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.yaml index 9362030..5e9a9e1 100644 --- a/docs/reqstream/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.yaml +++ b/docs/reqstream/sysml2-tools-core/rendering/internal/dynamic-view-synthesizer.yaml @@ -76,15 +76,17 @@ sections: title: >- DynamicViewSynthesizer shall construct a synthesized view node with a reserved, collision-checked qualified name, a RenderTargetName set to the resolved token, a - single Expose edge and ExposeMembers entry scoping the view to exactly the requested - target, and a FilterExpressionText passed through unchanged from --filter, reporting a - diagnostic instead of silently overwriting an existing declaration on the rare name - collision. + single Expose edge and ExposeMembers entry (using ExposeRecursionKind.MembershipRecursive) + scoping the view to the requested target's whole containment subtree, and a + FilterExpressionText passed through unchanged from --filter, reporting a diagnostic + instead of silently overwriting an existing declaration on the rare name collision. justification: | - Scoping the synthesized view to exactly its target (rather than the whole workspace, - as SynthesizeAutoView does) is the key behavior that makes a dynamic view usable for a - large model. The reserved, collision-checked name prevents any possibility of - corrupting an existing declaration. + Scoping the synthesized view to the target's containment subtree (rather than the + whole workspace, as SynthesizeAutoView does) is the key behavior that makes a dynamic + view usable for a large model — showing the requested element together with its full + nested context, matching the intended "inspect this element and everything under it" + use case. The reserved, collision-checked name prevents any possibility of corrupting + an existing declaration. tests: - Synthesize_FilterExpression_PassedThroughUnchanged - Synthesize_NoFilterExpression_ResultsInNullFilterExpressionText diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml index 2dc58f5..b7ba674 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -84,9 +84,10 @@ sections: - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewExposedNames title: >- AstBuilder shall capture each `expose [::**[]];` member (`view` usages - only, in source order) as a paired `ExposeMember` — qualified-name reference text plus - its own bracket-filter expression text (or null when absent) — on the corresponding - SysmlViewNode's ExposeMembers property. + only, in source order) as a paired `ExposeMember` — qualified-name reference text, + its own bracket-filter expression text (or null when absent), and its + `ExposeRecursionKind` classification — on the corresponding SysmlViewNode's + ExposeMembers property. justification: | Capturing this data during AST construction is the prerequisite for ReferenceResolver to resolve each entry's qualified name into edges that GeneralViewLayoutStrategy uses @@ -95,7 +96,9 @@ sections: text (rather than the earlier Phase 1 flattened, unpaired `ExposedNames`/ `ExposeBracketFilterTexts` lists) is the Phase 2a prerequisite for `ExposeScopeResolver` to evaluate a bracket filter against the specific path it was - declared on, when a view declares more than one `expose` member. + declared on, when a view declares more than one `expose` member; pairing each entry's + recursion kind alongside it is the prerequisite for `ExposeScopeResolver` to correctly + distinguish exact/direct-children/whole-subtree matching per entry. tests: - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge - WorkspaceLoader_LoadAsync_ViewUsageWithBracketedFilterExpose_RecordsExposeEdge @@ -103,6 +106,29 @@ sections: - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty - AstBuilder_MultipleExposeMembers_OnlyOneBracketed_PairsFilterWithCorrectPath + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ExposeRecursionKindDerivation + title: >- + AstBuilder shall classify each `expose` member's ExposeRecursionKind correctly per its + SysML v2 grammar form (MembershipExpose/NamespaceExpose) and recursion setting (a + trailing `::**`): `expose X;` as MembershipExact, `expose X::**;` as + MembershipRecursive, `expose X::*;` as NamespaceDirectChildren, and `expose X::*::**;` + as NamespaceRecursive, fixing the prior defect where `ExtractImportTarget`'s + namespace-import branch hard-coded its recursion bit away instead of checking for a + trailing `::**`. + justification: | + Per formal-26-03-02.md §8.3.26.2-4, MembershipExpose and NamespaceExpose each carry + their own independent `isRecursive` flag; conflating every non-bracket-filtered + `expose` entry into unconditional whole-subtree inclusion (the pre-fix behavior) + silently over-scoped every non-recursive `expose` statement in every rendered + diagram. Correct per-form, per-recursion classification during AST construction is + the prerequisite for `ExposeScopeResolver`'s corresponding scope-matching fix. + tests: + - AstBuilder_ExposeBareMembership_CapturesMembershipExact + - AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive + - AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren + - AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive + - WorkspaceLoader_LoadAsync_OmgSafetyFeatureViewsFixture_ResolvesBracketedExpose + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewUsageExpose title: >- AstBuilder shall build a SysmlViewNode from a named `view` usage (in addition to a diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml index c9a6af0..9f781e7 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml @@ -44,9 +44,10 @@ sections: - id: SysML2Tools-Language-Semantic-Model-SysmlNode-ViewExposedNames title: >- SysmlViewNode shall expose ExposeMembers: each `expose [::**[]];` member's - qualified-name reference text paired with its own bracket-filter expression text (or - null when the entry carries no bracket filter), in source order; GetExposedNames() - shall return the qualified name of each entry as a computed convenience projection. + qualified-name reference text, its own bracket-filter expression text (or null when the + entry carries no bracket filter), and its `ExposeRecursionKind` classification, in + source order; GetExposedNames() shall return the qualified name of each entry as a + computed convenience projection. justification: | Making a view's `expose` statements user-observable data is the prerequisite for ReferenceResolver, which resolves GetExposedNames() into `Expose`-kind edges that @@ -56,12 +57,19 @@ sections: bracket filter belonged to when a view declared more than one `expose` member; Phase 2a fixes this by pairing each entry's path and bracket-filter text together as a single `ExposeMember`, the prerequisite for `ExposeScopeResolver` to evaluate each entry's - bracket filter against that entry's own containment subtree. + bracket filter against that entry's own containment subtree. This fix additionally adds + the `ExposeRecursionKind` classification, the prerequisite for `ExposeScopeResolver` to + correctly distinguish exact/direct-children/whole-subtree matching per entry instead of + unconditionally treating every entry as whole-subtree. tests: - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty - AstBuilder_ExposeBracketFilter_CapturesRawText - AstBuilder_MultipleExposeMembers_OnlyOneBracketed_PairsFilterWithCorrectPath + - AstBuilder_ExposeBareMembership_CapturesMembershipExact + - AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive + - AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren + - AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive - id: SysML2Tools-Language-Semantic-Model-SysmlNode-ViewFilterExpression title: >- diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 6362282..872f1b3 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -170,21 +170,33 @@ view kind applies), `expose` now scopes the rendered diagram instead of always r entire workspace: - `expose ;` (valid only inside a named `view` usage's body, not a `view def` - definition's body) — scopes the diagram to the union of every exposed name's containment - subtree: `` plus every declaration whose qualified name is `` or is contained - within it (a containment-subtree match, not just the exact element). If `` does not - resolve to any declaration in the workspace (for example, a typo), the tool falls back to - rendering the full workspace for that view — but now also reports a diagnostic identifying the - unresolved name, so the mistake is visible instead of silently rendering everything with no - signal. The bracket-filter form `expose ::**[];` is now (Phase 2a) **evaluated** - using the same supported subset described below for standalone `filter ;`: when the - bracketed expression parses and evaluates successfully, that entry narrows to only the matched - descendant definitions within ``'s own containment subtree, instead of the whole - subtree — each `expose` entry in a view is evaluated independently, so one bracket-filtered - entry's narrowing never affects any other `expose` entry in the same view. A bracket - expression that fails to parse or falls outside the supported subset degrades gracefully to - the previous whole-subtree behavior for that entry, with a diagnostic identifying the failed - expression and reason. + definition's body) — per the SysML v2 grammar, `expose` has four distinct forms with + independent scoping behavior, driven by whether the grammar alternative is + MembershipExpose or NamespaceExpose and whether a trailing `::**` requests recursion: + - `expose X;` (bare MembershipExpose) — scopes to **`X` itself only**, not its containment + subtree. If `X` resolves to a usage (e.g. `part myVehicle : Vehicle;`) rather than a + definition, its resolved type (`Vehicle`) is also included, itself only (not the type's + subtree either). + - `expose X::**;` (recursive MembershipExpose) — scopes to `X` **and its entire containment + subtree**: `X` plus every declaration whose qualified name is `X` or is contained within it. + - `expose X::*;` (bare NamespaceExpose) — scopes to only `X`'s **direct (one-level) children**, + not `X` itself and not deeper descendants. + - `expose X::*::**;` (recursive NamespaceExpose) — scopes to `X` **and its entire containment + subtree**, the same whole-subtree behavior as recursive MembershipExpose. + + If `X` does not resolve to any declaration in the workspace (for example, a typo), the tool + falls back to rendering the full workspace for that view — but now also reports a diagnostic + identifying the unresolved name, so the mistake is visible instead of silently rendering + everything with no signal. The bracket-filter form `expose ::**[];` is now + (Phase 2a) **evaluated** using the same supported subset described below for standalone + `filter ;`: when the bracketed expression parses and evaluates successfully, that entry + narrows to only the matched descendant definitions within ``'s own containment + subtree, instead of the whole subtree — each `expose` entry in a view is evaluated + independently, so one bracket-filtered entry's narrowing never affects any other `expose` + entry in the same view. A bracket expression that fails to parse or falls outside the + supported subset degrades gracefully to whole-subtree inclusion for that entry (regardless of + whether the entry's own form was otherwise non-recursive), with a diagnostic identifying the + failed expression and reason. - `render ;` — per the SysML v2 grammar, this names a rendering style/format (e.g. `asTreeDiagram`, `asElementTable`). `render asTreeDiagram;`, `render asInterconnectionDiagram;`, `render asGeneralDiagram;`, `render asStateTransitionDiagram;`, @@ -255,7 +267,7 @@ of confusion, so it is worth stating plainly: | `render ;` | Selects a rendering style — see "View Body Statements" above. Never scopes content. | | `filter ;` | Narrows scope (Phase 1, and Phase 2a per bracketed `expose`); unsupported falls back unfiltered. | -### Example A: exposing a definition to scope down to a subsystem +### Example A: exposing a definition — exact vs. recursive ```sysml package Vehicle { @@ -272,16 +284,30 @@ package Vehicle { expose Engine; render asTreeDiagram; } + + view EngineRecursiveView { + expose Engine::**; + render asTreeDiagram; + } } ``` -`EngineOnlyView` declares `render asTreeDiagram;`, so it renders via `BrowserViewLayoutStrategy` -as an indented tree of rows rather than the General View's nested boxes. It renders **only** the -`Engine` definition's containment subtree (`Engine` and its `cylinder` part) — `Vehicle`, -`myVehicle`, and `wheel` are excluded entirely. Removing the `expose Engine;` statement (leaving -only `render asTreeDiagram;`, or an empty view body) renders the **full workspace** instead: -`render` never narrows the scope, only `expose` does — `BrowserViewLayoutStrategy` honors -`expose` scoping identically to every other layout strategy. +Both views declare `render asTreeDiagram;`, so they render via `BrowserViewLayoutStrategy` as an +indented tree of rows rather than the General View's nested boxes. `render` never narrows the +scope, only `expose` does — `BrowserViewLayoutStrategy` honors `expose` scoping identically to +every other layout strategy. + +- `EngineOnlyView`'s bare `expose Engine;` is non-recursive (`MembershipExact`): it renders + **only the `Engine` definition itself** — `cylinder` is **not** included, since a bare + `expose X;` no longer implies the whole containment subtree. Confirmed by hand-rendering this + exact fixture: the tree contains a single row, `Engine`. +- `EngineRecursiveView`'s `expose Engine::**;` is recursive (`MembershipRecursive`): it renders + `Engine`'s **entire containment subtree** — `Engine` and its `cylinder` part (two rows) — the + unchanged whole-subtree behavior from before this fix. + +In both views, `Vehicle`, `myVehicle`, and `wheel` are excluded entirely, since neither `Engine` +nor its subtree contains them. Removing the `expose` statement (leaving only `render +asTreeDiagram;`, or an empty view body) renders the **full workspace** instead. > **Note:** `expose` targets are qualified names (`::`-separated), not dotted member-access > chains. `expose myVehicle.engine;` is a **syntax error**, not merely an unresolved reference — @@ -308,19 +334,26 @@ package Vehicle { } ``` -Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`. The tool -resolves `myVehicle`'s own `Typing` edge to find the definition it is typed by (`Vehicle`), and -scopes the diagram to the union of `myVehicle`'s and `Vehicle`'s containment subtrees. This view -also declares `render asTreeDiagram;`, so — like Example A — it renders via -`BrowserViewLayoutStrategy` as an indented tree of rows. The rendered tree therefore includes -`Vehicle` (with its `engine` and `wheel` parts) and, because `engine` is typed by `Engine`, the -`Engine` definition (with its `cylinder` part) as well. - -Contrast this with `expose Vehicle;` (exposing the **definition** directly, as in Example A): -that scopes straight to `Vehicle`'s own containment subtree without needing to resolve any -`Typing` edge, since a definition's subtree is already the thing being scoped to. Exposing a -usage takes one extra hop — through the usage's type reference — to arrive at the same kind of -definition subtree that exposing a `def` reaches directly. +Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`, and is +non-recursive (`MembershipExact`). The tool resolves `myVehicle`'s own `Typing` edge to find the +definition it is typed by (`Vehicle`), and adds that resolved type to the scope too — using the +**same** exact-match (not whole-subtree) recursion kind, since the usage's own expose was itself +non-recursive. Confirmed by hand-rendering this exact fixture: the tree contains only the +`myVehicle` row — neither `Vehicle`'s own `engine`/`wheel` parts, nor `Engine`'s `cylinder`, are +included, because exact-match scoping does not pull in either exposed name's descendants. + +To render `myVehicle`'s and `Vehicle`'s full nested structure instead, expose recursively — +`expose myVehicle::**;` — which scopes to the union of `myVehicle`'s and `Vehicle`'s entire +containment subtrees (unchanged whole-subtree behavior), including `engine`, `wheel`, and (via +`engine`'s own type) `Engine`'s `cylinder` part. + +Contrast this with `expose Vehicle;` (exposing the **definition** directly): that scopes to just +`Vehicle` itself (exact match), without needing to resolve any `Typing` edge, since a +definition's own qualified name is already the exact-match subject. Exposing a usage takes one +extra hop — through the usage's type reference — to add the same kind of definition to the scope +that exposing a `def` reaches directly; in both cases, recursion (`::**`) is what controls +whether descendants are included, independent of whether the initial target was a usage or a +definition. ## Depth Limiting diff --git a/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md b/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md index 921a40e..aef090a 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md +++ b/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md @@ -5,7 +5,7 @@ `ExposeScopeResolver` is verified through direct unit tests in `ExposeScopeResolverTests` that call `ResolveExposedScope`, `IsInSubjectScope`, `IsRootRelevantToScope`, and `IsMoreSpecificCandidate` directly with synthetic `SysmlWorkspace`/`SysmlViewNode` inputs and -assert on the returned `ExposedScope` (Phase 2a: `PrefixSubjects`/`ExplicitMembers`/`Failures`, +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. @@ -20,38 +20,49 @@ configuration are required beyond a standard .NET SDK installation. - A `null` `ViewNode` resolves to a `null` scope. - A `ViewNode` with no resolved `Expose`-kind edges resolves to a `null` scope. - A resolved `Expose` edge targeting a definition resolves to an `ExposedScope` whose - `PrefixSubjects` contains exactly that definition's qualified name. + `Subjects` contains exactly that definition's qualified name. - A resolved `Expose` edge targeting a feature usage additionally includes that usage's own - resolved `Typing` edge target qualified name in `PrefixSubjects`. + resolved `Typing` edge target qualified name in `Subjects`. - Two resolved `Expose` edges on the same view — one targeting a plain definition, one targeting a - feature usage — union both targets plus the usage's resolved type into `PrefixSubjects`. + feature usage — union both targets plus the usage's resolved type into `Subjects`. - (Phase 2a) An `expose ::**[];` entry whose bracket-filter expression parses and evaluates successfully narrows to only the matched descendant `SysmlDefinitionNode`s under that entry's containment subtree, added to `ExposedScope.ExplicitMembers` — the target itself is - *not* added to `PrefixSubjects` (no whole-subtree fallback for a successfully-evaluated entry). + *not* added to `Subjects` (no whole-subtree fallback for a successfully-evaluated entry). - (Phase 2a) Two `expose` entries on the same view, only one bracket-filtered, narrow - independently: the unfiltered entry keeps its whole containment subtree in `PrefixSubjects` + independently: the unfiltered entry keeps its whole containment subtree in `Subjects` while the bracket-filtered entry narrows to only its own matched members in `ExplicitMembers`. - (Phase 2a) A bracket-filter expression that fails to parse or evaluate degrades gracefully to - whole-subtree inclusion for that entry (added to `PrefixSubjects`, same as the unfiltered case) + whole-subtree inclusion for that entry (added to `Subjects`, same as the unfiltered case) and records a `BracketFilterFailure` (expression text plus a reason) in `ExposedScope.Failures`. +- A non-recursive `MembershipExact` `expose X;` entry resolves to a scope containing only `X` + itself — not its containment subtree — and, for a usage target, adds the resolved type using + the same exact-match recursion kind (itself only, not the type's subtree either). +- A recursive `MembershipRecursive`/`NamespaceRecursive` `expose X::**;`/`expose X::*::**;` entry + continues to resolve to a scope containing `X` and its entire containment subtree, unchanged + from before this fix. +- A non-recursive `NamespaceDirectChildren` `expose X::*;` entry resolves to a scope containing + only `X`'s direct (one-level) children — not `X` itself and not deeper descendants. +- A bracket-filter expression that fails to parse or evaluate always falls back to whichever + Recursive variant matches the entry's form, regardless of the entry's own recursion kind — + never degrading to the narrower Exact/DirectChildren behavior on a fallback. - (Phase 2d) A successfully-evaluated bracket-filter expression's candidate set includes named usage-level (`SysmlFeatureNode`) declarations, not just `SysmlDefinitionNode`s, so a metaclass-kind filter like `@SysML::PartUsage` can match a usage-level candidate. -- `IsInSubjectScope` returns `true` for an exact qualified-name match against a `PrefixSubjects` +- `IsInSubjectScope` returns `true` for an exact qualified-name match against a `Subjects` entry. -- `IsInSubjectScope` returns `true` for a qualified name nested under a `PrefixSubjects` entry (a +- `IsInSubjectScope` returns `true` for a qualified name nested under a `Subjects` entry (a `"{subject}::"` prefix match). - `IsInSubjectScope` returns `false` for a qualified name that shares a string prefix with a - `PrefixSubjects` entry but without the `"::"` separator (e.g. `Root::AB` vs. subject `Root::A`). + `Subjects` entry but without the `"::"` separator (e.g. `Root::AB` vs. subject `Root::A`). - `IsInSubjectScope` returns `false` for an unrelated qualified name. - (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. -- `IsRootRelevantToScope` returns `true` when the candidate equals a `PrefixSubjects` entry. -- `IsRootRelevantToScope` returns `true` when the candidate is nested within a `PrefixSubjects` +- `IsRootRelevantToScope` returns `true` when the candidate equals a `Subjects` entry. +- `IsRootRelevantToScope` returns `true` when the candidate is nested within a `Subjects` entry. -- `IsRootRelevantToScope` returns `true` when a `PrefixSubjects` entry is nested within the +- `IsRootRelevantToScope` returns `true` when a `Subjects` entry is nested within the candidate. - `IsRootRelevantToScope` returns `false` for an unrelated candidate. - (Phase 2a) `IsRootRelevantToScope` returns `true` when the candidate exactly equals an @@ -72,30 +83,48 @@ configuration are required beyond a standard .NET SDK installation. - `ResolveExposedScope_NoResolvedExposeEdges_ReturnsNull`: No resolved `Expose` edges resolves to `null` scope - `ResolveExposedScope_ExposedDefinition_ReturnsThatQualifiedName`: - Exposed definition resolves to a scope whose `PrefixSubjects` is exactly that qualified name + Exposed definition resolves to a scope whose `Subjects` is exactly that qualified name - `ResolveExposedScope_ExposedUsage_AlsoIncludesResolvedTypeTarget`: Exposed usage resolves to a scope including both the usage and its resolved type in - `PrefixSubjects` + `Subjects` - `ResolveExposedScope_TwoExposeEdges_DefinitionAndUsageTarget_UnionsBothPlusResolvedType`: Two `Expose` edges (a definition and a usage) union both targets plus the usage's resolved type - into `PrefixSubjects` + into `Subjects` - `ResolveExposedScope_BracketFilterEvaluatesSuccessfully_NarrowsToMatchedDefinitionsOnly`: A successfully-evaluated bracket filter narrows to only the matched descendant definitions in - `ExplicitMembers`, with an empty `PrefixSubjects` and no `Failures` for that entry + `ExplicitMembers`, with an empty `Subjects` and no `Failures` for that entry - `ResolveExposedScope_MixedFilteredAndUnfilteredEntries_NarrowsIndependently`: An unfiltered `expose` entry and a bracket-filtered `expose` entry on the same view narrow - independently — whole subtree in `PrefixSubjects` for the former, matched members in + independently — whole subtree in `Subjects` for the former, matched members in `ExplicitMembers` for the latter - `ResolveExposedScope_BracketFilterFailsToParse_FallsBackToWholeSubtreeAndRecordsFailure`: - A bracket filter that fails to parse falls back to whole-subtree inclusion in `PrefixSubjects` + A bracket filter that fails to parse falls back to whole-subtree inclusion in `Subjects` and records a `BracketFilterFailure` (expression text and reason) in `Failures` +- `ResolveExposedScope_MembershipExact_ScopeIsExactOnly`: + A non-recursive `MembershipExact` entry scopes to only the exposed subject itself, not its + containment subtree +- `ResolveExposedScope_MembershipExactUsage_TypeFallbackIsAlsoExactOnly`: + A non-recursive `MembershipExact` entry targeting a usage adds the resolved type using the same + exact-match recursion kind — the type's own descendants are not included either +- `ResolveExposedScope_MembershipRecursive_ScopeIsWholeSubtree`: + A recursive `MembershipRecursive` entry scopes to the exposed subject's entire containment + subtree — unchanged whole-subtree behavior +- `ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnly`: + A non-recursive `NamespaceDirectChildren` entry scopes to only the exposed subject's direct + children — not the subject itself and not deeper descendants +- `ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree`: + A recursive `NamespaceRecursive` entry scopes to the exposed subject and its entire containment + subtree — unchanged whole-subtree behavior +- `ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive`: + A bracket-filter parse failure on an entry classified as `NamespaceDirectChildren` still falls + back to whole-subtree (Recursive) inclusion, never the narrower direct-children behavior - `ResolveExposedScope_BracketFilterMetaclassKind_MatchesUsageLevelCandidate`: A successfully-evaluated bracket-filter metaclass-kind expression matches a named usage-level candidate (`SysmlFeatureNode`), not just definitions - `IsInSubjectScope_ExactMatch_ReturnsTrue`: - Exact qualified-name match against a `PrefixSubjects` entry is in scope + Exact qualified-name match against a `Subjects` entry is in scope - `IsInSubjectScope_SubtreeMatch_ReturnsTrue`: - Nested qualified name under a `PrefixSubjects` entry is in scope + Nested qualified name under a `Subjects` entry is in scope - `IsInSubjectScope_PrefixWithoutSeparator_ReturnsFalse`: String-prefix-only match (no `::`) is not in scope - `IsInSubjectScope_UnrelatedName_ReturnsFalse`: @@ -105,11 +134,11 @@ configuration are required beyond a standard .NET SDK installation. - `IsInSubjectScope_ExplicitMemberDescendant_ReturnsFalse`: A descendant of an `ExplicitMembers` entry is not automatically in scope (exact match only) - `IsRootRelevantToScope_CandidateEqualsSubject_ReturnsTrue`: - Candidate equal to a `PrefixSubjects` entry is relevant + Candidate equal to a `Subjects` entry is relevant - `IsRootRelevantToScope_CandidateNestedInSubject_ReturnsTrue`: - Candidate nested within a `PrefixSubjects` entry is relevant + Candidate nested within a `Subjects` entry is relevant - `IsRootRelevantToScope_SubjectNestedInCandidate_ReturnsTrue`: - A `PrefixSubjects` entry nested within the candidate is relevant + A `Subjects` entry nested within the candidate is relevant - `IsRootRelevantToScope_UnrelatedCandidate_ReturnsFalse`: Unrelated candidate is not relevant - `IsRootRelevantToScope_ExplicitMember_ReturnsTrue`: diff --git a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md index 7d2b364..9a8ed39 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -31,10 +31,17 @@ external services or additional configuration are required beyond a standard .NE the corresponding `SysmlViewNode`, and leaves both null for a view with an empty body. - `VisitViewUsage` (a named `view` usage, not a `view def` definition) captures the same render/filter members plus `expose ;` members, producing a `SysmlViewNode` with - populated `ExposeMembers` (each entry pairing its qualified-name reference text with its own - bracket-filter expression text, or null when absent). This also makes every named `view` - usage its own renderable declaration, an intentional capability addition beyond `expose` - capture alone (see the ast-builder design doc). + populated `ExposeMembers` (each entry pairing its qualified-name reference text, its own + bracket-filter expression text (or null when absent), and its `ExposeRecursionKind` + classification). This also makes every named `view` usage its own renderable declaration, an + intentional capability addition beyond `expose` capture alone (see the ast-builder design + doc). +- Each `expose` member is classified into the correct `ExposeRecursionKind` per its grammar form + and recursion setting: a bare `expose X;` as `MembershipExact`, a recursive `expose X::**;` as + `MembershipRecursive`, a bare namespace `expose X::*;` as `NamespaceDirectChildren`, and a + recursive namespace `expose X::*::**;` as `NamespaceRecursive` — fixing the prior defect where + the namespace-import branch's recursion bit was hard-coded away instead of checking for a + trailing `::**`. - `BuildUsageNode` captures a feature's redefinition reference on `RedefinedFeatureName` for both the `redefines` keyword form and the `:>>` operator form, for both a bare simple name and a qualified `Owner::feature` form (captured verbatim, unresolved), and leaves it null for a @@ -109,6 +116,11 @@ external services or additional configuration are required beyond a standard .NE | `VisitViewUsage` expose capture | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | | `VisitViewUsage` bracket-filter capture | `AstBuilder_ExposeBracketFilter_CapturesRawText` | | Bracket filter paired to entry | `AstBuilder_MultipleExposeMembers_OnlyOneBracketed_PairsFilterWithCorrectPath` | +| Bare MembershipExpose classification | `AstBuilder_ExposeBareMembership_CapturesMembershipExact` | +| Recursive MembershipExpose classification | `AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive` | +| Bare NamespaceExpose classification | `AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren` | +| Recursive NamespaceExpose classification | `AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive` | +| Bracket-filtered classification | `WorkspaceLoader_LoadAsync_OmgSafetyFeatureViewsFixture_ResolvesBracketedExpose` | | `VisitViewUsage` renderable declaration | `RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages` | | Empty view body regression guard | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | | Redefinition, `redefines` keyword | `WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md index 17ac2f3..534de86 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md @@ -36,15 +36,18 @@ external services or additional configuration are required beyond a standard .NE - `SysmlViewNode.RenderTargetName`/`FilterExpressionText` are populated verbatim from a view's `render`/`filter` body members (raw reference/expression text, never resolved into an edge or diagnostic here), and are `null` for a view with no such members. `SysmlViewNode.ExposeMembers` - is populated with one paired `ExposeMember(QualifiedName, BracketFilterExpressionText)` entry - per `expose [::**[]];` member, in source order, and is empty for a view with none; + is populated with one paired + `ExposeMember(QualifiedName, BracketFilterExpressionText, RecursionKind)` entry per + `expose [::**[]];` member, in source order, and is empty for a view with none; `GetExposedNames()`'s projected qualified names are the only data independently resolved by `ReferenceResolver` into `Expose`-kind edges. Each entry's own `BracketFilterExpressionText` is captured verbatim (raw expression text) and is paired with that same entry's `QualifiedName` — fixing the earlier Phase 1 defect where a view declaring more than one `expose` member could not - reliably associate a bracket filter with the exposed path it was declared on. `AstBuilder` - itself never evaluates a captured bracket-filter expression; real evaluation (Phase 2a) is - `ExposeScopeResolver`'s responsibility. + reliably associate a bracket filter with the exposed path it was declared on. Each entry's + `RecursionKind` is correctly classified as `MembershipExact`/`MembershipRecursive`/ + `NamespaceDirectChildren`/`NamespaceRecursive` per its grammar form and recursion setting. + `AstBuilder` itself never evaluates a captured bracket-filter expression; real evaluation + (Phase 2a) is `ExposeScopeResolver`'s responsibility. - `SysmlFeatureNode.RedefinedFeatureName` is populated verbatim from a feature's `redefines`/`:>>` clause (bare-name and qualified `Owner::feature` forms, both keyword and operator syntax), and is `null` for a feature with no redefinition. It is resolved by @@ -67,6 +70,10 @@ external services or additional configuration are required beyond a standard .NE | `ExposeMembers` from a `view` usage | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | | `ExposeMember` bracket-filter text paired to its path | `AstBuilder_ExposeBracketFilter_CapturesRawText` | | Bracket filter paired to entry | `AstBuilder_MultipleExposeMembers_OnlyOneBracketed_PairsFilterWithCorrectPath` | +| `RecursionKind` bare membership | `AstBuilder_ExposeBareMembership_CapturesMembershipExact` | +| `RecursionKind` recursive membership | `AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive` | +| `RecursionKind` bare namespace | `AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren` | +| `RecursionKind` recursive namespace | `AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive` | | Empty view body leaves all fields null/empty | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | | `RedefinedFeatureName` — `redefines` | `WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName` | | `RedefinedFeatureName` — `:>>` operator | `WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName` | diff --git a/src/DemaConsulting.SysML2Tools.Core/DemaConsulting.SysML2Tools.Core.csproj b/src/DemaConsulting.SysML2Tools.Core/DemaConsulting.SysML2Tools.Core.csproj index 6834048..ba963d1 100644 --- a/src/DemaConsulting.SysML2Tools.Core/DemaConsulting.SysML2Tools.Core.csproj +++ b/src/DemaConsulting.SysML2Tools.Core/DemaConsulting.SysML2Tools.Core.csproj @@ -56,7 +56,7 @@ - + diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs index 123ef4f..7c56ee6 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs @@ -12,15 +12,18 @@ namespace DemaConsulting.SysML2Tools.Layout.Internal; /// /// The resolved qualified-name scope a view's expose statements restrict a diagram to, -/// distinguishing unfiltered "whole containment subtree" exposed paths from bracket-filtered +/// distinguishing unfiltered exposed paths — whose match kind (exact, direct-children-only, or +/// whole containment subtree) depends on each entry's SysML v2 expose grammar form and +/// recursion setting (see ) — from bracket-filtered /// (expose <path>::**[<expr>]) exposed paths that narrow to specific matched /// descendant definitions and/or named usages only. /// -/// -/// Exposed subject qualified names whose entire containment subtree is in scope (a -/// "{subject}::" prefix match) — the existing Phase 1 behavior for expose entries -/// with no bracket filter, and the fallback behavior for a bracket-filtered entry whose expression -/// failed to parse or evaluate. +/// +/// Exposed subject qualified names paired with the recursion kind that governs how they match a +/// candidate qualified name (see ) — the +/// existing Phase 1 behavior for expose entries with no bracket filter (now correctly +/// narrowed for non-recursive forms), and the fallback (always whole-subtree) behavior for a +/// bracket-filtered entry whose expression failed to parse or evaluate. /// /// /// Individual definition or named-usage qualified names matched by a successfully-evaluated @@ -28,7 +31,7 @@ namespace DemaConsulting.SysML2Tools.Layout.Internal; /// not automatically included unless they themselves also match the filter. /// internal sealed record ExposedScope( - IReadOnlyList PrefixSubjects, + IReadOnlyList Subjects, IReadOnlyList ExplicitMembers) { /// @@ -40,6 +43,17 @@ internal sealed record ExposedScope( public IReadOnlyList Failures { get; init; } = Array.Empty(); } +/// +/// A single resolved expose subject qualified name paired with the +/// that governs how it matches a candidate qualified name. +/// +/// The exposed subject's resolved qualified name. +/// +/// The recursion kind (exact, direct-children-only, or whole-subtree) governing how this subject +/// matches candidate qualified names — see . +/// +internal sealed record ExposeSubject(string QualifiedName, ExposeRecursionKind Recursion); + /// /// A single expose <path>::**[<expr>] bracket-filter expression that failed to /// parse or evaluate, degrading gracefully to whole-subtree inclusion for its exposed path. @@ -73,7 +87,7 @@ internal static class ExposeScopeResolver /// type's subtree. To avoid silently scoping to nothing, this also resolves the usage's own /// edge (if any) and adds that type's qualified name to the /// scope as well, so both the usage and its type's subtree are included. This expansion only - /// applies to whole-subtree () entries — a + /// applies to non-exact-only subjects — a /// successfully-evaluated bracket filter's already /// name the exact matched definitions or usages. /// @@ -91,7 +105,7 @@ internal static class ExposeScopeResolver return null; } - var prefixSubjects = new List(); + var subjects = new List(); var explicitMembers = new List(); var failures = new List(); @@ -120,9 +134,10 @@ internal static class ExposeScopeResolver } var bracketFilterText = member?.BracketFilterExpressionText; + var recursionKind = member?.RecursionKind ?? ExposeRecursionKind.MembershipRecursive; if (bracketFilterText is not { Length: > 0 }) { - AddWholeSubtreeSubject(workspace, target, prefixSubjects); + AddSubject(workspace, target, recursionKind, subjects); continue; } @@ -130,7 +145,14 @@ internal static class ExposeScopeResolver if (parseResult.Expression is not { } expression) { failures.Add(new BracketFilterFailure(bracketFilterText, parseResult.Diagnostics.FirstOrDefault()?.Message)); - AddWholeSubtreeSubject(workspace, target, prefixSubjects); + + // Bracket-filter failures always degrade to full-subtree inclusion, never to the + // narrower Exact/DirectChildren behavior — force whichever Recursive variant + // matches the entry's form. + var fallbackKind = recursionKind is ExposeRecursionKind.NamespaceDirectChildren or ExposeRecursionKind.NamespaceRecursive + ? ExposeRecursionKind.NamespaceRecursive + : ExposeRecursionKind.MembershipRecursive; + AddSubject(workspace, target, fallbackKind, subjects); continue; } @@ -144,18 +166,24 @@ internal static class ExposeScopeResolver explicitMembers.AddRange(evaluation.MatchedQualifiedNames); } - return new ExposedScope(prefixSubjects, explicitMembers) { Failures = failures }; + return new ExposedScope(subjects, explicitMembers) { Failures = failures }; } /// /// Adds (and, when it resolves to a usage, its resolved type's - /// qualified name) to — the existing Phase 1 whole-subtree - /// inclusion behavior, shared by unfiltered expose entries and bracket-filtered entries - /// that fell back after a parse/evaluation failure. + /// qualified name — using the same for both) to + /// — the existing Phase 1 whole-subtree inclusion behavior for + /// recursive kinds, now correctly narrowed to exact/direct-children matching for non-recursive + /// kinds, shared by unfiltered expose entries and bracket-filtered entries that fell + /// back after a parse/evaluation failure. /// - private static void AddWholeSubtreeSubject(SysmlWorkspace workspace, string target, List subjects) + private static void AddSubject( + SysmlWorkspace workspace, + string target, + ExposeRecursionKind kind, + List subjects) { - subjects.Add(target); + subjects.Add(new ExposeSubject(target, kind)); if (workspace.Declarations.TryGetValue(target, out var declaration) && declaration is SysmlFeatureNode { } feature) @@ -165,24 +193,64 @@ private static void AddWholeSubtreeSubject(SysmlWorkspace workspace, string targ ?.TargetQualifiedName; if (typeTarget is not null) { - subjects.Add(typeTarget); + subjects.Add(new ExposeSubject(typeTarget, kind)); } } } /// - /// Returns when is one of - /// 's or lies within one of - /// their containment subtrees (a "{subject}::" prefix match, reusing the same - /// qualified-name-prefix idiom - /// already uses for stdlib-prefix matching), or is an exact match of one of 's + /// Returns when matches one of + /// 's per that subject's own + /// : + /// + /// / + /// : the subject itself or lies within its + /// containment subtree (a "{subject}::" prefix match, reusing the same + /// qualified-name-prefix idiom + /// already uses for + /// stdlib-prefix matching). + /// : the subject itself + /// only, exact match. + /// : a direct + /// (one-level) child of the subject only — not the subject itself, not a deeper + /// descendant. + /// + /// or is an exact match of one of 's /// (a bracket-filter-matched definition or usage). /// public static bool IsInSubjectScope(string qualifiedName, ExposedScope scope) => - scope.PrefixSubjects.Any(subject => - qualifiedName == subject || qualifiedName.StartsWith(subject + "::", StringComparison.Ordinal)) || + scope.Subjects.Any(subject => subject.Recursion switch + { + ExposeRecursionKind.MembershipRecursive or ExposeRecursionKind.NamespaceRecursive => + qualifiedName == subject.QualifiedName || + qualifiedName.StartsWith(subject.QualifiedName + "::", StringComparison.Ordinal), + ExposeRecursionKind.MembershipExact => qualifiedName == subject.QualifiedName, + ExposeRecursionKind.NamespaceDirectChildren => IsDirectChildOf(qualifiedName, subject.QualifiedName), + _ => false, + }) || scope.ExplicitMembers.Contains(qualifiedName, StringComparer.Ordinal); + /// + /// Returns when is a direct (one-level) + /// child of — starts with "{container}::" and the + /// remainder after that prefix contains no further "::" separator. Not satisfied by + /// itself, nor by any deeper descendant. + /// + /// The candidate qualified name. + /// The container's qualified name. + /// when is a direct child of . + private static bool IsDirectChildOf(string qualifiedName, string container) + { + var prefix = container + "::"; + if (!qualifiedName.StartsWith(prefix, StringComparison.Ordinal)) + { + return false; + } + + var remainder = qualifiedName[prefix.Length..]; + return !remainder.Contains("::", StringComparison.Ordinal); + } + /// /// Returns when (a candidate /// single-root diagram root, e.g. the definition a , @@ -216,7 +284,7 @@ bool RelevantTo(string subject) => candidateQualifiedName.StartsWith(subject + "::", StringComparison.Ordinal) || subject.StartsWith(candidateQualifiedName + "::", StringComparison.Ordinal); - return scope.PrefixSubjects.Any(RelevantTo) || scope.ExplicitMembers.Any(RelevantTo); + return scope.Subjects.Select(s => s.QualifiedName).Any(RelevantTo) || scope.ExplicitMembers.Any(RelevantTo); } /// diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs index 1a94c4a..28f28fe 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs @@ -17,14 +17,16 @@ namespace DemaConsulting.SysML2Tools.Rendering.Internal; /// /// The synthesized node is scoped to its target by manually populating with a single edge (plus a -/// matching entry) — the same mechanism a real, -/// parsed view def V { expose Target; } produces via ReferenceResolver. reads only these two -/// properties and has no notion of provenance, so it treats a synthesized node identically to a -/// parsed one. This differs from , whose node +/// matching entry using +/// ) — the same mechanism a real, +/// parsed view def V { expose Target::**; } produces via ReferenceResolver, so +/// that a dynamic view shows the requested target's whole containment subtree rather than the +/// target alone. reads +/// only these two properties and has no notion of provenance, so it treats a synthesized node +/// identically to a parsed one. This differs from , whose node /// carries no ResolvedEdges at all — that absence is what makes ExposeScopeResolver /// return a scope (render everything); a dynamic view instead always -/// resolves to a definite, non-null scope naming exactly the requested target. +/// resolves to a definite, non-null scope rooted at the requested target's whole subtree. /// /// /// The synthesized view's uses a leading $ — a @@ -135,7 +137,7 @@ internal static (SysmlViewNode? ViewNode, string? Diagnostic) Synthesize( Name = viewName, QualifiedName = viewQualifiedName, RenderTargetName = renderTargetName, - ExposeMembers = [new ExposeMember(targetQualifiedName, null)], + ExposeMembers = [new ExposeMember(targetQualifiedName, null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge(viewQualifiedName, targetQualifiedName, SysmlEdgeKind.Expose)], FilterExpressionText = filterExpressionText, }; diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index 1ad293a..86d5fa6 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -2028,12 +2028,23 @@ private static IReadOnlyList ExtractExposedNames( continue; } - var (qn, _, bracketFilterText) = ExtractImportTarget( + var isNamespaceForm = expose.namespaceExpose() is not null; + var (qn, _, bracketFilterText, isRecursive) = ExtractImportTarget( expose.namespaceExpose()?.namespaceImport(), expose.membershipExpose()?.membershipImport()); if (qn is { Length: > 0 }) { - members.Add(new ExposeMember(qn, bracketFilterText)); + var hasBracketFilter = bracketFilterText is not null; + var recursionKind = (isNamespaceForm, isRecursive, hasBracketFilter) switch + { + (true, _, true) => ExposeRecursionKind.NamespaceRecursive, + (false, _, true) => ExposeRecursionKind.MembershipRecursive, + (true, true, false) => ExposeRecursionKind.NamespaceRecursive, + (true, false, false) => ExposeRecursionKind.NamespaceDirectChildren, + (false, true, false) => ExposeRecursionKind.MembershipRecursive, + (false, false, false) => ExposeRecursionKind.MembershipExact, + }; + members.Add(new ExposeMember(qn, bracketFilterText, recursionKind)); } } @@ -2070,7 +2081,7 @@ private static IReadOnlyList ExtractExposedNames( return null; } - var (qn, isWildcard, bracketFilterText) = ExtractImportTarget(decl.namespaceImport(), decl.membershipImport()); + var (qn, isWildcard, bracketFilterText, _) = ExtractImportTarget(decl.namespaceImport(), decl.membershipImport()); if (qn is null) { return null; @@ -2099,9 +2110,12 @@ private static IReadOnlyList ExtractExposedNames( /// The membership-import alternative, or null when not this form. /// /// The extracted qualified/dotted name text (or null when neither alternative yielded - /// text) and whether the import/expose is a wildcard. + /// text), whether the import/expose is a wildcard, the bracket-filter expression text (if + /// any), and whether this entry is recursive (derived from a trailing ::**, or + /// always true for bracket-filtered forms by design, since the grammar only reaches that + /// alternative via ::**[filterExpr]). /// - private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExpressionText) ExtractImportTarget( + private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExpressionText, bool IsRecursive) ExtractImportTarget( SysMLv2Parser.NamespaceImportContext? namespaceImport, SysMLv2Parser.MembershipImportContext? membershipImport) { @@ -2111,7 +2125,7 @@ private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExp var qn = namespaceImport.qualifiedName()?.GetText(); if (qn is { Length: > 0 }) { - return (qn, true, null); + return (qn, true, null, namespaceImport.STAR_STAR() is not null); } // Bracketed-filter form: qualifiedName::**[filterExpr] — the dominant expose form in @@ -2123,6 +2137,10 @@ private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExp // filterPackageMember (multiple bracket filters chained on one path are extremely // rare; the first is representative); it is paired with its ExposeMember by // ExtractExposedNames and evaluated per-entry by ExposeScopeResolver (Phase 2a). + // Bracket-filtered forms are always treated as recursive (IsRecursive: true) + // regardless of the nested optional STAR_STAR token — the grammar's filterPackage + // form is treated as always recursive by design (the grammar only reaches this + // alternative via ::**[filterExpr]). var filterPackage = namespaceImport.filterPackage(); var bracketFilterText = filterPackage?.filterPackageMember()?.FirstOrDefault()?.ownedExpression() is { } bracketExpr ? GetOriginalText(bracketExpr) @@ -2136,7 +2154,7 @@ private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExp var filterQn = filterMembershipImport.qualifiedName()?.GetText(); if (filterQn is { Length: > 0 }) { - return (filterQn, filterMembershipImport.STAR_STAR() is not null, bracketFilterText); + return (filterQn, filterMembershipImport.STAR_STAR() is not null, bracketFilterText, true); } } @@ -2146,7 +2164,7 @@ private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExp var directQn = namespaceImportDirect.qualifiedName()?.GetText(); if (directQn is { Length: > 0 }) { - return (directQn, true, bracketFilterText); + return (directQn, true, bracketFilterText, true); } } } @@ -2159,11 +2177,12 @@ private static (string? QualifiedName, bool IsWildcard, string? BracketFilterExp var qn = membershipImport.qualifiedName()?.GetText(); if (qn is { Length: > 0 }) { - return (qn, membershipImport.STAR_STAR() is not null, null); + var isRecursive = membershipImport.STAR_STAR() is not null; + return (qn, isRecursive, null, isRecursive); } } - return (null, false, null); + return (null, false, null, false); } /// diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 2cbbb0d..4411cbb 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -356,7 +356,56 @@ public sealed class SysmlViewNode : SysmlNode /// scope to the matching descendant definitions only, falling back to whole-subtree inclusion /// (plus a diagnostic) when the expression fails to parse or evaluate. /// -public sealed record ExposeMember(string QualifiedName, string? BracketFilterExpressionText); +/// +/// Classifies which SysML v2 expose grammar form and recursion setting produced this +/// entry (). Consumed by ExposeScopeResolver to decide +/// between an exact/direct-children match and a whole-subtree match when resolving this +/// entry's contribution to a view's exposed scope. +/// +public sealed record ExposeMember( + string QualifiedName, + string? BracketFilterExpressionText, + ExposeRecursionKind RecursionKind); + +/// +/// Classifies which SysML v2 expose grammar form and recursion setting produced an +/// entry, per formal-26-03-02.md §8.3.26.2-4: MembershipExpose +/// (expose X; / expose X::**;) and NamespaceExpose (expose X::*; / +/// expose X::*::**;) each carry their own independent isRecursive flag derived +/// from a trailing ::**. A bracket-filtered entry (expose X::**[expr]) is always +/// one of the two *Recursive kinds — the grammar's filterPackage form is treated as always +/// recursive by design, since that alternative is only reachable via ::**[filterExpr] +/// (regardless of the nested optional STAR_STAR token) — because +/// ExposeScopeResolver only consults this classification for its unfiltered/fallback +/// whole-subtree-vs-narrow behavior, never for a successfully-evaluated bracket filter's own +/// (already-exact) ExplicitMembers. +/// +public enum ExposeRecursionKind +{ + /// + /// MembershipExpose, non-recursive: expose X; — only X itself (plus, for a usage + /// target, its resolved type itself — see ExposeScopeResolver) is in scope. + /// + MembershipExact, + + /// + /// MembershipExpose, recursive: expose X::**; — X and its entire containment + /// subtree (the pre-fix, still-correct-for-this-case whole-subtree behavior). + /// + MembershipRecursive, + + /// + /// NamespaceExpose, non-recursive: expose X::*; — only X's direct (one-level) + /// children, not X itself and not deeper descendants. + /// + NamespaceDirectChildren, + + /// + /// NamespaceExpose, recursive: expose X::*::**; — X and its entire containment + /// subtree (the pre-fix, still-correct-for-this-case whole-subtree behavior). + /// + NamespaceRecursive, +} /// /// AST node representing a viewpoint definition. diff --git a/src/DemaConsulting.SysML2Tools.Tool/DemaConsulting.SysML2Tools.Tool.csproj b/src/DemaConsulting.SysML2Tools.Tool/DemaConsulting.SysML2Tools.Tool.csproj index ed8f6b0..01d00bd 100644 --- a/src/DemaConsulting.SysML2Tools.Tool/DemaConsulting.SysML2Tools.Tool.csproj +++ b/src/DemaConsulting.SysML2Tools.Tool/DemaConsulting.SysML2Tools.Tool.csproj @@ -98,8 +98,8 @@ - - + + diff --git a/test/DemaConsulting.SysML2Tools.Tests/DemaConsulting.SysML2Tools.Tests.csproj b/test/DemaConsulting.SysML2Tools.Tests/DemaConsulting.SysML2Tools.Tests.csproj index 9ccc32a..ebf7cf2 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/DemaConsulting.SysML2Tools.Tests.csproj +++ b/test/DemaConsulting.SysML2Tools.Tests/DemaConsulting.SysML2Tools.Tests.csproj @@ -56,15 +56,15 @@ - - + + - - - + + + diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs index fda2cba..f04fd15 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs @@ -457,7 +457,7 @@ public void ActionFlowView_BuildLayout_ExposeNonHeuristicRoot_SelectsExposedRoot { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("ProcessB", null)], + ExposeMembers = [new ExposeMember("ProcessB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessB", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -485,7 +485,7 @@ public void ActionFlowView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_Select { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("b1", null)], + ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessB::b1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -559,7 +559,7 @@ public void ActionFlowView_BuildLayout_ExposeInnerActionOfNestedDefinition_Selec { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("c1", null)], + ExposeMembers = [new ExposeMember("c1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessA::ProcessC::c1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -585,7 +585,7 @@ public void ActionFlowView_BuildLayout_ExposeUnrelatedDefinition_NoRootSelected_ { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("Unrelated", null)], + ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -613,7 +613,7 @@ public void ActionFlowView_BuildLayout_ExposeSingleAction_DropsOutOfScopeAction( { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("a1", null)], + ExposeMembers = [new ExposeMember("a1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessA::a1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -654,7 +654,7 @@ public void ActionFlowView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot( { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("myProcess", null)], + ExposeMembers = [new ExposeMember("myProcess", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myProcess", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs index 5b00fff..03bf20a 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs @@ -142,7 +142,7 @@ public void GridView_BuildLayout_ExposedName_UnionsAdditionalSubtree() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("A", null)], + ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -195,7 +195,7 @@ public void GridView_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefinitionS { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("myVehicle", null)], + ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -223,7 +223,7 @@ public void GridView_BuildLayout_ExposeMultipleTargets_UnionsBothSubtrees() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("A", null), new ExposeMember("B", null)], + ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("B", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose), @@ -267,7 +267,7 @@ public void GridView_BuildLayout_ExposeOneSideOfSpecialization_KeepsBothRowAndCo { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Sub", null)], + ExposeMembers = [new ExposeMember("Sub", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A::Sub", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -301,7 +301,7 @@ public void BrowserView_BuildLayout_ExposedName_UnionsAdditionalSubtree() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("A", null)], + ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -353,7 +353,7 @@ public void BrowserView_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefiniti { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("myVehicle", null)], + ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -380,7 +380,7 @@ public void BrowserView_BuildLayout_ExposeMultipleTargets_UnionsBothSubtrees() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("A", null), new ExposeMember("B", null)], + ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("B", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose), diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs index ba3626d..9d2e37b 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs @@ -52,7 +52,7 @@ public void ResolveExposedScope_NoResolvedExposeEdges_ReturnsNull() /// /// A resolved Expose edge targeting a definition, with no bracket filter, resolves to - /// a scope whose contains exactly that + /// a scope whose contains exactly that /// definition's qualified name. /// [Fact] @@ -69,14 +69,14 @@ public void ResolveExposedScope_ExposedDefinition_ReturnsThatQualifiedName() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::A", null)], + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Equal(["Root::A"], scope.PrefixSubjects); + Assert.Equal(["Root::A"], scope.Subjects.Select(s => s.QualifiedName)); Assert.Empty(scope.ExplicitMembers); Assert.Empty(scope.Failures); } @@ -107,29 +107,29 @@ public void ResolveExposedScope_ExposedUsage_AlsoIncludesResolvedTypeTarget() { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::myVehicle", null)], + ExposeMembers = [new ExposeMember("Root::myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Contains("Root::myVehicle", scope.PrefixSubjects); - Assert.Contains("Root::Vehicle", scope.PrefixSubjects); + Assert.Contains("Root::myVehicle", scope.Subjects.Select(s => s.QualifiedName)); + Assert.Contains("Root::Vehicle", scope.Subjects.Select(s => s.QualifiedName)); } /// An exact qualified-name match is in scope. [Fact] public void IsInSubjectScope_ExactMatch_ReturnsTrue() { - Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", new ExposedScope(["Root::A"], []))); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// A qualified name nested under a subject's containment subtree is in scope. [Fact] public void IsInSubjectScope_SubtreeMatch_ReturnsTrue() { - Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", new ExposedScope(["Root::A"], []))); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// @@ -140,14 +140,14 @@ public void IsInSubjectScope_SubtreeMatch_ReturnsTrue() [Fact] public void IsInSubjectScope_PrefixWithoutSeparator_ReturnsFalse() { - Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::AB", new ExposedScope(["Root::A"], []))); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::AB", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// An unrelated qualified name is not in scope. [Fact] public void IsInSubjectScope_UnrelatedName_ReturnsFalse() { - Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::B", new ExposedScope(["Root::A"], []))); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::B", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// An explicit (bracket-filter-matched) member is in scope by exact match only. @@ -171,7 +171,7 @@ public void IsInSubjectScope_ExplicitMemberDescendant_ReturnsFalse() [Fact] public void IsRootRelevantToScope_CandidateEqualsSubject_ReturnsTrue() { - Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A", new ExposedScope(["Root::A"], []))); + Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// @@ -181,7 +181,7 @@ public void IsRootRelevantToScope_CandidateEqualsSubject_ReturnsTrue() [Fact] public void IsRootRelevantToScope_CandidateNestedInSubject_ReturnsTrue() { - Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A::Child", new ExposedScope(["Root::A"], []))); + Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A::Child", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// @@ -192,14 +192,14 @@ public void IsRootRelevantToScope_CandidateNestedInSubject_ReturnsTrue() [Fact] public void IsRootRelevantToScope_SubjectNestedInCandidate_ReturnsTrue() { - Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A", new ExposedScope(["Root::A::Child"], []))); + Assert.True(ExposeScopeResolver.IsRootRelevantToScope("Root::A", new ExposedScope([new ExposeSubject("Root::A::Child", ExposeRecursionKind.MembershipRecursive)], []))); } /// A candidate root unrelated to any exposed subject is not relevant to the scope. [Fact] public void IsRootRelevantToScope_UnrelatedCandidate_ReturnsFalse() { - Assert.False(ExposeScopeResolver.IsRootRelevantToScope("Root::B", new ExposedScope(["Root::A"], []))); + Assert.False(ExposeScopeResolver.IsRootRelevantToScope("Root::B", new ExposedScope([new ExposeSubject("Root::A", ExposeRecursionKind.MembershipRecursive)], []))); } /// An explicit (bracket-filter-matched) member is itself relevant to the scope. @@ -306,7 +306,7 @@ public void ResolveExposedScope_TwoExposeEdges_DefinitionAndUsageTarget_UnionsBo { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::A", null), new ExposeMember("Root::myVehicle", null)], + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("Root::myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose), @@ -317,9 +317,9 @@ public void ResolveExposedScope_TwoExposeEdges_DefinitionAndUsageTarget_UnionsBo var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Contains("Root::A", scope.PrefixSubjects); - Assert.Contains("Root::myVehicle", scope.PrefixSubjects); - Assert.Contains("Root::Vehicle", scope.PrefixSubjects); + Assert.Contains("Root::A", scope.Subjects.Select(s => s.QualifiedName)); + Assert.Contains("Root::myVehicle", scope.Subjects.Select(s => s.QualifiedName)); + Assert.Contains("Root::Vehicle", scope.Subjects.Select(s => s.QualifiedName)); } /// @@ -344,7 +344,7 @@ public void ResolveExposedScope_BracketFilterMetaclassKind_MatchesUsageLevelCand { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::Container", "@SysML::PartUsage")], + ExposeMembers = [new ExposeMember("Root::Container", "@SysML::PartUsage", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; @@ -361,7 +361,7 @@ public void ResolveExposedScope_BracketFilterMetaclassKind_MatchesUsageLevelCand /// under the target's containment subtree that carry the @Safety metadata /// annotation — the target itself and its unannotated sibling are excluded from /// , and the target is not added to - /// (no whole-subtree fallback). + /// (no whole-subtree fallback). /// [Fact] public void ResolveExposedScope_BracketFilterEvaluatesSuccessfully_NarrowsToMatchedDefinitionsOnly() @@ -390,14 +390,14 @@ public void ResolveExposedScope_BracketFilterEvaluatesSuccessfully_NarrowsToMatc { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::Container", "@Safety")], + ExposeMembers = [new ExposeMember("Root::Container", "@Safety", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Empty(scope.PrefixSubjects); + Assert.Empty(scope.Subjects); Assert.Equal(["Root::Container::Matched"], scope.ExplicitMembers); Assert.Empty(scope.Failures); } @@ -405,7 +405,7 @@ public void ResolveExposedScope_BracketFilterEvaluatesSuccessfully_NarrowsToMatc /// /// Two expose entries on the same view, only one bracket-filtered, each narrow /// independently: the unfiltered entry keeps its whole containment subtree - /// (), while the bracket-filtered entry narrows to + /// (), while the bracket-filtered entry narrows to /// only its own matched descendant definitions () — /// proving a bracket filter on one entry does not affect an unrelated entry's contribution. /// @@ -439,8 +439,8 @@ public void ResolveExposedScope_MixedFilteredAndUnfilteredEntries_NarrowsIndepen QualifiedName = "Root::V", ExposeMembers = [ - new ExposeMember("Root::Plain", null), - new ExposeMember("Root::Container", "@Safety") + new ExposeMember("Root::Plain", null, ExposeRecursionKind.MembershipRecursive), + new ExposeMember("Root::Container", "@Safety", ExposeRecursionKind.MembershipRecursive) ], ResolvedEdges = [ @@ -452,7 +452,7 @@ public void ResolveExposedScope_MixedFilteredAndUnfilteredEntries_NarrowsIndepen var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Equal(["Root::Plain"], scope.PrefixSubjects); + Assert.Equal(["Root::Plain"], scope.Subjects.Select(s => s.QualifiedName)); Assert.Equal(["Root::Container::Matched"], scope.ExplicitMembers); Assert.Empty(scope.Failures); } @@ -460,7 +460,7 @@ public void ResolveExposedScope_MixedFilteredAndUnfilteredEntries_NarrowsIndepen /// /// A bracket-filter expression that fails to parse (an unsupported Phase 1 construct) /// degrades gracefully to whole-subtree inclusion for that entry (via - /// ) and records the failure (with a reason) in + /// ) and records the failure (with a reason) in /// , rather than crashing or silently excluding the /// entry. /// @@ -478,18 +478,219 @@ public void ResolveExposedScope_BracketFilterFailsToParse_FallsBackToWholeSubtre { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("Root::Container", "x istype Y")], + ExposeMembers = [new ExposeMember("Root::Container", "x istype Y", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.Equal(["Root::Container"], scope.PrefixSubjects); + Assert.Equal(["Root::Container"], scope.Subjects.Select(s => s.QualifiedName)); Assert.Empty(scope.ExplicitMembers); var failure = Assert.Single(scope.Failures); Assert.Equal("x istype Y", failure.ExpressionText); Assert.NotNull(failure.Reason); } + + /// + /// A non-recursive MembershipExact expose entry (expose X;) resolves to a + /// scope containing only the exposed subject itself — its containment subtree is not + /// included. + /// + [Fact] + public void ResolveExposedScope_MembershipExact_ScopeIsExactOnly() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipExact)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); + } + + /// + /// A non-recursive MembershipExact expose entry targeting a usage still adds the + /// usage's resolved type, using the same exact-match recursion kind — the type's own + /// descendants are not included either. + /// + [Fact] + public void ResolveExposedScope_MembershipExactUsage_TypeFallbackIsAlsoExactOnly() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::Vehicle"] = new SysmlDefinitionNode { Name = "Vehicle", QualifiedName = "Root::Vehicle", DefinitionKeyword = "part def" }, + ["Root::Vehicle::Engine"] = new SysmlDefinitionNode { Name = "Engine", QualifiedName = "Root::Vehicle::Engine", DefinitionKeyword = "part def" }, + ["Root::myVehicle"] = new SysmlFeatureNode + { + Name = "myVehicle", + QualifiedName = "Root::myVehicle", + FeatureTyping = "Vehicle", + ResolvedEdges = [new SysmlEdge("Root::myVehicle", "Root::Vehicle", SysmlEdgeKind.Typing)] + } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::myVehicle", null, ExposeRecursionKind.MembershipExact)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::myVehicle", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::Vehicle", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::Vehicle::Engine", scope)); + } + + /// + /// A recursive MembershipRecursive expose entry (expose X::**;) resolves to a + /// scope containing the exposed subject's entire containment subtree — unchanged whole + /// subtree behavior. + /// + [Fact] + public void ResolveExposedScope_MembershipRecursive_ScopeIsWholeSubtree() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipRecursive)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); + } + + /// + /// A non-recursive NamespaceDirectChildren expose entry (expose X::*;) + /// resolves to a scope containing only X's direct children — not X itself and + /// not deeper (grandchild) descendants. + /// + [Fact] + public void ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnly() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" }, + ["Root::A::Child::Grandchild"] = new SysmlDefinitionNode { Name = "Grandchild", QualifiedName = "Root::A::Child::Grandchild", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.NamespaceDirectChildren)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::A::Child::Grandchild", scope)); + } + + /// + /// A recursive NamespaceRecursive expose entry (expose X::*::**;) resolves to + /// a scope containing X and its entire containment subtree — unchanged whole subtree + /// behavior. + /// + [Fact] + public void ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" }, + ["Root::A::Child::Grandchild"] = new SysmlDefinitionNode { Name = "Grandchild", QualifiedName = "Root::A::Child::Grandchild", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.NamespaceRecursive)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child::Grandchild", scope)); + } + + /// + /// A bracket-filtered expose entry that fails to parse falls back to whole-subtree + /// inclusion regardless of the entry's own recursion kind — even a non-recursive + /// MembershipExact-classified bracket-filter entry (a hypothetical malformed + /// grammar edge case) degrades to recursive fallback, per the documented + /// "don't silently lose content" fallback intent. + /// + [Fact] + public void ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::A", DefinitionKeyword = "part def" }, + ["Root::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::A::Child", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [new ExposeMember("Root::A", "x istype Y", ExposeRecursionKind.NamespaceDirectChildren)], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); + Assert.Single(scope.Failures); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index efddb9f..0b092b7 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -1061,7 +1061,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSu { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("A", null)], + ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -1128,7 +1128,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedUsage_ResolvesThroughTy { Name = "V", QualifiedName = "Root::V", - ExposeMembers = [new ExposeMember("myVehicle", null)], + ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs index 314e4d1..7b48880 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs @@ -760,7 +760,7 @@ public void InterconnectionView_BuildLayout_ExposeInnerPartOfNestedDefinition_Se { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("c1", null)], + ExposeMembers = [new ExposeMember("c1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysA::SysC::c1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -953,7 +953,7 @@ public void InterconnectionView_BuildLayout_ExposeNonHeuristicRoot_SelectsExpose { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("SysB", null)], + ExposeMembers = [new ExposeMember("SysB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysB", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -1025,7 +1025,7 @@ public void InterconnectionView_BuildLayout_ExposeBothSameDepthSiblings_ScoreBre { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("AB", null), new ExposeMember("MuchLongerSiblingName", null)], + ExposeMembers = [new ExposeMember("AB", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("MuchLongerSiblingName", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("M::V", "M::AB", SysmlEdgeKind.Expose), @@ -1055,7 +1055,7 @@ public void InterconnectionView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_S { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("b1", null)], + ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysB::b1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -1086,7 +1086,7 @@ public void InterconnectionView_BuildLayout_ExposeUnrelatedDefinition_NoRootSele { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("Unrelated", null)], + ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -1111,7 +1111,7 @@ public void InterconnectionView_BuildLayout_ExposeSinglePart_NarrowsToThatPart() { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("a1", null)], + ExposeMembers = [new ExposeMember("a1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysA::a1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -1140,7 +1140,7 @@ public void InterconnectionView_BuildLayout_ExposeMultipleParts_UnionsBothSubtre { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("a1", null), new ExposeMember("a2", null)], + ExposeMembers = [new ExposeMember("a1", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("a2", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("M::V", "M::SysA::a1", SysmlEdgeKind.Expose), @@ -1179,7 +1179,7 @@ public void InterconnectionView_BuildLayout_ExposedUsage_ResolvesThroughTypingTo { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("myPart", null)], + ExposeMembers = [new ExposeMember("myPart", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myPart", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs index 2fd0490..1e72b66 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs @@ -269,7 +269,7 @@ public void SequenceView_BuildLayout_ExposeNonHeuristicRoot_SelectsExposedRoot() { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("ProtocolB", null)], + ExposeMembers = [new ExposeMember("ProtocolB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolB", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -298,7 +298,7 @@ public void SequenceView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_SelectsI { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("x", null)], + ExposeMembers = [new ExposeMember("x", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolB::x", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -372,7 +372,7 @@ public void SequenceView_BuildLayout_ExposeInnerLifelineOfNestedDefinition_Selec { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("x", null)], + ExposeMembers = [new ExposeMember("x", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolA::ProtocolC::x", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -399,7 +399,7 @@ public void SequenceView_BuildLayout_ExposeUnrelatedDefinition_NoRootSelected_Re { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("Unrelated", null)], + ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -428,7 +428,7 @@ public void SequenceView_BuildLayout_ExposeSingleLifeline_NarrowsLifelines() { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("server", null)], + ExposeMembers = [new ExposeMember("server", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolA::server", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -466,7 +466,7 @@ public void SequenceView_BuildLayout_ExposeBothLifelines_UnionsSubtreesKeepsMess { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("client", null), new ExposeMember("server", null)], + ExposeMembers = [new ExposeMember("client", null, ExposeRecursionKind.MembershipRecursive), new ExposeMember("server", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [ new SysmlEdge("M::V", "M::ProtocolA::client", SysmlEdgeKind.Expose), @@ -506,7 +506,7 @@ public void SequenceView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot() { Name = "V", QualifiedName = "M::V", - ExposeMembers = [new ExposeMember("myProtocol", null)], + ExposeMembers = [new ExposeMember("myProtocol", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myProtocol", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs index 3b74a3b..b8ec69c 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs @@ -313,7 +313,7 @@ public void StateTransitionView_BuildLayout_ExposeNonHeuristicRoot_SelectsExpose { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("MachineB", null)], + ExposeMembers = [new ExposeMember("MachineB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineB", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -341,7 +341,7 @@ public void StateTransitionView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_S { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("b1", null)], + ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineB::b1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -414,7 +414,7 @@ public void StateTransitionView_BuildLayout_ExposeInnerStateOfNestedDefinition_S { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("b1", null)], + ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineA::MachineB::b1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -440,7 +440,7 @@ public void StateTransitionView_BuildLayout_ExposeUnrelatedDefinition_NoRootSele { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("Unrelated", null)], + ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::Unrelated", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -468,7 +468,7 @@ public void StateTransitionView_BuildLayout_ExposeSingleState_DropsIsolatedOutOf { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("s1", null)], + ExposeMembers = [new ExposeMember("s1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineA::s1", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); @@ -508,7 +508,7 @@ public void StateTransitionView_BuildLayout_ExposedUsage_ResolvesThroughTypingTo { Name = "V", QualifiedName = "SM::V", - ExposeMembers = [new ExposeMember("myMachine", null)], + ExposeMembers = [new ExposeMember("myMachine", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::myMachine", SysmlEdgeKind.Expose)] }; var context = new ViewContext("v", workspace, viewNode); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs index 9454653..3fdfba1 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs @@ -33,6 +33,7 @@ public void Synthesize_GeneralKind_ResolvableTarget_Succeeds() Assert.Equal("asGeneralDiagram", viewNode!.RenderTargetName); Assert.Single(viewNode.ExposeMembers); Assert.Equal("P::Widget", viewNode.ExposeMembers[0].QualifiedName); + Assert.Equal(ExposeRecursionKind.MembershipRecursive, viewNode.ExposeMembers[0].RecursionKind); var edge = Assert.Single(viewNode.ResolvedEdges); Assert.Equal(SysmlEdgeKind.Expose, edge.Kind); Assert.Equal("P::Widget", edge.TargetQualifiedName); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs index 3045470..2f871fc 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs @@ -217,6 +217,7 @@ view V { var member = Assert.Single(viewNode.ExposeMembers); Assert.Equal("P", member.QualifiedName); Assert.Equal("@Safety", member.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.NamespaceRecursive, member.RecursionKind); } finally { @@ -270,10 +271,12 @@ view V { var chassisMember = viewNode.ExposeMembers[0]; Assert.Equal("Chassis", chassisMember.QualifiedName); Assert.Null(chassisMember.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.MembershipExact, chassisMember.RecursionKind); var engineMember = viewNode.ExposeMembers[1]; Assert.Equal("Engine", engineMember.QualifiedName); Assert.Equal("@Safety", engineMember.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.NamespaceRecursive, engineMember.RecursionKind); // GetExposedNames() remains the flat qualified-name projection consumed by // ReferenceResolver, unaffected by which entries carry a bracket filter. @@ -284,4 +287,165 @@ view V { File.Delete(tempFile); } } + + /// + /// A bare expose X; (MembershipExpose, non-recursive) captures + /// — only X itself is in scope, + /// not its containment subtree. + /// + [Fact] + public async Task AstBuilder_ExposeBareMembership_CapturesMembershipExact() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync( + tempFile, + """ + package P { + part def Engine; + + view V { + expose Engine; + } + } + """, + TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + Assert.True(result.Workspace!.Declarations.TryGetValue("P::V", out var view)); + var viewNode = Assert.IsType(view); + var member = Assert.Single(viewNode.ExposeMembers); + Assert.Equal("Engine", member.QualifiedName); + Assert.Null(member.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.MembershipExact, member.RecursionKind); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A recursive expose X::**; (MembershipExpose, recursive) captures + /// X and its entire + /// containment subtree are in scope. + /// + [Fact] + public async Task AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync( + tempFile, + """ + package P { + part def Engine; + + view V { + expose Engine::**; + } + } + """, + TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + Assert.True(result.Workspace!.Declarations.TryGetValue("P::V", out var view)); + var viewNode = Assert.IsType(view); + var member = Assert.Single(viewNode.ExposeMembers); + Assert.Equal("Engine", member.QualifiedName); + Assert.Null(member.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.MembershipRecursive, member.RecursionKind); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A non-recursive namespace expose X::*; (NamespaceExpose, non-recursive) captures + /// — only X's direct + /// children are in scope, not X itself and not deeper descendants. + /// + [Fact] + public async Task AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync( + tempFile, + """ + package P { + view V { + expose P::*; + } + } + """, + TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + Assert.True(result.Workspace!.Declarations.TryGetValue("P::V", out var view)); + var viewNode = Assert.IsType(view); + var member = Assert.Single(viewNode.ExposeMembers); + Assert.Equal("P", member.QualifiedName); + Assert.Null(member.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.NamespaceDirectChildren, member.RecursionKind); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A recursive namespace expose X::*::**; (NamespaceExpose, recursive) captures + /// X and its entire + /// containment subtree are in scope. + /// + [Fact] + public async Task AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync( + tempFile, + """ + package P { + view V { + expose P::*::**; + } + } + """, + TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + Assert.True(result.Workspace!.Declarations.TryGetValue("P::V", out var view)); + var viewNode = Assert.IsType(view); + var member = Assert.Single(viewNode.ExposeMembers); + Assert.Equal("P", member.QualifiedName); + Assert.Null(member.BracketFilterExpressionText); + Assert.Equal(ExposeRecursionKind.NamespaceRecursive, member.RecursionKind); + } + finally + { + File.Delete(tempFile); + } + } } + diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index edb44c7..cd87186 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -3869,6 +3869,7 @@ public async Task WorkspaceLoader_LoadAsync_OmgSafetyFeatureViewsFixture_Resolve e.TargetQualifiedName == "'11b-Safety and Security Feaure Views'::PartsTree::vehicle"); var member = Assert.Single(view.ExposeMembers); Assert.Equal("@Safety and (as Safety).isMandatory", member.BracketFilterExpressionText); + Assert.Equal(DemaConsulting.SysML2Tools.Semantic.Model.ExposeRecursionKind.NamespaceRecursive, member.RecursionKind); } /// From 6c81ca094a90a88566c77447fb88a8f26532d461 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 17:33:33 -0400 Subject: [PATCH 02/14] docs(gallery): regenerate gallery SVG/PNG assets for Rendering 0.1.0-beta.7 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/gallery/png/AvionicsBrowserView.png | Bin 49527 -> 49465 bytes docs/gallery/png/BatterySubsystemView.png | Bin 18074 -> 18096 bytes docs/gallery/png/CarLineageGridView.png | Bin 5984 -> 5974 bytes .../png/ComputerSystemInterconnectionView.png | Bin 17290 -> 17975 bytes .../png/CoreLinkInterconnectionView.png | Bin 6506 -> 6739 bytes docs/gallery/png/DroneGeneralView.png | Bin 148835 -> 148866 bytes docs/gallery/png/OAuthSequenceView.png | Bin 15817 -> 16165 bytes docs/gallery/png/TaxonomyMatrixView.png | Bin 21288 -> 21259 bytes .../gallery/png/TokenExchangeSequenceView.png | Bin 6813 -> 7004 bytes .../png/WorkstationInterconnectionView.png | Bin 25727 -> 26930 bytes docs/gallery/svg/BatterySubsystemView.svg | 1 - .../svg/ComputerSystemInterconnectionView.svg | 54 +++++------ .../svg/CoreLinkInterconnectionView.svg | 14 +-- docs/gallery/svg/DroneGeneralView.svg | 1 - .../svg/WorkstationInterconnectionView.svg | 90 +++++++++--------- 15 files changed, 79 insertions(+), 81 deletions(-) diff --git a/docs/gallery/png/AvionicsBrowserView.png b/docs/gallery/png/AvionicsBrowserView.png index 91b6b1c1a84d5a51f87ec7de7a3d979068fbe4dc..628a415a9b9f2115438f7079137dcc965d4fe30b 100644 GIT binary patch literal 49465 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 At^fc4 literal 49527 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 Ac>n+a diff --git a/docs/gallery/png/BatterySubsystemView.png b/docs/gallery/png/BatterySubsystemView.png index 7a6a38c77072cea447f4c126376c0929b15b4493..0f6218fe9aa402367766337895463ff201128a9d 100644 GIT binary patch literal 18096 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 F{{fTMNQeLc diff --git a/docs/gallery/png/ComputerSystemInterconnectionView.png b/docs/gallery/png/ComputerSystemInterconnectionView.png index f45e52bb1826ad08b3e10cede87feff72bad45e4..992c783680c6dd347a5f2fa63d89b2de87c3114b 100644 GIT binary patch literal 17975 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∋?;IJyE4?D(q@z1N+U`A=#gmx%&M9T~ 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 literal 17290 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 private static (LayoutGraph Graph, List Truncated) BuildGraph( IReadOnlyList<(string Package, List Items)> groups, + IReadOnlyDictionary> childrenByParent, IReadOnlyList modelEdges, Theme theme, - int depthLimit) + int depthLimit, + bool isScoped) { var graph = new LayoutGraph(); @@ -1068,12 +1118,14 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( // Folder contents sit at depth 1; truncate them when the depth limit forbids that level. var truncateFolderContents = depthLimit > 0 && depthLimit <= 1; - // Every located definition's node and owning package, so edges can be resolved and scoped. + // Every located definition's node and owning container scope key, so edges can be resolved + // and scoped. var located = new Dictionary(StringComparer.Ordinal); - // Each non-truncated package folder's own child scope, keyed by package name, so intra-package - // edges can be added there instead of at the root. - var folderScopes = new Dictionary(StringComparer.Ordinal); + // Each non-truncated container's own child scope, keyed by its scope key (a package name, or + // an admitted definition's own qualified name when it owns nested definitions), so + // same-container edges can be added there instead of at the root. + var containerScopes = new Dictionary(StringComparer.Ordinal); for (var g = 0; g < groups.Count; g++) { @@ -1082,12 +1134,25 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( if (!isFolder) { - // Top-level (unpackaged) definitions become plain leaves directly on the root graph. + // Top-level (unpackaged) items: plain leaves directly on the root graph, recursing + // through PlaceDef so any of these items' own nested-definition children are still + // handled. foreach (var def in items) { - var leaf = MakeDefNode(graph, def); - located[def.QualifiedName] = new Location(leaf, package); - AddAnnotationNote(graph, def, leaf, theme); + PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme); + } + + continue; + } + + if (isScoped) + { + // The view is narrowed by a resolved expose scope: a bare package is never rendered + // as a wrapping folder purely because it is an ancestor of admitted content — promote + // its items directly to the root graph instead. + foreach (var def in items) + { + PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme); } continue; @@ -1118,19 +1183,17 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( folder.Keyword = "package"; folder.TitleHeight = folderTitleHeight; folder.Set(CoreOptions.Algorithm, LayeredLayoutAlgorithm.AlgorithmId); - folderScopes[package] = folder.Children; + containerScopes[package] = folder.Children; foreach (var def in items) { - var leaf = MakeDefNode(folder.Children, def); - located[def.QualifiedName] = new Location(leaf, package); - AddAnnotationNote(folder.Children, def, leaf, theme); + PlaceDef(def, folder.Children, package, childrenByParent, located, containerScopes, theme); } } // Add every model edge whose endpoints both received a node, scoped per the - // lowest-common-ancestor rule: same non-empty package → that folder's own scope; otherwise the - // root graph, referencing the (possibly nested) endpoint nodes directly. + // lowest-common-ancestor rule: same non-empty container scope key → that container's own + // scope; otherwise the root graph, referencing the (possibly nested) endpoint nodes directly. var edgeId = 0; foreach (var edge in modelEdges) { @@ -1142,8 +1205,8 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( var scope = source.Package.Length > 0 && source.Package == target.Package && - folderScopes.TryGetValue(source.Package, out var folderScope) - ? folderScope + containerScopes.TryGetValue(source.Package, out var containerScope) + ? containerScope : graph; var added = scope.AddEdge($"e{edgeId++}", source.Node, target.Node); @@ -1155,6 +1218,44 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( return (graph, truncated); } + /// + /// Places a single definition's node into , then recursively + /// places every nested definition that owns as its immediate parent (see + /// ) inside 's own node's + /// scope — to any nesting depth, one level per recursive + /// call — instead of rendering the parent definition a second time as a duplicate sibling + /// folder. When owns nested definitions, its node's + /// is set to its own already-computed + /// (which already includes the title band and every compartment row + /// for its own owned usages), reserving exactly that band above where its nested children begin, + /// and its scope key is registered in so intra-container edges + /// between its own nested children resolve to its scope rather than the root. + /// + private static void PlaceDef( + DefBox def, + LayoutGraph targetScope, + string packageKeyForEdgeScoping, + IReadOnlyDictionary> childrenByParent, + Dictionary located, + Dictionary containerScopes, + Theme theme) + { + var node = MakeDefNode(targetScope, def); + located[def.QualifiedName] = new Location(node, packageKeyForEdgeScoping); + AddAnnotationNote(targetScope, def, node, theme); + + if (childrenByParent.TryGetValue(def.QualifiedName, out var children)) + { + node.TitleHeight = def.Height; + containerScopes[def.QualifiedName] = node.Children; + + foreach (var child in children) + { + PlaceDef(child, node.Children, def.QualifiedName, childrenByParent, located, containerScopes, theme); + } + } + } + /// Creates a definition leaf node in the given scope, carrying its keyword and compartments. private static LayoutGraphNode MakeDefNode(LayoutGraph scope, DefBox def) { diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index 0b092b7..d2f92c1 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -167,6 +167,237 @@ public void GeneralViewLayoutStrategy_BuildLayout_PackagedDefinitions_ProducesFo Assert.Equal("Sys", folder.Label); } + /// + /// Builds a workspace where Sys::OperatorConsole owns two nested definitions, + /// Sys::OperatorConsole::DisplayPanel and Sys::OperatorConsole::CommsHandset, + /// all inside package Sys — the fixture reproducing the real mission-control + /// gallery model's duplicate-box defect (Defect A). + /// + private static SysmlWorkspace BuildNestedDefinitionWorkspace() => new() + { + Declarations = new Dictionary + { + ["Sys::OperatorConsole"] = new SysmlDefinitionNode + { + Name = "OperatorConsole", + QualifiedName = "Sys::OperatorConsole", + DefinitionKeyword = "part def" + }, + ["Sys::OperatorConsole::DisplayPanel"] = new SysmlDefinitionNode + { + Name = "DisplayPanel", + QualifiedName = "Sys::OperatorConsole::DisplayPanel", + DefinitionKeyword = "part def" + }, + ["Sys::OperatorConsole::CommsHandset"] = new SysmlDefinitionNode + { + Name = "CommsHandset", + QualifiedName = "Sys::OperatorConsole::CommsHandset", + DefinitionKeyword = "part def" + } + } + }; + + /// + /// Unscoped: a definition that owns nested definitions renders exactly one box for + /// itself (regression guard against the sibling-duplicate-folder defect) with its nested + /// definitions placed as children of that single box, nested inside the still-present + /// package folder — not as a duplicate sibling folder. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_DefinitionOwningNestedDefinitions_RendersOneContainerBoxUnscoped() + { + // Arrange: OperatorConsole owns DisplayPanel and CommsHandset, all inside package Sys, unscoped + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildNestedDefinitionWorkspace(); + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: exactly one OperatorConsole box exists + var allBoxes = CollectBoxes(layout.Nodes); + var consoleBoxes = allBoxes.Where(b => b.Label == "OperatorConsole").ToList(); + Assert.Single(consoleBoxes); + + // Assert: the single OperatorConsole box's own children include DisplayPanel and CommsHandset + var consoleChildLabels = CollectBoxes(consoleBoxes[0].Children).Select(b => b.Label).ToList(); + Assert.Contains("DisplayPanel", consoleChildLabels); + Assert.Contains("CommsHandset", consoleChildLabels); + + // Assert: the Sys package folder still exists (unscoped preserves package folders), and its + // own direct children include the single OperatorConsole box, not DisplayPanel/CommsHandset + // directly. + var folder = allBoxes.First(b => b.Shape == BoxShape.Folder && b.Label == "Sys"); + var folderChildLabels = folder.Children.OfType().Select(b => b.Label).ToList(); + Assert.Contains("OperatorConsole", folderChildLabels); + Assert.DoesNotContain("DisplayPanel", folderChildLabels); + Assert.DoesNotContain("CommsHandset", folderChildLabels); + } + + /// + /// Scoped: the same nested-definition-owning fixture, but with a view exposing + /// Sys::OperatorConsole::** (whole-subtree recursion). Still renders exactly one + /// OperatorConsole box (no duplicate), its children still nested correctly, and — + /// combined with Defect B's bare-package folder suppression — no Sys folder appears + /// at all (its only admitted content's immediate parent, OperatorConsole, is itself + /// an admitted definition, so OperatorConsole is promoted directly to root). + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_DefinitionOwningNestedDefinitions_RendersOneContainerBoxScoped() + { + // Arrange: same workspace as the unscoped test, but exposing Sys::OperatorConsole::** + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildNestedDefinitionWorkspace(); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Sys::V", + ExposeMembers = [new ExposeMember("OperatorConsole", null, ExposeRecursionKind.MembershipRecursive)], + ResolvedEdges = [new SysmlEdge("Sys::V", "Sys::OperatorConsole", SysmlEdgeKind.Expose)] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: exactly one OperatorConsole box exists, with DisplayPanel/CommsHandset nested + // as its own children. + var allBoxes = CollectBoxes(layout.Nodes); + var consoleBoxes = allBoxes.Where(b => b.Label == "OperatorConsole").ToList(); + Assert.Single(consoleBoxes); + var consoleChildLabels = CollectBoxes(consoleBoxes[0].Children).Select(b => b.Label).ToList(); + Assert.Contains("DisplayPanel", consoleChildLabels); + Assert.Contains("CommsHandset", consoleChildLabels); + + // Assert: no Sys folder appears — OperatorConsole is promoted directly to root instead of + // being wrapped in a folder for its bare-package ancestor. + Assert.DoesNotContain(allBoxes, b => b.Shape == BoxShape.Folder); + } + + /// + /// Scoped, isolating Defect B alone (no nested-definition-owning case involved): a view + /// exposing Sys::* (direct-children recursion) over a bare package Sys + /// containing plain sibling definitions renders no box + /// anywhere, while the exposed definitions' boxes are present directly at the root. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_ExposedNamespaceChildren_BarePackageAncestor_NoFolderRendered() + { + // Arrange: bare package Sys with two sibling definitions, exposing Sys::* (direct children) + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Sys::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Sys::A", DefinitionKeyword = "part def" }, + ["Sys::B"] = new SysmlDefinitionNode { Name = "B", QualifiedName = "Sys::B", DefinitionKeyword = "part def" } + } + }; + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Sys::V", + ExposeMembers = [new ExposeMember("Sys", null, ExposeRecursionKind.NamespaceDirectChildren)], + ResolvedEdges = [new SysmlEdge("Sys::V", "Sys", SysmlEdgeKind.Expose)] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: no folder-shaped box exists anywhere in the result + var allBoxes = CollectBoxes(layout.Nodes); + Assert.DoesNotContain(allBoxes, b => b.Shape == BoxShape.Folder); + + // Assert: the exposed definitions' boxes are present directly under the root node list + var rootLabels = CollectBoxes(layout.Nodes).Select(b => b.Label).ToList(); + Assert.Contains("A", rootLabels); + Assert.Contains("B", rootLabels); + } + + /// + /// Explicit regression guard: with no expose/no , an + /// ordinary bare-package case still renders a box with the + /// expected package label and expected non-definition-container children, confirming + /// unscoped behavior is provably unchanged. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_Unscoped_StillRendersFullPackageFolderStructure() + { + // Arrange: two sibling definitions inside package Sys, unscoped + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Sys::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Sys::A", DefinitionKeyword = "part def" }, + ["Sys::B"] = new SysmlDefinitionNode { Name = "B", QualifiedName = "Sys::B", DefinitionKeyword = "part def" } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: a Sys folder box still exists, with A and B nested directly as its own children + var allBoxes = CollectBoxes(layout.Nodes); + var folder = allBoxes.First(b => b.Shape == BoxShape.Folder); + Assert.Equal("package", folder.Keyword); + Assert.Equal("Sys", folder.Label); + var folderChildLabels = CollectBoxes(folder.Children).Select(b => b.Label).ToList(); + Assert.Contains("A", folderChildLabels); + Assert.Contains("B", folderChildLabels); + } + + /// + /// Mirrors the real BatterySubsystemView gallery scenario directly: a single + /// part def Battery inside bare package QuadcopterDrone, exposed via + /// expose Battery; (exact match, single-target expose). No + /// box exists in the result, and the Battery box is present directly at the root + /// level — the primary Defect B correctness guard. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_ExposedDefinitionInsideBarePackage_NoAncestorFolderRendered() + { + // Arrange: Battery inside bare package QuadcopterDrone, exposing Battery exactly + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["QuadcopterDrone::Battery"] = new SysmlDefinitionNode + { + Name = "Battery", + QualifiedName = "QuadcopterDrone::Battery", + DefinitionKeyword = "part def" + } + } + }; + var viewNode = new SysmlViewNode + { + Name = "BatterySubsystemView", + QualifiedName = "QuadcopterDrone::BatterySubsystemView", + ExposeMembers = [new ExposeMember("Battery", null, ExposeRecursionKind.MembershipExact)], + ResolvedEdges = [new SysmlEdge("QuadcopterDrone::BatterySubsystemView", "QuadcopterDrone::Battery", SysmlEdgeKind.Expose)] + }; + var context = new ViewContext("v", workspace, viewNode); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: no folder-shaped box exists, and Battery is present directly at the root level + var allBoxes = CollectBoxes(layout.Nodes); + Assert.DoesNotContain(allBoxes, b => b.Shape == BoxShape.Folder); + var rootLabels = layout.Nodes.OfType().Select(b => b.Label).ToList(); + Assert.Contains("Battery", rootLabels); + } + /// /// BuildLayout draws a specialization edge (a ) between a subtype /// and its supertype when both are present in the workspace. From f9bbf734a0deca937b5705c78ef2e84fd393b013 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 19:53:18 -0400 Subject: [PATCH 04/14] docs(gallery): correct RadioNetwork description in GroundSegmentRecursiveView section RadioNetwork is a bare package, not a definition, so it is never itself rendered as a box - only its Uplink/Downlink members are in scope and render as flat top-level boxes. The prior text incorrectly implied RadioNetwork appears as an element in the diagram. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/gallery/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/gallery/README.md b/docs/gallery/README.md index 3e20887..ca4e320 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -279,8 +279,11 @@ SVG: [`svg/GroundSegmentDirectChildrenView.svg`](svg/GroundSegmentDirectChildren `GroundSegmentRecursiveView` (`expose GroundSegment::*::**;`, NamespaceRecursive) exposes every descendant of `GroundSegment`, recursively — `OperatorConsole`, `DisplayPanel`, -`CommsHandset`, `ThermalRegulator`, `RadioNetwork`, `Uplink`, and `Downlink` are all in -scope — but (unlike MembershipRecursive) `GroundSegment` itself is still excluded. +`CommsHandset`, `ThermalRegulator`, `Uplink`, and `Downlink` all appear. `RadioNetwork` +is a bare `package`, not a definition, so it is never itself rendered as a box; its members +`Uplink`/`Downlink` are in scope and render as flat top-level boxes with no `RadioNetwork` +wrapper. `GroundSegment` (also a bare package) is excluded from scope and, like +`RadioNetwork`, shows no folder either. Model: [`models/10-mission-control-expose-recursion.sysml`](models/10-mission-control-expose-recursion.sysml) (`GroundSegmentRecursiveView`) · SVG: [`svg/GroundSegmentRecursiveView.svg`](svg/GroundSegmentRecursiveView.svg) From b6a77c74a642d3ef86780efc9c2801569519dfe6 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 20:18:47 -0400 Subject: [PATCH 05/14] docs: fix inaccurate doc comments flagged in PR #36 review - ExposeScopeResolver.ResolveExposedScope: clarify that the usage-typing expansion applies to every Subjects entry regardless of recursion kind (not just non-exact-only subjects), and never to ExplicitMembers. - DynamicViewSynthesizer: correct example syntax from 'view def V { expose Target::**; }' to 'view V { expose Target::**; }' - expose is valid in a named view usage body, not a view def. - GeneralViewLayoutStrategy.BuildGraph: correct the edge-scoping comment - it's an exact container-scope-key match, not a genuine lowest-common-ancestor search. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Layout/Internal/ExposeScopeResolver.cs | 10 ++++++---- .../Layout/Internal/GeneralViewLayoutStrategy.cs | 16 ++++++++++------ .../Rendering/Internal/DynamicViewSynthesizer.cs | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs index 7c56ee6..fd529e3 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs @@ -86,10 +86,12 @@ internal static class ExposeScopeResolver /// usage's own containment subtree is typically empty — the real content lives under its /// type's subtree. To avoid silently scoping to nothing, this also resolves the usage's own /// edge (if any) and adds that type's qualified name to the - /// scope as well, so both the usage and its type's subtree are included. This expansion only - /// applies to non-exact-only subjects — a - /// successfully-evaluated bracket filter's already - /// name the exact matched definitions or usages. + /// scope as well, using the same recursion kind for both — so both the usage and its type's + /// subtree are included, with the recursion kind (exact/direct-children/whole-subtree) + /// controlling whether the type's own descendants are also in scope. This expansion applies to + /// every entry regardless of recursion kind — it never + /// applies to , since a successfully-evaluated + /// bracket filter's matches already name the exact matched definitions or usages. /// /// The workspace, used to look up each exposed target's declaration. /// The view's AST node, or null for the synthetic --auto view. diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs index 10e2ea7..73d4dba 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs @@ -1086,9 +1086,11 @@ private static bool TryResolveQualified( /// node placement is known: an edge whose endpoints share the same non-empty container scope /// (a package folder or a definition-as-container) is added to that scope (an intra-container /// edge the layered algorithm can use to order the container's contents); every other edge — - /// including any crossing containers — is added at the root, referencing the descendant nodes - /// directly, per the lowest-common-ancestor edge convention. An edge touching a depth-truncated - /// (unrendered) definition has no node to reference and is dropped, exactly as before. + /// including one whose endpoints sit in different containers, even when one container is nested + /// inside the other — is added at the root, referencing the descendant nodes directly (this is + /// an exact container-scope-key match, not a genuine lowest-common-ancestor search). An edge + /// touching a depth-truncated (unrendered) definition has no node to reference and is dropped, + /// exactly as before. /// private static (LayoutGraph Graph, List Truncated) BuildGraph( IReadOnlyList<(string Package, List Items)> groups, @@ -1191,9 +1193,11 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( } } - // Add every model edge whose endpoints both received a node, scoped per the - // lowest-common-ancestor rule: same non-empty container scope key → that container's own - // scope; otherwise the root graph, referencing the (possibly nested) endpoint nodes directly. + // Add every model edge whose endpoints both received a node: an edge whose endpoints share + // the same non-empty container scope key is scoped to that container's own scope; every + // other edge — including one whose endpoints sit in different containers, even when one + // container is nested inside the other — is added at the root graph instead (an exact + // scope-key match, not a genuine lowest-common-ancestor search). var edgeId = 0; foreach (var edge in modelEdges) { diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs index 28f28fe..5cb210f 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs @@ -19,7 +19,7 @@ namespace DemaConsulting.SysML2Tools.Rendering.Internal; /// cref="SysmlNode.ResolvedEdges"/> with a single edge (plus a /// matching entry using /// ) — the same mechanism a real, -/// parsed view def V { expose Target::**; } produces via ReferenceResolver, so +/// parsed view V { expose Target::**; } usage produces via ReferenceResolver, so /// that a dynamic view shows the requested target's whole containment subtree rather than the /// target alone. reads /// only these two properties and has no notion of provenance, so it treats a synthesized node From f1e21bde30a8c7250195bcf6691d9bb161362219 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 21:11:31 -0400 Subject: [PATCH 06/14] fix: eliminate ExposeMember re-pairing ambiguity, fix NamespaceRecursive subject inclusion Two follow-up fixes from PR #36 review comments: 1. ExposeScopeResolver.ResolveExposedScope previously re-associated each resolved Expose edge with its originating ExposeMember via a fragile forward-scan + EndsWith("::" + raw) string match. When an earlier expose entry failed to resolve but its raw text happened to be a suffix of a later entry's resolved target, the wrong ExposeMember (and therefore the wrong RecursionKind/BracketFilterExpressionText) could be paired with a resolved edge, corrupting the resolved scope. Fixed by having ReferenceResolver iterate SysmlViewNode.ExposeMembers directly and populate a new SysmlViewNode.ResolvedExposeMembers list that pairs each successfully-resolved ExposeMember with its resolved qualified name unambiguously, by object identity, at resolution time. ExposeScopeResolver now consumes this list directly instead of re-deriving the pairing, and the old memberIndex/scan-and-match loop is removed entirely. The existing SysmlEdgeKind.Expose edges are still populated in parallel, unchanged, for their existing consumers (impact analysis / query command reverse-lookup index). 2. IsInSubjectScope shared one switch arm between MembershipRecursive and NamespaceRecursive, both including "qualifiedName == subject.QualifiedName". Per formal-26-03-02.md Section 8.3.26.4, a NamespaceExpose only ever exposes a namespace's Memberships (its members) -- the namespace is never a member of itself, regardless of the recursive flag. NamespaceRecursive now has its own arm matching only descendants at any depth, never the subject itself. This did not previously manifest in the gallery because every current NamespaceRecursive usage targets a bare package, which never gets its own rendered box; confirmed unchanged gallery output after regenerating all SVG/PNG files. Added regression tests: one reproducing the exact reviewer-flagged re-pairing scenario (unresolved "expose A;" followed by resolved "expose X::A;" with different recursion kinds), and one confirming NamespaceRecursive excludes a SysmlDefinitionNode subject itself while including all of its descendants, contrasted with MembershipRecursive on the same fixture. Updated docs/user_guide/introduction.md to correct the NamespaceRecursive description (previously claimed the same whole-subtree behavior as MembershipRecursive, including the subject itself, which was wrong). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/user_guide/introduction.md | 6 +- .../Layout/Internal/ExposeScopeResolver.cs | 56 +++---- .../Semantic/Model/ReferenceResolver.cs | 31 ++-- .../Semantic/Model/SysmlNode.cs | 26 +++- .../ActionFlowViewLayoutStrategyTests.cs | 12 +- .../BrowserAndGridViewLayoutStrategyTests.cs | 14 +- .../Layout/ExposeScopeResolverTests.cs | 144 +++++++++++++++++- .../Layout/GeneralViewLayoutStrategyTests.cs | 10 +- .../InterconnectionViewLayoutStrategyTests.cs | 16 +- .../Layout/SequenceViewLayoutStrategyTests.cs | 14 +- .../StateTransitionViewLayoutStrategyTests.cs | 12 +- .../Layout/TestSysmlViewNodeExtensions.cs | 43 ++++++ 12 files changed, 290 insertions(+), 94 deletions(-) create mode 100644 test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 872f1b3..3684d5d 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -181,8 +181,10 @@ entire workspace: subtree**: `X` plus every declaration whose qualified name is `X` or is contained within it. - `expose X::*;` (bare NamespaceExpose) — scopes to only `X`'s **direct (one-level) children**, not `X` itself and not deeper descendants. - - `expose X::*::**;` (recursive NamespaceExpose) — scopes to `X` **and its entire containment - subtree**, the same whole-subtree behavior as recursive MembershipExpose. + - `expose X::*::**;` (recursive NamespaceExpose) — like `expose X::*;`, still **excludes `X` + itself** (a NamespaceExpose only ever exposes `X`'s Memberships — its members — never `X` as + a member of itself), but additionally includes descendants beyond direct children, at any + depth — unlike `NamespaceDirectChildren`, which stops at one level. If `X` does not resolve to any declaration in the workspace (for example, a typo), the tool falls back to rendering the full workspace for that view — but now also reports a diagnostic diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs index fd529e3..1f69def 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ExposeScopeResolver.cs @@ -98,11 +98,8 @@ internal static class ExposeScopeResolver /// The resolved , or null when no scoping applies. public static ExposedScope? ResolveExposedScope(SysmlWorkspace workspace, SysmlViewNode? viewNode) { - var exposedTargets = viewNode?.ResolvedEdges - .Where(edge => edge.Kind == SysmlEdgeKind.Expose) - .Select(edge => edge.TargetQualifiedName) - .ToList(); - if (exposedTargets is not { Count: > 0 }) + var resolvedExposeMembers = viewNode?.ResolvedExposeMembers; + if (resolvedExposeMembers is not { Count: > 0 }) { return null; } @@ -111,32 +108,15 @@ internal static class ExposeScopeResolver var explicitMembers = new List(); var failures = new List(); - // Re-pair each resolved Expose edge's target with the ExposeMember it originated from, so - // a bracket-filtered entry's expression can be evaluated against its own containment - // subtree only — not the view's other, unrelated expose entries. ReferenceResolver builds - // one edge per successfully-resolved ExposeMember in source order (skipping entries that - // failed to resolve, without emitting an edge for them), so a forward scan that skips - // non-matching entries reliably re-establishes the pairing even when an earlier entry - // failed to resolve. - var members = viewNode!.ExposeMembers; - var memberIndex = 0; - foreach (var target in exposedTargets) + // Each entry directly pairs a resolved qualified name with the specific ExposeMember it + // originated from (populated by ReferenceResolver), so a bracket-filtered entry's + // expression is always evaluated against its own containment subtree only -- not the + // view's other, unrelated expose entries. This avoids re-deriving the pairing later via + // ambiguous raw-text matching against resolved targets. + foreach (var (member, target) in resolvedExposeMembers) { - ExposeMember? member = null; - while (memberIndex < members.Count) - { - var candidate = members[memberIndex]; - memberIndex++; - if (candidate.QualifiedName == target || - target.EndsWith("::" + candidate.QualifiedName, StringComparison.Ordinal)) - { - member = candidate; - break; - } - } - - var bracketFilterText = member?.BracketFilterExpressionText; - var recursionKind = member?.RecursionKind ?? ExposeRecursionKind.MembershipRecursive; + var bracketFilterText = member.BracketFilterExpressionText; + var recursionKind = member.RecursionKind; if (bracketFilterText is not { Length: > 0 }) { AddSubject(workspace, target, recursionKind, subjects); @@ -205,12 +185,16 @@ private static void AddSubject( /// 's per that subject's own /// : /// - /// / - /// : the subject itself or lies within its - /// containment subtree (a "{subject}::" prefix match, reusing the same - /// qualified-name-prefix idiom + /// : the subject + /// itself or lies within its containment subtree (a "{subject}::" prefix match, reusing + /// the same qualified-name-prefix idiom /// already uses for /// stdlib-prefix matching). + /// : lies within the + /// subject's containment subtree at any depth — never the subject itself. Per + /// formal-26-03-02.md §8.3.26.4, a NamespaceExpose exposes the subject's Memberships (its + /// members), and a namespace is never a member of itself, regardless of the recursive + /// flag. /// : the subject itself /// only, exact match. /// : a direct @@ -223,9 +207,11 @@ private static void AddSubject( public static bool IsInSubjectScope(string qualifiedName, ExposedScope scope) => scope.Subjects.Any(subject => subject.Recursion switch { - ExposeRecursionKind.MembershipRecursive or ExposeRecursionKind.NamespaceRecursive => + ExposeRecursionKind.MembershipRecursive => qualifiedName == subject.QualifiedName || qualifiedName.StartsWith(subject.QualifiedName + "::", StringComparison.Ordinal), + ExposeRecursionKind.NamespaceRecursive => + qualifiedName.StartsWith(subject.QualifiedName + "::", StringComparison.Ordinal), ExposeRecursionKind.MembershipExact => qualifiedName == subject.QualifiedName, ExposeRecursionKind.NamespaceDirectChildren => IsDirectChildOf(qualifiedName, subject.QualifiedName), _ => false, diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index 8e96c8d..bce7a87 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -795,30 +795,39 @@ private void ResolveNode( } } - // Views resolve only GetExposedNames() (a view's expose members) into Expose edges; each - // entry is resolved independently, and an unresolved entry produces the Warning diagnostic - // below with no edge. RenderTargetName (a view's render member) names a rendering style or - // format, not a content-scoping subject -- per the SysML v2 grammar it never refers to - // model content, so ReferenceResolver never inspects it: no edge is produced and no - // diagnostic is emitted for it, mirroring how FilterExpressionText (raw source text, not a - // reference) is also intentionally never touched here. + // Views resolve each ExposeMembers entry independently into an Expose edge (unresolved + // entries produce the Warning diagnostic below with no edge) -- iterating ExposeMembers + // directly (rather than the flattened GetExposedNames() projection) lets each resolved + // qualified name be paired unambiguously with the specific ExposeMember object it came + // from, via ResolvedExposeMembers, instead of leaving callers to re-derive that pairing + // later from raw-text matching (which is ambiguous whenever an earlier entry fails to + // resolve but its raw text is a suffix of a later entry's resolved target). RenderTargetName + // (a view's render member) names a rendering style or format, not a content-scoping subject + // -- per the SysML v2 grammar it never refers to model content, so ReferenceResolver never + // inspects it: no edge is produced and no diagnostic is emitted for it, mirroring how + // FilterExpressionText (raw source text, not a reference) is also intentionally never + // touched here. if (node is SysmlViewNode view) { - foreach (var exposedName in view.GetExposedNames()) + var resolvedExposeMembers = new List<(ExposeMember Member, string ResolvedQualifiedName)>(); + foreach (var member in view.ExposeMembers) { - if (TryResolve(exposedName, namespaceStack, imports, out var resolvedExposed)) + if (TryResolve(member.QualifiedName, namespaceStack, imports, out var resolvedExposed)) { nodeEdges.Add(new SysmlEdge(node.QualifiedName, resolvedExposed, SysmlEdgeKind.Expose)); + resolvedExposeMembers.Add((member, resolvedExposed)); } - else if (resolvedInFile.Add(exposedName)) + else if (resolvedInFile.Add(member.QualifiedName)) { _diagnostics.Add(new SysmlDiagnostic( filePath, 0, 0, DiagnosticSeverity.Warning, - $"Unresolved reference: '{exposedName}'")); + $"Unresolved reference: '{member.QualifiedName}'")); } } + + view.ResolvedExposeMembers = resolvedExposeMembers; } // Metadata annotations (SysmlMetadataNode, a Children entry of the element it annotates) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 4411cbb..ec52422 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -320,6 +320,26 @@ public sealed class SysmlViewNode : SysmlNode /// The qualified name of each expose member, in source order. public IReadOnlyList GetExposedNames() => ExposeMembers.Select(m => m.QualifiedName).ToList(); + /// + /// Gets each paired directly with its own resolved qualified + /// name, populated by for every ExposeMembers entry + /// that successfully resolves (in the same relative order, skipping entries that fail to + /// resolve without inserting a placeholder). Unlike re-deriving this pairing from + /// 's edges by + /// forward-scanning and loose-matching raw reference text against resolved qualified names + /// (ambiguous when an earlier entry fails to resolve but its raw text happens to be a + /// suffix of a later entry's resolved target), this list unambiguously identifies which + /// specific object produced each resolved qualified name, so + /// ExposeScopeResolver can read each resolved target's own + /// / + /// correctly. Empty when the view has no expose members or none resolved. The + /// edges on are + /// still populated in parallel and retain their existing role (e.g. impact analysis, the + /// query command's reverse-lookup index). + /// + public IReadOnlyList<(ExposeMember Member, string ResolvedQualifiedName)> ResolvedExposeMembers { get; set; } = + Array.Empty<(ExposeMember Member, string ResolvedQualifiedName)>(); + /// /// Gets the raw source text of this view's filter [<expression>]; statement /// (from elementFilterMember().ownedExpression().GetText()), or @@ -401,8 +421,10 @@ public enum ExposeRecursionKind NamespaceDirectChildren, /// - /// NamespaceExpose, recursive: expose X::*::**; — X and its entire containment - /// subtree (the pre-fix, still-correct-for-this-case whole-subtree behavior). + /// NamespaceExpose, recursive: expose X::*::**; — X's descendants at any depth + /// (not just direct children), but never X itself: per formal-26-03-02.md §8.3.26.4, a + /// NamespaceExpose exposes the subject's own Memberships (its members), and a namespace is + /// never a member of itself regardless of the recursive flag. /// NamespaceRecursive, } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs index f04fd15..e34e8a8 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs @@ -459,7 +459,7 @@ public void ActionFlowView_BuildLayout_ExposeNonHeuristicRoot_SelectsExposedRoot QualifiedName = "M::V", ExposeMembers = [new ExposeMember("ProcessB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessB", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -487,7 +487,7 @@ public void ActionFlowView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_Select QualifiedName = "M::V", ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessB::b1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -561,7 +561,7 @@ public void ActionFlowView_BuildLayout_ExposeInnerActionOfNestedDefinition_Selec QualifiedName = "M::V", ExposeMembers = [new ExposeMember("c1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessA::ProcessC::c1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -587,7 +587,7 @@ public void ActionFlowView_BuildLayout_ExposeUnrelatedDefinition_NoRootSelected_ QualifiedName = "M::V", ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -615,7 +615,7 @@ public void ActionFlowView_BuildLayout_ExposeSingleAction_DropsOutOfScopeAction( QualifiedName = "M::V", ExposeMembers = [new ExposeMember("a1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProcessA::a1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -656,7 +656,7 @@ public void ActionFlowView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot( QualifiedName = "M::V", ExposeMembers = [new ExposeMember("myProcess", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myProcess", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs index 03bf20a..8c52f9a 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/BrowserAndGridViewLayoutStrategyTests.cs @@ -144,7 +144,7 @@ public void GridView_BuildLayout_ExposedName_UnionsAdditionalSubtree() QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -197,7 +197,7 @@ public void GridView_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefinitionS QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -229,7 +229,7 @@ public void GridView_BuildLayout_ExposeMultipleTargets_UnionsBothSubtrees() new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose), new SysmlEdge("Root::V", "Root::B", SysmlEdgeKind.Expose) ] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -269,7 +269,7 @@ public void GridView_BuildLayout_ExposeOneSideOfSpecialization_KeepsBothRowAndCo QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("Sub", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A::Sub", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -303,7 +303,7 @@ public void BrowserView_BuildLayout_ExposedName_UnionsAdditionalSubtree() QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -355,7 +355,7 @@ public void BrowserView_BuildLayout_ExposedUsage_ResolvesThroughTypingToDefiniti QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -386,7 +386,7 @@ public void BrowserView_BuildLayout_ExposeMultipleTargets_UnionsBothSubtrees() new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose), new SysmlEdge("Root::V", "Root::B", SysmlEdgeKind.Expose) ] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs index 9d2e37b..27dc537 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/ExposeScopeResolverTests.cs @@ -16,6 +16,17 @@ namespace DemaConsulting.SysML2Tools.Tests.Layout; /// public sealed class ExposeScopeResolverTests { + /// + /// Builds the pairing for test fixtures + /// where each 's raw is + /// already the fully-resolved qualified name (i.e. every member resolves to itself, in + /// order) — the common case for every test in this file except the dedicated re-pairing + /// regression test, which builds its own mismatched pairing explicitly. + /// + private static IReadOnlyList<(ExposeMember Member, string ResolvedQualifiedName)> ResolvedMembers( + params ExposeMember[] members) => + members.Select(m => (m, m.QualifiedName)).ToList(); + /// /// A null ViewNode (the synthetic --auto view) resolves to null scope, /// meaning "no scoping applies" — every non-stdlib element is included. @@ -72,6 +83,7 @@ public void ResolveExposedScope_ExposedDefinition_ReturnsThatQualifiedName() ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -110,6 +122,7 @@ public void ResolveExposedScope_ExposedUsage_AlsoIncludesResolvedTypeTarget() ExposeMembers = [new ExposeMember("Root::myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -313,6 +326,7 @@ public void ResolveExposedScope_TwoExposeEdges_DefinitionAndUsageTarget_UnionsBo new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose) ] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -347,6 +361,7 @@ public void ResolveExposedScope_BracketFilterMetaclassKind_MatchesUsageLevelCand ExposeMembers = [new ExposeMember("Root::Container", "@SysML::PartUsage", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -393,6 +408,7 @@ public void ResolveExposedScope_BracketFilterEvaluatesSuccessfully_NarrowsToMatc ExposeMembers = [new ExposeMember("Root::Container", "@Safety", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -448,6 +464,7 @@ public void ResolveExposedScope_MixedFilteredAndUnfilteredEntries_NarrowsIndepen new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose) ] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -481,6 +498,7 @@ public void ResolveExposedScope_BracketFilterFailsToParse_FallsBackToWholeSubtre ExposeMembers = [new ExposeMember("Root::Container", "x istype Y", ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::Container", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -515,6 +533,7 @@ public void ResolveExposedScope_MembershipExact_ScopeIsExactOnly() ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipExact)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -553,6 +572,7 @@ public void ResolveExposedScope_MembershipExactUsage_TypeFallbackIsAlsoExactOnly ExposeMembers = [new ExposeMember("Root::myVehicle", null, ExposeRecursionKind.MembershipExact)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -585,6 +605,7 @@ public void ResolveExposedScope_MembershipRecursive_ScopeIsWholeSubtree() ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -617,6 +638,7 @@ public void ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnl ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.NamespaceDirectChildren)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); @@ -628,11 +650,12 @@ public void ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnl /// /// A recursive NamespaceRecursive expose entry (expose X::*::**;) resolves to - /// a scope containing X and its entire containment subtree — unchanged whole subtree - /// behavior. + /// a scope containing X's descendants at any depth — but never X itself, per + /// formal-26-03-02.md §8.3.26.4 (a NamespaceExpose exposes the subject's own Memberships, + /// and a namespace is never a member of itself regardless of the recursive flag). /// [Fact] - public void ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree() + public void ResolveExposedScope_NamespaceRecursive_ScopeIsDescendantsOnlyExcludingSubject() { var workspace = new SysmlWorkspace { @@ -650,11 +673,12 @@ public void ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree() ExposeMembers = [new ExposeMember("Root::A", null, ExposeRecursionKind.NamespaceRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child::Grandchild", scope)); } @@ -684,13 +708,123 @@ public void ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive( ExposeMembers = [new ExposeMember("Root::A", "x istype Y", ExposeRecursionKind.NamespaceDirectChildren)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] }; + viewNode.ResolvedExposeMembers = ResolvedMembers(viewNode.ExposeMembers.ToArray()); var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); Assert.NotNull(scope); - Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::A", scope)); Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::A::Child", scope)); Assert.Single(scope.Failures); } + + /// + /// Regression test for the reviewer-flagged re-pairing ambiguity on PR #36: a view with + /// expose A; where A does not resolve (a typo/nonexistent name, so no + /// entry is produced for it) followed by + /// expose X::A; which does resolve, where X::A happens to share its + /// simple name with the unresolved first entry. The two entries also have different + /// s (MembershipExact vs. NamespaceRecursive) + /// so a wrong pairing would be observably different in the resolved scope. Because + /// now populates + /// directly (one entry per successfully-resolved member, correctly skipping the unresolved + /// first entry without leaving a placeholder), the resolved scope correctly reflects + /// X::A's own semantics — not the + /// unresolved first entry's . + /// + [Fact] + public void ResolveExposedScope_EarlierUnresolvedEntrySuffixOfLaterResolvedTarget_PairsCorrectly() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::X"] = new SysmlDefinitionNode { Name = "X", QualifiedName = "Root::X", DefinitionKeyword = "part def" }, + ["Root::X::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Root::X::A", DefinitionKeyword = "part def" }, + ["Root::X::A::Child"] = new SysmlDefinitionNode { Name = "Child", QualifiedName = "Root::X::A::Child", DefinitionKeyword = "part def" } + } + }; + + // "expose A;" never resolves (A does not exist), so ReferenceResolver produces no + // ResolvedExposeMembers entry for it -- only the "expose X::A::**;" entry resolves. + var unresolvedMember = new ExposeMember("A", null, ExposeRecursionKind.MembershipExact); + var resolvedMember = new ExposeMember("X::A", null, ExposeRecursionKind.NamespaceRecursive); + var viewNode = new SysmlViewNode + { + Name = "V", + QualifiedName = "Root::V", + ExposeMembers = [unresolvedMember, resolvedMember], + ResolvedEdges = [new SysmlEdge("Root::V", "Root::X::A", SysmlEdgeKind.Expose)], + ResolvedExposeMembers = [(resolvedMember, "Root::X::A")] + }; + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode); + + Assert.NotNull(scope); + + // If the resolved edge were wrongly paired with the unresolved "A" (MembershipExact) entry, + // "Root::X::A" itself would be in scope but its descendant "Root::X::A::Child" would not. + // The correct pairing (NamespaceRecursive) excludes "Root::X::A" itself but includes its + // descendant at any depth. + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::X::A", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::X::A::Child", scope)); + } + + /// + /// Regression test for Bug 2: a NamespaceRecursive expose entry + /// (expose SomeDefinition::*::**;) targeting a + /// directly (not a bare package) excludes the definition's own box from the resolved scope, + /// while all of its descendants at any depth (not just direct children) are included. + /// Contrasted with an equivalent MembershipRecursive entry + /// (expose SomeDefinition::**;) on the same fixture, which does include the + /// definition itself. + /// + [Fact] + public void ResolveExposedScope_NamespaceRecursiveOnDefinition_ExcludesDefinitionItselfIncludesDescendants() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Root::SomeDefinition"] = new SysmlDefinitionNode { Name = "SomeDefinition", QualifiedName = "Root::SomeDefinition", DefinitionKeyword = "part def" }, + ["Root::SomeDefinition::Nested"] = new SysmlDefinitionNode { Name = "Nested", QualifiedName = "Root::SomeDefinition::Nested", DefinitionKeyword = "part def" }, + ["Root::SomeDefinition::Nested::Deep"] = new SysmlDefinitionNode { Name = "Deep", QualifiedName = "Root::SomeDefinition::Nested::Deep", DefinitionKeyword = "part def" } + } + }; + + var namespaceRecursiveMember = new ExposeMember("Root::SomeDefinition", null, ExposeRecursionKind.NamespaceRecursive); + var namespaceViewNode = new SysmlViewNode + { + Name = "V1", + QualifiedName = "Root::V1", + ExposeMembers = [namespaceRecursiveMember], + ResolvedEdges = [new SysmlEdge("Root::V1", "Root::SomeDefinition", SysmlEdgeKind.Expose)] + }; + namespaceViewNode.ResolvedExposeMembers = ResolvedMembers(namespaceViewNode.ExposeMembers.ToArray()); + + var namespaceScope = ExposeScopeResolver.ResolveExposedScope(workspace, namespaceViewNode); + + Assert.NotNull(namespaceScope); + Assert.False(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition", namespaceScope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition::Nested", namespaceScope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition::Nested::Deep", namespaceScope)); + + var membershipRecursiveMember = new ExposeMember("Root::SomeDefinition", null, ExposeRecursionKind.MembershipRecursive); + var membershipViewNode = new SysmlViewNode + { + Name = "V2", + QualifiedName = "Root::V2", + ExposeMembers = [membershipRecursiveMember], + ResolvedEdges = [new SysmlEdge("Root::V2", "Root::SomeDefinition", SysmlEdgeKind.Expose)] + }; + membershipViewNode.ResolvedExposeMembers = ResolvedMembers(membershipViewNode.ExposeMembers.ToArray()); + + var membershipScope = ExposeScopeResolver.ResolveExposedScope(workspace, membershipViewNode); + + Assert.NotNull(membershipScope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition", membershipScope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition::Nested", membershipScope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("Root::SomeDefinition::Nested::Deep", membershipScope)); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index d2f92c1..1b80983 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -256,7 +256,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_DefinitionOwningNestedDefiniti QualifiedName = "Sys::V", ExposeMembers = [new ExposeMember("OperatorConsole", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Sys::V", "Sys::OperatorConsole", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -302,7 +302,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedNamespaceChildren_BareP QualifiedName = "Sys::V", ExposeMembers = [new ExposeMember("Sys", null, ExposeRecursionKind.NamespaceDirectChildren)], ResolvedEdges = [new SysmlEdge("Sys::V", "Sys", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -384,7 +384,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedDefinitionInsideBarePac QualifiedName = "QuadcopterDrone::BatterySubsystemView", ExposeMembers = [new ExposeMember("Battery", null, ExposeRecursionKind.MembershipExact)], ResolvedEdges = [new SysmlEdge("QuadcopterDrone::BatterySubsystemView", "QuadcopterDrone::Battery", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1294,7 +1294,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedName_UnionsAdditionalSu QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("A", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::A", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1361,7 +1361,7 @@ public void GeneralViewLayoutStrategy_BuildLayout_ExposedUsage_ResolvesThroughTy QualifiedName = "Root::V", ExposeMembers = [new ExposeMember("myVehicle", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("Root::V", "Root::myVehicle", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs index 7b48880..fbf6a48 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/InterconnectionViewLayoutStrategyTests.cs @@ -762,7 +762,7 @@ public void InterconnectionView_BuildLayout_ExposeInnerPartOfNestedDefinition_Se QualifiedName = "M::V", ExposeMembers = [new ExposeMember("c1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysA::SysC::c1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -955,7 +955,7 @@ public void InterconnectionView_BuildLayout_ExposeNonHeuristicRoot_SelectsExpose QualifiedName = "M::V", ExposeMembers = [new ExposeMember("SysB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysB", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1031,7 +1031,7 @@ public void InterconnectionView_BuildLayout_ExposeBothSameDepthSiblings_ScoreBre new SysmlEdge("M::V", "M::AB", SysmlEdgeKind.Expose), new SysmlEdge("M::V", "M::MuchLongerSiblingName", SysmlEdgeKind.Expose) ] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1057,7 +1057,7 @@ public void InterconnectionView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_S QualifiedName = "M::V", ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysB::b1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1088,7 +1088,7 @@ public void InterconnectionView_BuildLayout_ExposeUnrelatedDefinition_NoRootSele QualifiedName = "M::V", ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1113,7 +1113,7 @@ public void InterconnectionView_BuildLayout_ExposeSinglePart_NarrowsToThatPart() QualifiedName = "M::V", ExposeMembers = [new ExposeMember("a1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::SysA::a1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1146,7 +1146,7 @@ public void InterconnectionView_BuildLayout_ExposeMultipleParts_UnionsBothSubtre new SysmlEdge("M::V", "M::SysA::a1", SysmlEdgeKind.Expose), new SysmlEdge("M::V", "M::SysA::a2", SysmlEdgeKind.Expose) ] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -1181,7 +1181,7 @@ public void InterconnectionView_BuildLayout_ExposedUsage_ResolvesThroughTypingTo QualifiedName = "M::V", ExposeMembers = [new ExposeMember("myPart", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myPart", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs index 1e72b66..1a8849c 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/SequenceViewLayoutStrategyTests.cs @@ -271,7 +271,7 @@ public void SequenceView_BuildLayout_ExposeNonHeuristicRoot_SelectsExposedRoot() QualifiedName = "M::V", ExposeMembers = [new ExposeMember("ProtocolB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolB", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -300,7 +300,7 @@ public void SequenceView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_SelectsI QualifiedName = "M::V", ExposeMembers = [new ExposeMember("x", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolB::x", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -374,7 +374,7 @@ public void SequenceView_BuildLayout_ExposeInnerLifelineOfNestedDefinition_Selec QualifiedName = "M::V", ExposeMembers = [new ExposeMember("x", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolA::ProtocolC::x", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -401,7 +401,7 @@ public void SequenceView_BuildLayout_ExposeUnrelatedDefinition_NoRootSelected_Re QualifiedName = "M::V", ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::Unrelated", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -430,7 +430,7 @@ public void SequenceView_BuildLayout_ExposeSingleLifeline_NarrowsLifelines() QualifiedName = "M::V", ExposeMembers = [new ExposeMember("server", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::ProtocolA::server", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -472,7 +472,7 @@ public void SequenceView_BuildLayout_ExposeBothLifelines_UnionsSubtreesKeepsMess new SysmlEdge("M::V", "M::ProtocolA::client", SysmlEdgeKind.Expose), new SysmlEdge("M::V", "M::ProtocolA::server", SysmlEdgeKind.Expose) ] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -508,7 +508,7 @@ public void SequenceView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot() QualifiedName = "M::V", ExposeMembers = [new ExposeMember("myProtocol", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("M::V", "M::myProtocol", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs index b8ec69c..1465df2 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs @@ -315,7 +315,7 @@ public void StateTransitionView_BuildLayout_ExposeNonHeuristicRoot_SelectsExpose QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("MachineB", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineB", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -343,7 +343,7 @@ public void StateTransitionView_BuildLayout_ExposeInnerChildOfNonHeuristicRoot_S QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineB::b1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -416,7 +416,7 @@ public void StateTransitionView_BuildLayout_ExposeInnerStateOfNestedDefinition_S QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("b1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineA::MachineB::b1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -442,7 +442,7 @@ public void StateTransitionView_BuildLayout_ExposeUnrelatedDefinition_NoRootSele QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("Unrelated", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::Unrelated", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -470,7 +470,7 @@ public void StateTransitionView_BuildLayout_ExposeSingleState_DropsIsolatedOutOf QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("s1", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::MachineA::s1", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); @@ -510,7 +510,7 @@ public void StateTransitionView_BuildLayout_ExposedUsage_ResolvesThroughTypingTo QualifiedName = "SM::V", ExposeMembers = [new ExposeMember("myMachine", null, ExposeRecursionKind.MembershipRecursive)], ResolvedEdges = [new SysmlEdge("SM::V", "SM::myMachine", SysmlEdgeKind.Expose)] - }; + }.WithResolvedExposeMembers(); var context = new ViewContext("v", workspace, viewNode); var options = new RenderOptions(Themes.Light); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs new file mode 100644 index 0000000..5fb8698 --- /dev/null +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) DemaConsulting. All rights reserved. +// + +using DemaConsulting.SysML2Tools.Semantic.Model; + +namespace DemaConsulting.SysML2Tools.Tests.Layout; + +/// +/// Test-only helper reconstructing for +/// hand-built test fixtures across this directory that manually set +/// and +/// directly (bypassing ReferenceResolver, which normally populates +/// itself). Pairs each +/// with its corresponding resolved +/// edge target by position, valid only for the simple 1:1 fixtures used throughout these +/// layout-strategy tests (every member resolves, in source order, with no gaps) — the dedicated +/// re-pairing regression test in ExposeScopeResolverTests builds its own mismatched +/// pairing explicitly instead of using this helper. +/// +internal static class TestSysmlViewNodeExtensions +{ + /// + /// Populates 's by + /// zipping its (in order) with its resolved + /// edge targets (in order), and returns the same instance + /// for fluent chaining in object-initializer-style test setup. + /// + /// The view node to populate. + /// The same instance, for chaining. + public static SysmlViewNode WithResolvedExposeMembers(this SysmlViewNode view) + { + var targets = view.ResolvedEdges + .Where(edge => edge.Kind == SysmlEdgeKind.Expose) + .Select(edge => edge.TargetQualifiedName); + + view.ResolvedExposeMembers = view.ExposeMembers + .Zip(targets, (member, target) => (Member: member, ResolvedQualifiedName: target)) + .ToList(); + + return view; + } +} From ed3ac9b530207f56155e8f2ed7d4162048b1876f Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 22:11:08 -0400 Subject: [PATCH 07/14] fix: dynamic views now populate ResolvedExposeMembers so scoping works DynamicViewSynthesizer.Synthesize (the render --view-type/--view-target ad-hoc view feature) constructed a SysmlViewNode using only the old ExposeMembers/ResolvedEdges pair. Since the prior round's fix moved ExposeScopeResolver.ResolveExposedScope to consume the new SysmlViewNode.ResolvedExposeMembers field instead, dynamic views were silently falling back to rendering the entire unscoped workspace instead of narrowing to the requested target - a regression. Fix populates ResolvedExposeMembers alongside the existing fields, and adds a regression test that exercises ExposeScopeResolver.ResolveExposedScope end-to-end against a synthesized dynamic view to catch this class of bug in future. Also corrects two companion-artifact inconsistencies surfaced by the same review round: - docs/reqstream expose-scope-resolver.yaml referenced a renamed/nonexistent test, failing CI's reqstream satisfaction check. Split the requirement into a MembershipRecursive one and a new NamespaceRecursive-specific one referencing the correct, currently-existing tests. - docs/verification expose-scope-resolver.md conflated MembershipRecursive and NamespaceRecursive behavior in a bullet and was missing a test-scenario entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/expose-scope-resolver.yaml | 26 ++++++++--- .../layout/internal/expose-scope-resolver.md | 20 ++++++--- .../Internal/DynamicViewSynthesizer.cs | 31 ++++++++----- .../Rendering/DynamicViewSynthesizerTests.cs | 45 +++++++++++++++++++ 4 files changed, 99 insertions(+), 23 deletions(-) diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml index 325a7d9..5c8e4c9 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/expose-scope-resolver.yaml @@ -100,10 +100,10 @@ sections: - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-RecursiveWholeSubtreeUnchanged title: >- ExposeScopeResolver.ResolveExposedScope shall continue to resolve a recursive - MembershipExpose entry (expose X::**;) or recursive NamespaceExpose entry - (expose X::*::**;) to a scope containing X and its entire containment subtree, and a - bracket-filtered entry that fails to parse or evaluate shall continue to fall back to - this same whole-subtree behavior regardless of the entry's own recursion kind. + MembershipExpose entry (expose X::**;) to a scope containing X and its entire + containment subtree, and a bracket-filtered entry that fails to parse or evaluate shall + continue to fall back to this same whole-subtree behavior regardless of the entry's own + recursion kind. justification: | Recursive `expose` forms (`::**`) are the one case where the pre-fix whole-subtree behavior was already correct; this requirement is a regression guard confirming the fix @@ -113,9 +113,25 @@ sections: bracket-filter failure). tests: - ResolveExposedScope_MembershipRecursive_ScopeIsWholeSubtree - - ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree - ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-NamespaceRecursiveExcludesSubject + title: >- + ExposeScopeResolver.ResolveExposedScope shall resolve a recursive NamespaceExpose entry + (expose X::*::**;) to a scope containing X's entire containment subtree excluding X + itself, mirroring the non-recursive NamespaceExpose (expose X::*;) exclusion of X but + extending it to descendants at every depth rather than direct children only. + justification: | + Per the OMG SysML v2 specification, a NamespaceExpose never includes the namespace + being exposed, regardless of whether the recursive (`::**`) or direct-children-only + (`::*`) form is used — only MembershipExpose (`expose X;`/`expose X::**;`) includes the + subject itself. A prior implementation incorrectly treated NamespaceRecursive + identically to MembershipRecursive (including the subject); this requirement guards + against that regression recurring. + tests: + - ResolveExposedScope_NamespaceRecursive_ScopeIsDescendantsOnlyExcludingSubject + - ResolveExposedScope_NamespaceRecursiveOnDefinition_ExcludesDefinitionItselfIncludesDescendants + - id: SysML2Tools-Core-Layout-Internal-ExposeScopeResolver-RootRelevance title: >- ExposeScopeResolver.IsRootRelevantToScope shall return true for a candidate root diff --git a/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md b/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md index aef090a..29c2659 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md +++ b/docs/verification/sysml2-tools-core/layout/internal/expose-scope-resolver.md @@ -38,9 +38,12 @@ configuration are required beyond a standard .NET SDK installation. - A non-recursive `MembershipExact` `expose X;` entry resolves to a scope containing only `X` itself — not its containment subtree — and, for a usage target, adds the resolved type using the same exact-match recursion kind (itself only, not the type's subtree either). -- A recursive `MembershipRecursive`/`NamespaceRecursive` `expose X::**;`/`expose X::*::**;` entry - continues to resolve to a scope containing `X` and its entire containment subtree, unchanged - from before this fix. +- A recursive `MembershipRecursive` `expose X::**;` entry continues to resolve to a scope + containing `X` and its entire containment subtree, unchanged from before this fix. +- A recursive `NamespaceRecursive` `expose X::*::**;` entry resolves to a scope containing `X`'s + entire containment subtree at every depth, but excludes `X` itself — mirroring the + non-recursive `NamespaceDirectChildren` exclusion of `X`, just extended to all descendants + rather than direct children only. - A non-recursive `NamespaceDirectChildren` `expose X::*;` entry resolves to a scope containing only `X`'s direct (one-level) children — not `X` itself and not deeper descendants. - A bracket-filter expression that fails to parse or evaluate always falls back to whichever @@ -112,9 +115,14 @@ configuration are required beyond a standard .NET SDK installation. - `ResolveExposedScope_NamespaceDirectChildren_ScopeIsDirectChildrenOnly`: A non-recursive `NamespaceDirectChildren` entry scopes to only the exposed subject's direct children — not the subject itself and not deeper descendants -- `ResolveExposedScope_NamespaceRecursive_ScopeIsWholeSubtree`: - A recursive `NamespaceRecursive` entry scopes to the exposed subject and its entire containment - subtree — unchanged whole-subtree behavior +- `ResolveExposedScope_NamespaceRecursive_ScopeIsDescendantsOnlyExcludingSubject`: + A recursive `NamespaceRecursive` entry scopes to the exposed subject's entire containment + subtree at every depth, excluding the subject itself +- `ResolveExposedScope_NamespaceRecursiveOnDefinition_ExcludesDefinitionItselfIncludesDescendants`: + A `NamespaceRecursive` entry targeting a definition (not a bare package) excludes the + definition's own box from the resolved scope while including all descendants at any depth, + contrasted with an equivalent `MembershipRecursive` entry on the same fixture which does + include the definition itself - `ResolveExposedScope_BracketFilterFailure_AlwaysFallsBackToRecursive`: A bracket-filter parse failure on an entry classified as `NamespaceDirectChildren` still falls back to whole-subtree (Recursive) inclusion, never the narrower direct-children behavior diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs index 5cb210f..2891880 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DynamicViewSynthesizer.cs @@ -16,17 +16,22 @@ namespace DemaConsulting.SysML2Tools.Rendering.Internal; /// /// /// The synthesized node is scoped to its target by manually populating with a single edge (plus a -/// matching entry using -/// ) — the same mechanism a real, -/// parsed view V { expose Target::**; } usage produces via ReferenceResolver, so -/// that a dynamic view shows the requested target's whole containment subtree rather than the -/// target alone. reads -/// only these two properties and has no notion of provenance, so it treats a synthesized node -/// identically to a parsed one. This differs from , whose node -/// carries no ResolvedEdges at all — that absence is what makes ExposeScopeResolver -/// return a scope (render everything); a dynamic view instead always -/// resolves to a definite, non-null scope rooted at the requested target's whole subtree. +/// cref="SysmlViewNode.ResolvedExposeMembers"/> with a single entry pairing a synthesized +/// member (using +/// ) with the target's own qualified name — +/// the same shape ReferenceResolver produces for a real, parsed +/// view V { expose Target::**; } usage, so that a dynamic view shows the requested +/// target's whole containment subtree rather than the target alone. The matching +/// edge on is also +/// populated in parallel, for parity with a parsed view and for other SysmlEdgeKind.Expose +/// consumers (e.g. impact analysis, the query command's reverse-lookup index), but +/// itself reads only +/// and has no notion of provenance, so it +/// treats a synthesized node identically to a parsed one. This differs from , whose node +/// carries no ResolvedExposeMembers/ResolvedEdges at all — that absence is what +/// makes ExposeScopeResolver return a scope (render everything); a +/// dynamic view instead always resolves to a definite, non-null scope rooted at the requested +/// target's whole subtree. /// /// /// The synthesized view's uses a leading $ — a @@ -132,13 +137,15 @@ internal static (SysmlViewNode? ViewNode, string? Diagnostic) Synthesize( var viewName = "$" + (target.Name ?? targetQualifiedName); + var syntheticExposeMember = new ExposeMember(targetQualifiedName, null, ExposeRecursionKind.MembershipRecursive); var viewNode = new SysmlViewNode { Name = viewName, QualifiedName = viewQualifiedName, RenderTargetName = renderTargetName, - ExposeMembers = [new ExposeMember(targetQualifiedName, null, ExposeRecursionKind.MembershipRecursive)], + ExposeMembers = [syntheticExposeMember], ResolvedEdges = [new SysmlEdge(viewQualifiedName, targetQualifiedName, SysmlEdgeKind.Expose)], + ResolvedExposeMembers = [(syntheticExposeMember, targetQualifiedName)], FilterExpressionText = filterExpressionText, }; diff --git a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs index 3fdfba1..71aa83e 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DynamicViewSynthesizerTests.cs @@ -2,6 +2,7 @@ // Copyright (c) DemaConsulting. All rights reserved. // +using DemaConsulting.SysML2Tools.Layout.Internal; using DemaConsulting.SysML2Tools.Rendering; using DemaConsulting.SysML2Tools.Semantic; using DemaConsulting.SysML2Tools.Semantic.Model; @@ -37,6 +38,50 @@ public void Synthesize_GeneralKind_ResolvableTarget_Succeeds() var edge = Assert.Single(viewNode.ResolvedEdges); Assert.Equal(SysmlEdgeKind.Expose, edge.Kind); Assert.Equal("P::Widget", edge.TargetQualifiedName); + + // ExposeScopeResolver.ResolveExposedScope reads ResolvedExposeMembers (not + // ResolvedEdges/ExposeMembers directly) to resolve a view's scope — this must be + // populated too, or a dynamic view silently renders unscoped (the full workspace) + // instead of narrowed to the requested target. + var resolvedMember = Assert.Single(viewNode.ResolvedExposeMembers); + Assert.Equal("P::Widget", resolvedMember.Member.QualifiedName); + Assert.Equal(ExposeRecursionKind.MembershipRecursive, resolvedMember.Member.RecursionKind); + Assert.Equal("P::Widget", resolvedMember.ResolvedQualifiedName); + } + + /// + /// Regression test: a synthesized dynamic view must actually narrow the rendered scope to + /// the requested target's subtree via , + /// not silently fall back to rendering the entire unscoped workspace. This guards against a + /// previous regression where DynamicViewSynthesizer populated the (now-superseded) + /// / pair but + /// not , which + /// actually reads. + /// + [Fact] + public void Synthesize_ResolveExposedScope_NarrowsToTargetSubtreeOnly() + { + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::Widget"] = new SysmlDefinitionNode { Name = "Widget", QualifiedName = "P::Widget", DefinitionKeyword = "part def" }, + ["P::Widget::Nested"] = new SysmlDefinitionNode { Name = "Nested", QualifiedName = "P::Widget::Nested", DefinitionKeyword = "part def" }, + ["P::Unrelated"] = new SysmlDefinitionNode { Name = "Unrelated", QualifiedName = "P::Unrelated", DefinitionKeyword = "part def" } + } + }; + + var viewNode = DiagramRenderer.SynthesizeDynamicView(workspace, "general", "P::Widget", null, out var diagnostic); + + Assert.Null(diagnostic); + Assert.NotNull(viewNode); + + var scope = ExposeScopeResolver.ResolveExposedScope(workspace, viewNode!); + + Assert.NotNull(scope); + Assert.True(ExposeScopeResolver.IsInSubjectScope("P::Widget", scope)); + Assert.True(ExposeScopeResolver.IsInSubjectScope("P::Widget::Nested", scope)); + Assert.False(ExposeScopeResolver.IsInSubjectScope("P::Unrelated", scope)); } /// A "grid" dynamic view targeting any resolvable non-stdlib definition succeeds. From a03def9fa6648c76bb861c7170fe5e055fe0dffc Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 22:57:21 -0400 Subject: [PATCH 08/14] fix(layout): thread DepthLimit through PlaceDef nested-definition recursion RenderOptions.DepthLimit previously only capped package-folder-contents nesting in GeneralViewLayoutStrategy.BuildGraph (truncateFolderContents); nested-definition containment introduced by PlaceDef's own recursion had no depth limiting at all, so a definition nested inside another definition could render to unlimited depth regardless of the requested DepthLimit. PlaceDef now threads a depth counter (starting at 1, matching the existing folder-contents-is-depth-1 convention) plus depthLimit and a shared truncated-container list through its recursion. When the next nesting level would exceed depthLimit, PlaceDef stops recursing into that definition's nested children and instead places a single '+N more...' ellipsis placeholder leaf inside the definition's own body, mirroring the existing package-folder truncation UX. TruncatedFolder is generalized to TruncatedContainer (adding a TitleOffset field so a folder's own title band and a nested definition's already-reserved title band are decorated correctly), and DecorateTruncatedFolders is generalized into a recursive DecorateTruncated that walks every nested container scope to find and decorate a truncated placeholder at any nesting depth, not just at the root. Added regression tests proving: a DepthLimit that previously had no effect on nested-definition containment now truncates it correctly; DepthLimit == 0 still renders full nested-definition depth unchanged; and the pre-existing package-folder-contents truncation path is unaffected. Updated the GeneralViewLayoutStrategy design doc to describe the new depth-threading scheme and the generalized TruncatedContainer/ DecorateTruncated mechanism. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/general-view-layout-strategy.md | 111 ++++++--- .../Internal/GeneralViewLayoutStrategy.cs | 225 +++++++++++++----- .../Layout/GeneralViewLayoutStrategyTests.cs | 127 ++++++++++ 3 files changed, 371 insertions(+), 92 deletions(-) diff --git a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md index b8c6ad1..1fb8966 100644 --- a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md +++ b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md @@ -33,9 +33,10 @@ own `SysmlNode.Annotations` list, threaded through by `CollectDefinitions` so `A specialization/membership/attribute-typing/redefinition/subsetting/connect/allocate/dependency/ binding relationship expressed by qualified name, together with its target end marker, edge kind, and an optional midpoint label), `Location` (a located definition's -graph node and owning package, used to resolve and scope edges), and `TruncatedFolder` (a -depth-truncated package folder's leaf graph node and hidden-definition count, used to stamp its -ellipsis label onto the placed box after layout). `DroppedRelationshipEdge` (a private record) +graph node and owning package, used to resolve and scope edges), and `TruncatedContainer` (a +depth-truncated package folder's or nested-definition-containing definition's leaf placeholder graph +node, hidden-item count, and vertical title-band offset, used to stamp its ellipsis label onto the +placed box after layout — see `DecorateTruncated` below). `DroppedRelationshipEdge` (a private record) carries the short kind label, raw source/target reference, and human-readable reason for one `Connect`/`Allocate`/`Dependency`/`Binding` edge `BuildModelEdges` could not map to two distinct rendered boxes, feeding `LayoutWarnings.ForDroppedRelationshipEdges`. `FeatureMembership` (a private record) carries @@ -95,9 +96,9 @@ whole graph is placed with one `HierarchicalLayoutAlgorithm().Apply(graph, LayoutOptions.ForAlgorithm("containment"))` call — passing the desired root-scope leaf algorithm through the options parameter (not `graph.Set(CoreOptions.Algorithm, …)`) so a caller going through `LayoutEngine.Layout(graph)` later -is never misled into skipping the hierarchical engine. When any package folder was depth-truncated, -`DecorateTruncatedFolders` stamps each truncated folder's "+N more…" ellipsis label onto its placed -box. Finally, the returned tree's `Warnings` concatenates +is never misled into skipping the hierarchical engine. When any package folder or +nested-definition-containing definition was depth-truncated, `DecorateTruncated` stamps each +truncated container's "+N more…" ellipsis label onto its placed box. Finally, the returned tree's `Warnings` concatenates `LayoutWarnings.ForUnevaluatedFilter` (only when standalone filter parsing/evaluation failed), `LayoutWarnings.ForUnevaluatedExposeBracketFilter(context.ViewName, scope?.Failures ?? [])` (Phase 2a: one warning per bracket-filter expression that failed to parse or evaluate — a @@ -347,9 +348,19 @@ never both apply to the same folder, since the scope-suppression branch already folder's items before the depth check runs), its definitions are never added as individual boxes at all — the folder becomes a single leaf node (its `Children` graph is never touched, so the hierarchical engine keeps this caller-computed ellipsis size rather than auto-sizing it as a -container) sized like the previous ellipsis-indicator formula, and recorded as a `TruncatedFolder` +container) sized like the previous ellipsis-indicator formula, and recorded as a `TruncatedContainer` for later decoration. +Package-folder contents (and the top-level/scoped-promoted items placed directly on the root graph) +sit at depth 1 — the pre-existing convention `truncateFolderContents` (`depthLimit > 0 && depthLimit +<= 1`) already relies on. Every `PlaceDef` call `BuildGraph` makes passes this same `rootDepth == 1` +as `def`'s own starting depth, so `PlaceDef`'s own nested-definition-containment recursion (see +below) goes one depth deeper per level from this same baseline — a definition's immediate nested +child sits at depth 2, its grandchild at depth 3, and so on — regardless of whether that definition +sits directly on the root, inside a folder, or is itself already nested inside another definition. +This is what fixes the reviewer-confirmed defect where `RenderOptions.DepthLimit` only capped +package-folder-contents nesting and had no effect at all on nested-definition containment. + Every located definition's node and owning container scope key ("package", or an admitted definition's own qualified name — see `PlaceDef`) is recorded in a `located` map so model edges can be resolved and scoped: an edge whose endpoints share the same non-empty scope key is added to that @@ -359,27 +370,55 @@ root, referencing the (possibly nested) endpoint nodes directly, per the graph's lowest-common-ancestor edge convention. An edge touching a depth-truncated (unrendered) definition has no node in the `located` map to reference and is silently dropped, exactly as before. -###### `PlaceDef(def, targetScope, packageKeyForEdgeScoping, childrenByParent, located, containerScopes, theme)` +###### `PlaceDef(def, targetScope, packageKeyForEdgeScoping, childrenByParent, located, containerScopes, ...)` Places a single definition's node into `targetScope` via `MakeDefNode`, records its `Location` in `located` (keyed by the container scope it was placed in, for edge scoping), and emits its annotation note via `AddAnnotationNote` — exactly the three steps `BuildGraph` used to perform -inline for every leaf definition, now factored into one recursive helper. When `def` owns nested -definitions (a `childrenByParent` entry keyed by `def.QualifiedName`), the definition's own node -becomes a genuine container in its own right: `node.TitleHeight` is set to `def.Height` (which -already includes the title band and every compartment row for `def`'s own owned usages, from -`ComputeBoxSize`), reserving exactly that band above where its nested children begin, and -`node.Children` is registered in `containerScopes` under `def.QualifiedName` so intra-container -edges between `def`'s own nested children resolve to `def`'s scope rather than the root. Each child -is then placed by a recursive call to `PlaceDef` with `node.Children` as the new target scope and -`def.QualifiedName` as the new edge-scoping key — resolving arbitrarily deep nesting (a definition -owning a definition owning a definition, and so on) through repeated single-level recursion, one -call per level, rather than a multi-hop ancestor walk. `LayoutGraphNode.Children` and `TitleHeight` -are shape-agnostic in `DemaConsulting.Rendering.Layout` (not reserved for `BoxShape.Folder`), so a -`Rectangle`-shaped definition box can safely become a container this way with no engine-side -change: the `HierarchicalLayoutAlgorithm`'s cascading sizing pass grows any container whose placed -content needs more room than its initial `Width`/`Height`, exactly as an (initially `0, 0`-sized) -package folder already relied on. +inline for every leaf definition, now factored into one recursive helper. `depth` is `def`'s own +nesting depth, using the same numbering `BuildGraph` already uses for package-folder contents (a +top-level or folder-scoped definition is depth 1, per `BuildGraph`'s `rootDepth` convention above); +`def`'s own immediate nested children (if any) sit one depth deeper, at `depth + 1`. + +When `def` owns nested definitions (a `childrenByParent` entry keyed by `def.QualifiedName`), the +definition's own node's `TitleHeight` is set to `def.Height` (which already includes the title band +and every compartment row for `def`'s own owned usages, from `ComputeBoxSize`) regardless of what +happens next, reserving exactly that band above where its nested content begins. Then, exactly one +of two things happens to that nested content: + +- If `depthLimit` is positive and does not permit the children's depth (`depthLimit <= depth + 1`), + the children are **not** recursed into or rendered as individual boxes at all — mirroring + `BuildGraph`'s `truncateFolderContents` package-folder-contents truncation one nesting level + deeper. A single ellipsis placeholder leaf node (sized by the same `ComputeEllipsisSize` helper + `BuildGraph`'s folder truncation uses) is added to `def`'s own `node.Children` scope instead, and + recorded in `truncated` with a `TitleOffset` of `0` (unlike a truncated package folder's + placeholder, this ellipsis node is an ordinary child placed inside `def`'s own already-title + reserved `Children` scope, not a standalone box carrying its own title/keyword band, so no further + offset is needed once its absolute position is known). No `containerScopes` entry is registered + for `def.QualifiedName` in this branch (nothing beyond the single ellipsis leaf is ever placed in + its scope, so no intra-container edge could resolve there), and none of the hidden children + receive a `located` entry, so every edge touching one is dropped exactly like a depth-truncated + package folder's contents. +- Otherwise, `node.Children` is registered in `containerScopes` under `def.QualifiedName` so + intra-container edges between `def`'s own nested children resolve to `def`'s scope rather than + the root, and each child is placed by a recursive call to `PlaceDef` with `node.Children` as the + new target scope, `def.QualifiedName` as the new edge-scoping key, and `depth + 1` as the child's + own depth — resolving arbitrarily deep nesting (a definition owning a definition owning a + definition, and so on) through repeated single-level recursion, one call per level, rather than a + multi-hop ancestor walk, with the depth-limit check re-applied at every level. + +`LayoutGraphNode.Children` and `TitleHeight` are shape-agnostic in `DemaConsulting.Rendering.Layout` +(not reserved for `BoxShape.Folder`), so a `Rectangle`-shaped definition box can safely become a +container this way with no engine-side change: the `HierarchicalLayoutAlgorithm`'s cascading sizing +pass grows any container whose placed content needs more room than its initial `Width`/`Height`, +exactly as an (initially `0, 0`-sized) package folder already relied on. + +###### `ComputeEllipsisSize(hiddenCount, theme)` + +Computes the width/height of a "+N more…" ellipsis placeholder leaf from its hidden-item count. +Factored out of the pre-existing package-folder-contents-truncation sizing formula so both +`BuildGraph`'s folder truncation and `PlaceDef`'s nested-definition-containment truncation size +their placeholder identically. ###### `MakeDefNode(scope, def)` @@ -408,18 +447,26 @@ single-pass `BuildGraph`/`CollectDefinitions` traversal that builds each definit deliberate, documented deviation from a literal "post-layout marker decoration" approach (the pattern used by, e.g., `StateTransitionViewLayoutStrategy.AddInitialMarker`, which stamps a marker onto an already-placed box after layout completes). This strategy has no equivalent -post-layout decoration phase — its entire graph, including folder containers and truncated-folder +post-layout decoration phase — its entire graph, including folder containers and truncated placeholders, is built in one `BuildGraph` pass before layout ever runs — so adding the note as an ordinary graph node/edge pair up front is the natural fit, and lets the layout engine route and position it like any other element rather than requiring bespoke placement math. -###### `DecorateTruncatedFolders(tree, graph, truncated, theme)` - -Replaces each truncated folder's placed box with one carrying its "+N more…" ellipsis label, -positioned within the box's now-known absolute placement. Because the leaf algorithm at a -compound-graph scope emits one box per node in `graph.Nodes` order, the boxes portion of the root -`LayoutTree.Nodes` aligns with `graph.Nodes` by index, so each truncated folder's placed box can be -found by index without any auxiliary identifier lookup (`LayoutBox` carries no `Id`). +###### `DecorateTruncated(tree, graph, truncated, theme)` + +Replaces each truncated container's placed box with one carrying its "+N more…" ellipsis label, +positioned within the box's now-known absolute placement, using each `TruncatedContainer`'s own +`TitleOffset` to position the label below any title/keyword band the placeholder box itself +carries. Delegates to a recursive worker overload that walks the tree scope-by-scope: at every +compound-graph scope (the root graph, or any container node's own `Children` graph), the leaf +algorithm emits one box per node in that scope's own `Nodes` order, so a scope's placed boxes align +with that same scope's graph nodes by index (`LayoutBox` carries no `Id` to match by instead). The +worker decorates any box whose matching graph node is a recorded truncation, and otherwise — when +that graph node itself owns children and the placed box has its own nested children — recurses one +level deeper into that box's own `Children` against the graph node's own `Children.Nodes`. This is +what lets a truncated nested-definition-containment placeholder (potentially several levels deep — +inside a folder, inside another definition, or both) be found and decorated exactly like a +root-level truncated package folder, not just the ones directly on the root graph. ##### Error Handling diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs index 73d4dba..eac916e 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs @@ -29,10 +29,13 @@ namespace DemaConsulting.SysML2Tools.Layout.Internal; /// intra-package edges with the bundled layered algorithm (). All /// box sizing (title bands, compartment rows) remains this strategy's responsibility, since the /// layout stage is theme-agnostic. Standard-library declarations are excluded via -/// . Depth-limited (truncated) package contents are never added to the -/// graph as individual boxes — the truncated folder becomes a single leaf node sized like a plain -/// ellipsis indicator, and the "+N more…" label is stamped onto its placed box once the layout is -/// known. +/// . caps nesting depth +/// uniformly regardless of whether the extra nesting comes from a package folder or from a +/// definition containing another definition (nested-definition containment, via +/// ): depth-limited (truncated) content is never added to the graph as +/// individual boxes — the truncated folder or definition body becomes (or gains) a single leaf +/// node sized like a plain ellipsis indicator, and the "+N more…" label is stamped onto its placed +/// box once the layout is known. /// internal sealed class GeneralViewLayoutStrategy : ILayoutStrategy { @@ -173,12 +176,25 @@ private sealed record DefBox( private readonly record struct Location(LayoutGraphNode Node, string Package); /// - /// A package folder that was depth-truncated: replaced by a leaf node sized as an ellipsis - /// indicator, decorated with its "+N more…" label once the layout places it. + /// A package folder or nested-definition container whose contents were depth-truncated: + /// replaced by a leaf ellipsis-indicator node, decorated with its "+N more…" label once the + /// layout places it. Covers both 's package-folder-contents truncation + /// (truncateFolderContents) and 's nested-definition-containment + /// truncation, so caps nesting depth uniformly regardless + /// of whether the extra nesting level comes from a package folder or from a definition + /// containing another definition. /// - /// The leaf graph node standing in for the folder. + /// The leaf graph node standing in for the truncated folder or definition body. /// Number of hidden definitions the ellipsis label reports. - private sealed record TruncatedFolder(LayoutGraphNode Node, int HiddenCount); + /// + /// Vertical offset, in logical pixels, from the placed leaf node's own top edge down to where + /// the ellipsis label should be drawn. A truncated package folder's placeholder node carries its + /// own title/keyword band (it stands in for the whole folder, not just its contents), so its + /// label sits below that reserved band. A truncated definition's ellipsis node is instead an + /// ordinary child placed inside the definition's own already-title-reserved + /// scope, so no further offset is needed (0). + /// + private sealed record TruncatedContainer(LayoutGraphNode Node, int HiddenCount, double TitleOffset); /// /// A // @@ -314,10 +330,12 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) var rootOptions = LayoutOptions.ForAlgorithm(ContainmentLayoutAlgorithm.AlgorithmId); var tree = new HierarchicalLayoutAlgorithm().Apply(graph, rootOptions); - // Stamp the "+N more…" ellipsis label onto each truncated folder's placed box. The leaf - // algorithm emits one box per root node in Nodes order, so the boxes portion of the placed - // tree aligns with graph.Nodes by index. - var placed = truncated.Count == 0 ? tree : DecorateTruncatedFolders(tree, graph, truncated, theme); + // Stamp the "+N more…" ellipsis label onto each truncated folder's or truncated + // definition's placed box. The leaf algorithm emits one box per node in Nodes order at + // every scope (root graph and every container's own Children graph alike), so the boxes + // portion of the placed tree aligns with the corresponding graph's Nodes by index at every + // nesting depth, not just the root. + var placed = truncated.Count == 0 ? tree : DecorateTruncated(tree, graph, truncated, theme); // Surface any standalone `filter [];` evaluation failure, plus a distinct warning // for each `expose ::**[]` bracket filter that failed to parse or evaluate @@ -1090,9 +1108,16 @@ private static bool TryResolveQualified( /// inside the other — is added at the root, referencing the descendant nodes directly (this is /// an exact container-scope-key match, not a genuine lowest-common-ancestor search). An edge /// touching a depth-truncated (unrendered) definition has no node to reference and is dropped, - /// exactly as before. + /// exactly as before. caps nesting depth uniformly whether the + /// extra nesting comes from a package folder or from a definition containing another + /// definition: package-folder contents and top-level/scoped-promoted items placed directly by + /// this method sit at depth 1 (the pre-existing convention — see truncateFolderContents + /// below), and every level of nested-definition containment recurses into + /// is one depth deeper again (a definition's immediate nested children sit at depth 2, its + /// grandchildren at depth 3, and so on) — see 's own remarks for how that + /// deeper truncation is applied. /// - private static (LayoutGraph Graph, List Truncated) BuildGraph( + private static (LayoutGraph Graph, List Truncated) BuildGraph( IReadOnlyList<(string Package, List Items)> groups, IReadOnlyDictionary> childrenByParent, IReadOnlyList modelEdges, @@ -1110,15 +1135,17 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( // explicitly to keep every distinct model relationship visible regardless of the layered // algorithm's own default. graph.Set(CoreOptions.MergeParallelEdges, false); - var truncated = new List(); + var truncated = new List(); // Reserve the full title area (package keyword + name) above a folder's contents so the // label never overlaps the first child box; the renderer draws the smaller tab notch within. var folderTitleHeight = BoxMetrics.TitleAreaHeight(theme, hasLabel: true, hasKeyword: true); - var margin = 2.0 * theme.LabelPadding; - // Folder contents sit at depth 1; truncate them when the depth limit forbids that level. - var truncateFolderContents = depthLimit > 0 && depthLimit <= 1; + // Folder contents (and top-level or scoped-promoted items placed directly here) sit at + // depth 1 - the baseline every PlaceDef call below starts from. Nested-definition + // containment recursion goes one depth deeper per level from there. + const int rootDepth = 1; + var truncateFolderContents = depthLimit > 0 && depthLimit <= rootDepth; // Every located definition's node and owning container scope key, so edges can be resolved // and scoped. @@ -1141,7 +1168,7 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( // handled. foreach (var def in items) { - PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme); + PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme, rootDepth, depthLimit, truncated); } continue; @@ -1154,7 +1181,7 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( // its items directly to the root graph instead. foreach (var def in items) { - PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme); + PlaceDef(def, graph, package, childrenByParent, located, containerScopes, theme, rootDepth, depthLimit, truncated); } continue; @@ -1165,15 +1192,12 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( // Replace the folder's definition boxes with a single leaf ellipsis indicator. It // stays a leaf (its Children graph is never touched) so the hierarchical engine keeps // this caller-computed size rather than auto-sizing it as a container. - var ellipsisWidth = Math.Max( - MinBoxWidth, - (2.0 * margin) + (items.Count.ToString(System.Globalization.CultureInfo.InvariantCulture).Length * 8.0) + 60.0); - var ellipsisHeight = (2.0 * margin) + theme.FontSizeTitle; + var (ellipsisWidth, ellipsisHeight) = ComputeEllipsisSize(items.Count, theme); var placeholder = graph.AddNode($"folder:{package}", ellipsisWidth, folderTitleHeight + ellipsisHeight); placeholder.Label = SimplePackageName(package); placeholder.Shape = BoxShape.Folder; placeholder.Keyword = "package"; - truncated.Add(new TruncatedFolder(placeholder, items.Count)); + truncated.Add(new TruncatedContainer(placeholder, items.Count, folderTitleHeight)); // None of the folder's definitions receive a node: every edge touching one is dropped. continue; @@ -1189,7 +1213,7 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( foreach (var def in items) { - PlaceDef(def, folder.Children, package, childrenByParent, located, containerScopes, theme); + PlaceDef(def, folder.Children, package, childrenByParent, located, containerScopes, theme, rootDepth, depthLimit, truncated); } } @@ -1231,9 +1255,24 @@ private static (LayoutGraph Graph, List Truncated) BuildGraph( /// folder. When owns nested definitions, its node's /// is set to its own already-computed /// (which already includes the title band and every compartment row - /// for its own owned usages), reserving exactly that band above where its nested children begin, - /// and its scope key is registered in so intra-container edges - /// between its own nested children resolve to its scope rather than the root. + /// for its own owned usages), reserving exactly that band above where its nested children begin. + /// is 's own nesting depth, using the same + /// numbering already uses for package-folder contents (a top-level or + /// folder-scoped definition is depth 1); nested-definition containment is one depth deeper per + /// recursive call, so 's immediate nested children sit at + /// depth + 1. When is positive and does not permit that + /// next depth, the children are not recursed into or rendered as full boxes at all — + /// mirroring BuildGraph's truncateFolderContents package-folder-contents + /// truncation one level deeper — and a single "+N more…" ellipsis leaf (recorded in + /// ) is placed inside 's own + /// scope instead, so + /// caps nested-definition containment depth exactly as it already caps package-folder nesting. + /// A truncated definition still registers no entry (nothing + /// is ever placed inside its scope beyond the ellipsis leaf, so no intra-container edge could + /// ever resolve there); an edge touching one of the hidden children has no + /// entry to reference and is dropped, exactly like a depth-truncated package folder's contents. + /// Otherwise, its scope key is registered in so intra-container + /// edges between its own nested children resolve to its scope rather than the root. /// private static void PlaceDef( DefBox def, @@ -1242,22 +1281,58 @@ private static void PlaceDef( IReadOnlyDictionary> childrenByParent, Dictionary located, Dictionary containerScopes, - Theme theme) + Theme theme, + int depth, + int depthLimit, + List truncated) { var node = MakeDefNode(targetScope, def); located[def.QualifiedName] = new Location(node, packageKeyForEdgeScoping); AddAnnotationNote(targetScope, def, node, theme); - if (childrenByParent.TryGetValue(def.QualifiedName, out var children)) + if (!childrenByParent.TryGetValue(def.QualifiedName, out var children)) { - node.TitleHeight = def.Height; - containerScopes[def.QualifiedName] = node.Children; + return; + } - foreach (var child in children) - { - PlaceDef(child, node.Children, def.QualifiedName, childrenByParent, located, containerScopes, theme); - } + node.TitleHeight = def.Height; + var childDepth = depth + 1; + + if (depthLimit > 0 && depthLimit <= childDepth) + { + // The depth limit forbids def's nested children from being rendered as individual boxes + // at all: replace them with a single leaf ellipsis indicator inside def's own Children + // scope, exactly like truncateFolderContents' package-level ellipsis in BuildGraph, one + // nesting level deeper. None of the hidden children receive a node, so every edge + // touching one is dropped. + var (ellipsisWidth, ellipsisHeight) = ComputeEllipsisSize(children.Count, theme); + var placeholder = node.Children.AddNode($"more:{def.QualifiedName}", ellipsisWidth, ellipsisHeight); + truncated.Add(new TruncatedContainer(placeholder, children.Count, TitleOffset: 0.0)); + return; } + + containerScopes[def.QualifiedName] = node.Children; + + foreach (var child in children) + { + PlaceDef(child, node.Children, def.QualifiedName, childrenByParent, located, containerScopes, theme, childDepth, depthLimit, truncated); + } + } + + /// + /// Computes the width/height of a "+N more…" ellipsis placeholder leaf from its hidden-item + /// count, shared by 's package-folder-contents truncation and + /// 's nested-definition-containment truncation so both depth-limiting + /// paths size their placeholder identically. + /// + private static (double Width, double Height) ComputeEllipsisSize(int hiddenCount, Theme theme) + { + var margin = 2.0 * theme.LabelPadding; + var width = Math.Max( + MinBoxWidth, + (2.0 * margin) + (hiddenCount.ToString(System.Globalization.CultureInfo.InvariantCulture).Length * 8.0) + 60.0); + var height = (2.0 * margin) + theme.FontSizeTitle; + return (width, height); } /// Creates a definition leaf node in the given scope, carrying its keyword and compartments. @@ -1277,7 +1352,7 @@ private static LayoutGraphNode MakeDefNode(LayoutGraph scope, DefBox def) /// definition's own node was just added to (folder or root — mirrors the existing per-package /// folder scoping already used for def nodes and model edges). Implemented as an ordinary /// node/edge (rather than a post-layout coordinate-decoration pass - /// like ) so the existing + /// like ) so the existing /// / can position /// and route it automatically — a deliberate departure from the ROADMAP's suggested "adapt /// StateTransitionViewLayoutStrategy.AddInitialMarker's marker-plus-line pattern": that @@ -1347,41 +1422,71 @@ private static (double Width, double Height, IReadOnlyList Lines) Comput } /// - /// Replaces each truncated folder's placed box with one carrying its "+N more…" ellipsis label, - /// positioned within the box's now-known absolute placement. + /// Replaces each truncated folder's or truncated definition's placed box with one carrying its + /// "+N more…" ellipsis label, positioned within the box's now-known absolute placement. Recurses + /// into every placed container box's own against its matching + /// 's , so a truncated + /// definition nested at any depth — inside a folder, inside another definition, or both — is + /// found and decorated exactly like a root-level truncated folder, not just the ones directly on + /// the root graph. /// - private static LayoutTree DecorateTruncatedFolders( + private static LayoutTree DecorateTruncated( LayoutTree tree, LayoutGraph graph, - IReadOnlyList truncated, + IReadOnlyList truncated, Theme theme) { - var hiddenByNode = truncated.ToDictionary(t => t.Node, t => t.HiddenCount); - var folderTitleHeight = BoxMetrics.TitleAreaHeight(theme, hasLabel: true, hasKeyword: true); + var hiddenByNode = truncated.ToDictionary(t => t.Node, t => (t.HiddenCount, t.TitleOffset)); + var nodes = DecorateTruncated(tree.Nodes, graph.Nodes, hiddenByNode, theme); + return tree with { Nodes = nodes }; + } - var nodes = new List(tree.Nodes); - for (var i = 0; i < graph.Nodes.Count && i < nodes.Count; i++) + /// + /// Recursive worker for : + /// walks one graph scope's placed nodes, positionally matched by index against that same scope's + /// (the leaf algorithm at any compound-graph scope, root or + /// nested, emits one box per node in that scope's own Nodes order — + /// carries no Id to match by instead), decorating any box whose matching graph node is a + /// recorded truncation and recursing into every other container box's own children. + /// + private static List DecorateTruncated( + IReadOnlyList nodes, + IReadOnlyList graphNodes, + IReadOnlyDictionary hiddenByNode, + Theme theme) + { + var result = new List(nodes); + for (var i = 0; i < graphNodes.Count && i < result.Count; i++) { - if (!hiddenByNode.TryGetValue(graph.Nodes[i], out var hiddenCount) || - nodes[i] is not LayoutBox box) + if (result[i] is not LayoutBox box) + { + continue; + } + + if (hiddenByNode.TryGetValue(graphNodes[i], out var hidden)) { + var indicator = new LayoutLabel( + X: box.X + theme.LabelPadding, + Y: box.Y + hidden.TitleOffset + theme.LabelPadding + (theme.FontSizeTitle / 2.0), + MaxWidth: box.Width - (2.0 * theme.LabelPadding), + Text: $"+{hidden.HiddenCount} more\u2026", + Align: TextAlign.Center, + Weight: FontWeight.Regular, + Style: FontStyle.Normal, + FontSize: theme.FontSizeTitle); + + result[i] = box with { Children = [indicator] }; continue; } - var indicator = new LayoutLabel( - X: box.X + theme.LabelPadding, - Y: box.Y + folderTitleHeight + theme.LabelPadding + (theme.FontSizeTitle / 2.0), - MaxWidth: box.Width - (2.0 * theme.LabelPadding), - Text: $"+{hiddenCount} more\u2026", - Align: TextAlign.Center, - Weight: FontWeight.Regular, - Style: FontStyle.Normal, - FontSize: theme.FontSizeTitle); - - nodes[i] = box with { Children = [indicator] }; + if (graphNodes[i].HasChildren && box.Children.Count > 0) + { + var decoratedChildren = DecorateTruncated(box.Children, graphNodes[i].Children.Nodes, hiddenByNode, theme); + result[i] = box with { Children = decoratedChildren }; + } } - return tree with { Nodes = nodes }; + return result; } /// Returns the last segment of a qualified package name for use as a folder label. diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index 1b80983..fc8cfe7 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -2490,5 +2490,132 @@ public void GeneralViewLayoutStrategy_BuildLayout_EnumDefLiteralValues_ProducesE var box = CollectBoxes(layout.Nodes).First(b => b.Label == "FlightMode"); Assert.Contains(box.Compartments, c => c.Title == "enum values" && c.Rows.Contains("manual") && c.Rows.Contains("auto")); } + + /// + /// Builds a workspace where package Sys contains Sys::Outer, which owns one + /// nested definition Sys::Outer::Inner — a 2-level-deep nested-definition-containment + /// structure (folder contents at depth 1, Outer's own nested child at depth 2), used + /// by the nested-definition-containment truncation + /// regression tests. + /// + private static SysmlWorkspace BuildTwoLevelNestedDefinitionWorkspace() => new() + { + Declarations = new Dictionary + { + ["Sys::Outer"] = new SysmlDefinitionNode + { + Name = "Outer", + QualifiedName = "Sys::Outer", + DefinitionKeyword = "part def" + }, + ["Sys::Outer::Inner"] = new SysmlDefinitionNode + { + Name = "Inner", + QualifiedName = "Sys::Outer::Inner", + DefinitionKeyword = "part def" + } + } + }; + + /// + /// Regression test for the reviewer-confirmed bug: previously + /// only capped package-folder-contents nesting (truncateFolderContents), never + /// nested-definition containment introduced by PlaceDef's own recursion — so a + /// DepthLimit that should cap all nesting had no effect once a definition owned a + /// nested definition. With the fix, a DepthLimit of 2 over the 2-level-deep + /// Outer/Inner nested-definition-containment fixture truncates the innermost + /// level (Inner, Outer's own nested child, at depth 2): Outer's own box + /// is still rendered normally, but in place of an Inner box its own + /// carries a single "+1 more…" ellipsis + /// , mirroring how a depth-truncated package folder's contents are + /// already reported. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_NestedDefinitionContainment_DepthLimitTruncatesInnerLevel() + { + // Arrange: Sys::Outer owns Sys::Outer::Inner, DepthLimit = 2 + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildTwoLevelNestedDefinitionWorkspace(); + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light, DepthLimit: 2); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: Outer's own box still renders, but Inner does not appear as a box anywhere + var allBoxes = CollectBoxes(layout.Nodes); + var outerBox = allBoxes.Single(b => b.Label == "Outer"); + Assert.DoesNotContain(allBoxes, b => b.Label == "Inner"); + + // Assert: Outer's own Children carries a single placeholder box standing in for the + // truncated Inner definition, itself decorated with a "+1 more…" ellipsis label — mirroring + // how a depth-truncated package folder's own placed box is decorated, one nesting level + // deeper. + var placeholder = Assert.IsType(Assert.Single(outerBox.Children)); + var indicator = Assert.IsType(Assert.Single(placeholder.Children)); + Assert.Equal("+1 more\u2026", indicator.Text); + } + + /// + /// Regression guard: == 0 (unlimited) still renders + /// full nested-definition-containment depth unchanged — the fix must not truncate anything + /// when no depth limit is requested. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_NestedDefinitionContainment_DepthLimitZero_RendersFullDepth() + { + // Arrange: same Outer/Inner fixture, but with the default unlimited DepthLimit + var strategy = new GeneralViewLayoutStrategy(); + var workspace = BuildTwoLevelNestedDefinitionWorkspace(); + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: both Outer and Inner render as full boxes, Inner nested as Outer's own child, and + // no ellipsis placeholder label appears anywhere. + var allBoxes = CollectBoxes(layout.Nodes); + var outerBox = allBoxes.Single(b => b.Label == "Outer"); + var innerBox = Assert.IsType(Assert.Single(outerBox.Children)); + Assert.Equal("Inner", innerBox.Label); + } + + /// + /// Regression guard: unrelated to nested-definition containment, a + /// of 1 still truncates a plain package folder's own contents exactly as before the fix + /// (truncateFolderContents in BuildGraph), proving the nested-definition-containment + /// fix did not regress the pre-existing package-folder-contents truncation path. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_PackageFolderContents_DepthLimitOne_StillTruncatesFolder() + { + // Arrange: package Sys with two sibling definitions (no definition-containment nesting), + // DepthLimit = 1 + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["Sys::A"] = new SysmlDefinitionNode { Name = "A", QualifiedName = "Sys::A", DefinitionKeyword = "part def" }, + ["Sys::B"] = new SysmlDefinitionNode { Name = "B", QualifiedName = "Sys::B", DefinitionKeyword = "part def" } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light, DepthLimit: 1); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: the folder is present but neither A nor B renders as an individual box + var allBoxes = CollectBoxes(layout.Nodes); + var folder = allBoxes.Single(b => b.Shape == BoxShape.Folder); + Assert.Equal("Sys", folder.Label); + Assert.DoesNotContain(allBoxes, b => b.Label is "A" or "B"); + + // Assert: the folder carries a "+2 more…" ellipsis label reporting its two hidden definitions + var indicator = Assert.IsType(Assert.Single(folder.Children)); + Assert.Equal("+2 more\u2026", indicator.Text); + } } From 06d773b5d7d2750a9a05d538ba9d925dcfb00d28 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 23:17:30 -0400 Subject: [PATCH 09/14] test: avoid orphan temp files in AstBuilderMetadataTests expose tests Path.GetTempFileName() creates a real temp file on disk; appending '.sysml' to build the actual test path left the original extensionless file behind as an orphan, since only the extended path was deleted in the finally block. Replaced all 10 occurrences with Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + '.sysml'), matching the existing convention used elsewhere in the test suite (GetRandomFileName does not create a file on disk). Also corrected an adjacent doc-comment wording inaccuracy found while in this file: a test summary claimed NamespaceRecursive expose includes the subject itself in scope, contradicting the already-fixed actual semantics (NamespaceRecursive excludes the subject). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Semantic/AstBuilderMetadataTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs index 2f871fc..2a3fe8b 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/AstBuilderMetadataTests.cs @@ -22,7 +22,7 @@ public sealed class AstBuilderMetadataTests [Fact] public async Task AstBuilder_BareMetadataAnnotation_CapturesMetadataNode() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -62,7 +62,7 @@ part def Engine { [Fact] public async Task AstBuilder_MetadataAnnotationWithBooleanAttribute_CapturesLiteralValue() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -107,7 +107,7 @@ part def Engine { [Fact] public async Task AstBuilder_MetadataAnnotation_ResolvesTypeReference() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -149,7 +149,7 @@ part def Engine { [Fact] public async Task AstBuilder_MetadataAnnotation_UnresolvedType_ProducesWarning() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -186,7 +186,7 @@ part def Engine { [Fact] public async Task AstBuilder_ExposeBracketFilter_CapturesRawText() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -235,7 +235,7 @@ view V { [Fact] public async Task AstBuilder_MultipleExposeMembers_OnlyOneBracketed_PairsFilterWithCorrectPath() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -296,7 +296,7 @@ view V { [Fact] public async Task AstBuilder_ExposeBareMembership_CapturesMembershipExact() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -337,7 +337,7 @@ view V { [Fact] public async Task AstBuilder_ExposeRecursiveMembership_CapturesMembershipRecursive() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -378,7 +378,7 @@ view V { [Fact] public async Task AstBuilder_ExposeNamespaceDirectChildren_CapturesNamespaceDirectChildren() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( @@ -411,13 +411,13 @@ view V { /// /// A recursive namespace expose X::*::**; (NamespaceExpose, recursive) captures - /// X and its entire - /// containment subtree are in scope. + /// X's entire containment + /// subtree is in scope, excluding X itself. /// [Fact] public async Task AstBuilder_ExposeNamespaceRecursive_CapturesNamespaceRecursive() { - var tempFile = Path.GetTempFileName() + ".sysml"; + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".sysml"); try { await File.WriteAllTextAsync( From 4242156a0322de69c35ea36512b633329b028eae Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 23:29:12 -0400 Subject: [PATCH 10/14] docs: fix README expose-form code span broken across a line wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdown code spans cannot contain a line break; the inline-code list of expose grammar forms (xpose X;/xpose X::**;/xpose X::*;/ xpose X::*::**;) was split mid-span across two lines, which breaks code-span parsing and renders incorrectly. Rewrapped so each backtick pair stays on a single line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ab5e09..a9776ca 100644 --- a/README.md +++ b/README.md @@ -291,9 +291,9 @@ sysml2tools help [lint|render|query []|export] For every layout strategy (not just the General View), a view's body `expose <...>;` statements now scope the rendered diagram to the union of the exposed entries' resolved scopes — an exact match, direct children only, or the entire containment subtree, depending on each entry's SysML -v2 `expose` grammar form and recursion setting (`expose X;`/`expose X::**;`/`expose -X::*;`/`expose X::*::**;`) — instead of always rendering the full workspace; a view with no -`expose` statement continues to render +v2 `expose` grammar form and recursion setting (`expose X;`/`expose X::**;`/`expose X::*;`/ +`expose X::*::**;`) — instead of always rendering the full workspace; a view with no `expose` +statement continues to render the full workspace. `render asTreeDiagram;` and `render asInterconnectionDiagram;` now select the Browser View and Interconnection View layout strategies respectively, taking precedence over the name/supertype heuristic; other `render` targets (`asElementTable`, `asTextualNotation`, an From 5b87bc0e983f1460f289a5cc9d92a73f0a1a40ef Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 23:39:08 -0400 Subject: [PATCH 11/14] fix: give ExposeMember.RecursionKind a backward-compatible default ExposeMember is a public record; adding RecursionKind as a required third positional parameter removed the previous two-argument constructor, a source-breaking change for any external consumer manually constructing an ExposeMember. Defaulted RecursionKind to ExposeRecursionKind.MembershipRecursive, matching the pre-existing whole-subtree scoping behavior that applied unconditionally before this parameter existed, so the old two-argument call site keeps compiling and behaving identically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Semantic/Model/SysmlNode.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index ec52422..2c57458 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -380,12 +380,16 @@ public sealed class SysmlViewNode : SysmlNode /// Classifies which SysML v2 expose grammar form and recursion setting produced this /// entry (). Consumed by ExposeScopeResolver to decide /// between an exact/direct-children match and a whole-subtree match when resolving this -/// entry's contribution to a view's exposed scope. +/// entry's contribution to a view's exposed scope. Defaults to +/// — the pre-existing whole-subtree +/// scoping behavior — so external code constructing an via the +/// previous two-argument form continues to compile and behave exactly as before this +/// parameter was added. /// public sealed record ExposeMember( string QualifiedName, string? BracketFilterExpressionText, - ExposeRecursionKind RecursionKind); + ExposeRecursionKind RecursionKind = ExposeRecursionKind.MembershipRecursive); /// /// Classifies which SysML v2 expose grammar form and recursion setting produced an From 9152418dbc220b74f0b63de43d876ef6b426c086 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 23:44:57 -0400 Subject: [PATCH 12/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/user_guide/introduction.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 3684d5d..4864aa9 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -340,9 +340,8 @@ Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`, non-recursive (`MembershipExact`). The tool resolves `myVehicle`'s own `Typing` edge to find the definition it is typed by (`Vehicle`), and adds that resolved type to the scope too — using the **same** exact-match (not whole-subtree) recursion kind, since the usage's own expose was itself -non-recursive. Confirmed by hand-rendering this exact fixture: the tree contains only the -`myVehicle` row — neither `Vehicle`'s own `engine`/`wheel` parts, nor `Engine`'s `cylinder`, are -included, because exact-match scoping does not pull in either exposed name's descendants. +Confirmed by hand-rendering this exact fixture: the tree contains the `myVehicle` row and the `Vehicle` row, but neither `Vehicle`'s own `engine`/`wheel` parts nor `Engine`'s `cylinder` are included, +because exact-match scoping does not pull in either exposed name's descendants. To render `myVehicle`'s and `Vehicle`'s full nested structure instead, expose recursively — `expose myVehicle::**;` — which scopes to the union of `myVehicle`'s and `Vehicle`'s entire From 6fd3e8df2b3f75a110a781a25a95bfeec215b18e Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 13 Jul 2026 23:52:17 -0400 Subject: [PATCH 13/14] docs: fix broken sentence and rewrap overlong line in user guide The GitHub Autofix suggestion applied for the usage-expose-scoping example (accepting that it correctly identified the resolved Vehicle type row also being present in the tree, not just myVehicle) dropped 'non-recursive.' mid-sentence when merging two lines, and produced a 199-char line that failed markdownlint MD013. Restored the missing sentence-ending and rewrapped the paragraph under 120 chars, while keeping the corrected content (myVehicle row and Vehicle row are both in scope; only descendant-expansion is suppressed by exact-match scoping). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/user_guide/introduction.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 4864aa9..2593f80 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -340,8 +340,10 @@ Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`, non-recursive (`MembershipExact`). The tool resolves `myVehicle`'s own `Typing` edge to find the definition it is typed by (`Vehicle`), and adds that resolved type to the scope too — using the **same** exact-match (not whole-subtree) recursion kind, since the usage's own expose was itself -Confirmed by hand-rendering this exact fixture: the tree contains the `myVehicle` row and the `Vehicle` row, but neither `Vehicle`'s own `engine`/`wheel` parts nor `Engine`'s `cylinder` are included, -because exact-match scoping does not pull in either exposed name's descendants. +non-recursive. Confirmed by hand-rendering this exact fixture: the tree contains the `myVehicle` +row and the `Vehicle` row, but neither `Vehicle`'s own `engine`/`wheel` parts nor `Engine`'s +`cylinder` are included, because exact-match scoping does not pull in either exposed name's +descendants. To render `myVehicle`'s and `Vehicle`'s full nested structure instead, expose recursively — `expose myVehicle::**;` — which scopes to the union of `myVehicle`'s and `Vehicle`'s entire From ea2cf559fc1225d6e09bc8e62c60c332dafd9275 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 14 Jul 2026 00:24:21 -0400 Subject: [PATCH 14/14] Fix box/graph-node index desync in DecorateTruncated; fail-fast Zip in test helper - GeneralViewLayoutStrategy.DecorateTruncated (recursive worker) previously matched placed LayoutBox entries against a scope's LayoutGraphNode list using a single shared loop index. Since a scope's placed node list can intersperse LayoutLine entries (routed intra-scope edges) among boxes at arbitrary positions, any line encountered before a box permanently shifted the box-to-graph-node pairing, causing truncation/hidden-count decoration to land on the wrong box (or be skipped). Fixed by tracking a separate graphIndex cursor that only advances on actual LayoutBox encounters. - Added a reflection-based regression test invoking the private worker directly with a fixture that places a LayoutLine before two LayoutBox entries, proving the fix (confirmed it fails against the pre-fix code). - TestSysmlViewNodeExtensions.WithResolvedExposeMembers() replaced its silently-truncating Zip() with a fail-fast count validation, since a mismatched ExposeMembers/resolved-Expose-edge count in a test fixture should surface as a clear failure rather than a silently truncated pairing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Internal/GeneralViewLayoutStrategy.cs | 29 ++++--- .../Layout/GeneralViewLayoutStrategyTests.cs | 75 +++++++++++++++++++ .../Layout/TestSysmlViewNodeExtensions.cs | 21 +++++- 3 files changed, 114 insertions(+), 11 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs index eac916e..55d538a 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs @@ -1443,11 +1443,19 @@ private static LayoutTree DecorateTruncated( /// /// Recursive worker for : - /// walks one graph scope's placed nodes, positionally matched by index against that same scope's - /// (the leaf algorithm at any compound-graph scope, root or - /// nested, emits one box per node in that scope's own Nodes order — - /// carries no Id to match by instead), decorating any box whose matching graph node is a - /// recorded truncation and recursing into every other container box's own children. + /// walks one graph scope's placed nodes, matching each encountered (in + /// order) against that same scope's (in order) — the leaf + /// algorithm at any compound-graph scope, root or nested, emits exactly one box per node in + /// that scope's own Nodes order, but may also intersperse + /// entries for that scope's own routed edges among those boxes at arbitrary positions (see + /// e.g. LayeredPlacement's tree.Nodes.OfType<LayoutBox>()/ + /// OfType<LayoutLine>() filtering elsewhere in this codebase) — so the two lists + /// are matched by tracking a separate box-only cursor into + /// (advanced only when a is actually encountered), rather than by a + /// single shared index, which would fall out of sync permanently after the first interspersed line. + /// carries no Id to match by directly, so this relative-order + /// pairing is the only mechanism available. Decorates any box whose matching graph node is a + /// recorded truncation and recurses into every other container box's own children. /// private static List DecorateTruncated( IReadOnlyList nodes, @@ -1456,14 +1464,17 @@ private static List DecorateTruncated( Theme theme) { var result = new List(nodes); - for (var i = 0; i < graphNodes.Count && i < result.Count; i++) + var graphIndex = 0; + for (var i = 0; i < result.Count && graphIndex < graphNodes.Count; i++) { if (result[i] is not LayoutBox box) { continue; } - if (hiddenByNode.TryGetValue(graphNodes[i], out var hidden)) + var graphNode = graphNodes[graphIndex++]; + + if (hiddenByNode.TryGetValue(graphNode, out var hidden)) { var indicator = new LayoutLabel( X: box.X + theme.LabelPadding, @@ -1479,9 +1490,9 @@ private static List DecorateTruncated( continue; } - if (graphNodes[i].HasChildren && box.Children.Count > 0) + if (graphNode.HasChildren && box.Children.Count > 0) { - var decoratedChildren = DecorateTruncated(box.Children, graphNodes[i].Children.Nodes, hiddenByNode, theme); + var decoratedChildren = DecorateTruncated(box.Children, graphNode.Children.Nodes, hiddenByNode, theme); result[i] = box with { Children = decoratedChildren }; } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index fc8cfe7..3c2bd02 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -2,6 +2,7 @@ // Copyright (c) DemaConsulting. All rights reserved. // +using System.Reflection; using DemaConsulting.Rendering; using DemaConsulting.Rendering.Abstractions; using DemaConsulting.SysML2Tools.Layout.Internal; @@ -2617,5 +2618,79 @@ public void GeneralViewLayoutStrategy_BuildLayout_PackageFolderContents_DepthLim var indicator = Assert.IsType(Assert.Single(folder.Children)); Assert.Equal("+2 more\u2026", indicator.Text); } + + /// + /// Regression test (reflection-based, since the recursive worker is private) for the + /// reviewer-confirmed pairing bug in GeneralViewLayoutStrategy's recursive + /// DecorateTruncated worker: it previously matched each placed + /// against a scope's list using a single shared loop index, but + /// a scope's placed node list can intersperse entries (routed + /// intra-scope edges) among its boxes at arbitrary positions — every + /// encountered before a box permanently shifted the box-to-graph-node pairing by one, so the + /// wrong (and therefore the wrong truncation/hidden-count + /// decoration) got matched to each subsequent box. This test builds a minimal fixture with + /// one placed before two entries, only the + /// second of which corresponds to a truncated , and invokes the + /// private recursive worker directly via reflection — there is no public seam to reach it, and + /// reproducing this box/line ordering deterministically through the public + /// BuildLayout/DemaConsulting.Rendering layout-algorithm pipeline is not + /// practical since box/line placement ordering is an internal detail of that external + /// package. With the fix, the leading line no longer perturbs the pairing: the first box + /// (matching the non-truncated node) is left unchanged, and only the second box (matching the + /// truncated node) is decorated with the ellipsis label. + /// + [Fact] + public void GeneralViewLayoutStrategy_DecorateTruncated_LineBeforeBoxes_DoesNotBreakBoxToNodePairing() + { + // Arrange: two graph nodes at this scope — nodeA (kept) and nodeB (truncated, hiding 3 items) + var graph = new LayoutGraph(); + var nodeA = graph.AddNode("A", 100, 60); + var nodeB = graph.AddNode("B", 100, 60); + + // A placed line (an intra-scope routed edge) appears before either box in the placed-node list + var line = new LayoutLine( + Waypoints: [], + SourceEnd: EndMarkerStyle.None, + TargetEnd: EndMarkerStyle.None, + LineStyle: LineStyle.Solid, + MidpointLabel: null); + + var boxA = new LayoutBox( + X: 0, Y: 0, Width: 100, Height: 60, Label: "A", Depth: 1, Shape: BoxShape.Rectangle, + Compartments: [], Children: [], Keyword: null, RoundedCornerRadius: null, + FolderTabWidth: null, FolderTabHeight: null, ContentInsetLeft: 0, ContentInsetRight: 0, + ContentInsetTop: 0, ContentInsetBottom: 0, CenterTitle: false); + var boxB = boxA with { Label = "B" }; + + var placed = new List { line, boxA, boxB }; + var graphNodes = new List { nodeA, nodeB }; + var hiddenByNode = new Dictionary + { + [nodeB] = (3, 0.0) + }; + + var method = typeof(GeneralViewLayoutStrategy) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single(m => m.Name == "DecorateTruncated" && m.GetParameters().Length == 4 + && m.GetParameters()[0].ParameterType == typeof(IReadOnlyList)); + + // Act + var result = (List)method.Invoke( + null, [placed, graphNodes, hiddenByNode, Themes.Light])!; + + // Assert: the leading line is preserved untouched + Assert.Same(line, result[0]); + + // Assert: boxA (paired with the non-truncated nodeA) is unchanged — not decorated + var resultBoxA = Assert.IsType(result[1]); + Assert.Equal("A", resultBoxA.Label); + Assert.Empty(resultBoxA.Children); + + // Assert: boxB (paired with the truncated nodeB) carries the "+3 more…" ellipsis decoration + var resultBoxB = Assert.IsType(result[2]); + Assert.Equal("B", resultBoxB.Label); + var indicatorB = Assert.IsType(Assert.Single(resultBoxB.Children)); + Assert.Equal("+3 more\u2026", indicatorB.Text); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs index 5fb8698..aafbddf 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/TestSysmlViewNodeExtensions.cs @@ -22,17 +22,34 @@ internal static class TestSysmlViewNodeExtensions { /// /// Populates 's by - /// zipping its (in order) with its resolved + /// pairing its (in order) with its resolved /// edge targets (in order), and returns the same instance /// for fluent chaining in object-initializer-style test setup. /// /// The view node to populate. /// The same instance, for chaining. + /// + /// Thrown when the number of does not exactly + /// match the number of resolved edges — this helper's + /// documented 1:1 contract is violated, and silently pairing a mismatched or truncated + /// count via Zip could mask a broken test fixture behind an incorrect pairing rather + /// than failing fast. + /// public static SysmlViewNode WithResolvedExposeMembers(this SysmlViewNode view) { var targets = view.ResolvedEdges .Where(edge => edge.Kind == SysmlEdgeKind.Expose) - .Select(edge => edge.TargetQualifiedName); + .Select(edge => edge.TargetQualifiedName) + .ToList(); + + if (targets.Count != view.ExposeMembers.Count) + { + throw new InvalidOperationException( + $"WithResolvedExposeMembers requires a 1:1 pairing: {view.ExposeMembers.Count} " + + $"ExposeMembers but {targets.Count} resolved Expose edges. Use a mismatched " + + "fixture built by hand (see ExposeScopeResolverTests) instead of this helper if " + + "that is the scenario under test."); + } view.ResolvedExposeMembers = view.ExposeMembers .Zip(targets, (member, target) => (Member: member, ResolvedQualifiedName: target))