From 37452e1891e37303b2ed24ddba131c9b33786a4d Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 29 Jul 2026 21:21:07 -0400 Subject: [PATCH 1/6] feat: upgrade to SysML2Tools 0.2.0-beta.1 and expose connection-aware impact SysML2Tools 0.2.0-beta.1 adds connection-aware impact analysis (PR #54). The upgrade itself is source-compatible - the new API is purely additive - but it changes default impact semantics: 0.1.0 walked every incoming edge, and connector edges are in that set, so connectors were traversed unbounded and misattributed. 0.2.0 restricts the default to resolved reference edges and makes connector traversal opt-in and bounded. That would silently narrow Impact results in the Query dialog for user models containing connect/bind, so expose the opt-in rather than just bumping the version: - Add an Impact-only "Include connections (connect/bind)" checkbox, wired through BuildOptions to QueryOptions.IncludeConnections and gated to the Impact verb so no other verb's option payload changes. Toggling recomputes immediately, matching the dialog's no-explicit-Run contract. - Correct the walk-depth label: blank means unlimited, but only one connector hop once connections are included. - Surface the new per-entry traversal metadata (Depth, Relation, ViaQualifiedName) as results-panel columns shown only when a result actually carries them, so non-traversing verbs render exactly as before. Depth is read from the engine property, never parsed out of the human-readable Detail text, per the upstream API contract. DemaConsulting.Rendering stays at 0.1.0; no transitive churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- docs/design/ots/sysml2-tools.md | 16 +++ .../app-shell-subsystem/query-dialog.md | 63 +++++++-- docs/reqstream/ots/sysml2-tools.yaml | 7 + .../app-shell-subsystem/query-dialog.yaml | 36 ++++- docs/user_guide/getting_started.md | 21 ++- docs/verification/ots/sysml2-tools.md | 6 +- .../app-shell-subsystem/query-dialog.md | 39 +++++- .../AppShellSubsystem/QueryDialogView.axaml | 13 +- .../QueryDialogView.axaml.cs | 28 ++-- .../AppShellSubsystem/QueryDialogViewModel.cs | 77 +++++++++-- .../DemaConsulting.SysML2Workbench.csproj | 6 +- .../QueryDialogViewModelTests.cs | 129 ++++++++++++++++++ test/OtsSoftwareTests/OtsSoftwareTests.csproj | 4 +- test/OtsSoftwareTests/SysML2ToolsTests.cs | 60 ++++++++ 15 files changed, 460 insertions(+), 47 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f53b67b..7a8ba73 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -66,7 +66,7 @@ "rollForward": false }, "demaconsulting.sysml2tools.tool": { - "version": "0.1.0", + "version": "0.2.0-beta.1", "commands": [ "sysml2tools" ], diff --git a/docs/design/ots/sysml2-tools.md b/docs/design/ots/sysml2-tools.md index b9698cb..e2f83b1 100644 --- a/docs/design/ots/sysml2-tools.md +++ b/docs/design/ots/sysml2-tools.md @@ -26,6 +26,13 @@ and UI behavior instead of duplicating language infrastructure. named view usage directly into finished SVG output in one call; the workbench never sees an intermediate layout graph or a public layout strategy registry. +- **Element querying** — `QueryEngine` runs the verb-scoped queries the Query + dialog exposes, including connection-aware impact analysis: an opt-in that + traverses connector (`connect`/`bind`) relationships undirected in addition + to the resolved reference edges the impact walk follows by default. The + library also reports per-entry traversal metadata (depth, the resolved + relation kind that reached the entry, and the far endpoint behind a + connection roll-up) which the dialog surfaces directly. ### Integration Pattern @@ -42,3 +49,12 @@ cannot express), LayoutInvoker instead constructs an ephemeral before rendering. Initialization is limited to constructing the required workspace and render inputs; no separate service process or background daemon is introduced. + +QueryDialog is the one place where the workbench builds a library option +object directly rather than through an adapter unit: it composes a +`QueryOptions` from the dialog's form state and hands it to `QueryEngine`, +gating each verb-specific option (hierarchy direction, impact walk depth, +impact connection-awareness) on the selected verb so no other verb's option +payload is affected. Result entries are projected once into a flattened +row record for display, reading the library's machine-readable traversal +metadata rather than parsing its human-readable detail text. diff --git a/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md b/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md index 86dec54..ceacad9 100644 --- a/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md +++ b/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md @@ -24,16 +24,23 @@ workspace through a single, always-visible adaptive form — there is no box, plus a selectable candidate list used as the target-element selector). - Verb-specific extra controls appear only when relevant: a "Direction" - combo for `Hierarchy`, and a "Walk depth" text box for `Impact`. + combo for `Hierarchy`, and — for `Impact` — a "Walk depth" text box plus + an "Include connections (connect/bind)" checkbox. The walk-depth label + states both meanings of a blank value: unlimited normally, or a single + connector hop when connections are included. - Every interaction — Query Type selection, chip add/remove, search text, - element selection, Direction change, Walk depth text change, or the - Include-standard-library toggle — immediately recomputes and displays the - result synchronously; there is no "Run Query" button. + element selection, Direction change, Walk depth text change, the + Include-connections toggle, or the Include-standard-library toggle — + immediately recomputes and displays the result synchronously; there is no + "Run Query" button. Results feed the same shared results panel: a summary bullet list plus a -`Grid`-based (not `DataGrid`) entries table with columns for Qualified Name, -Kind, Detail, and a Direction column shown only when the current result is a -`dependencies` verb result. There is no toolbar: "Copy as Markdown" and +`DataGrid` entries table whose always-visible columns are Qualified Name, +Kind, and Detail, followed by four optional columns. Direction is shown only +when the current result is a `dependencies` verb result; Depth, Relation, and +Via are data-driven, shown only when at least one displayed row actually +carries that value, so verbs that do not traverse render exactly the same +three-column panel as before. There is no toolbar: "Copy as Markdown" and "Copy as JSON" are right-click `ContextMenu` items on the results panel, wired to `QueryResultRenderer.RenderMarkdown`/`RenderJson` via the same `AvaloniaClipboardService` pattern `DiagramDocumentView` uses for its "Copy @@ -88,6 +95,15 @@ attached to `QueryOptions.Direction` when `SelectedQueryType` is `Hierarchy`. optional walk-depth bound. Only parsed for `Impact`, and only when it parses cleanly as a non-negative integer. +**IncludeConnections**: `bool` — the Impact verb's "Include connections" +checkbox state, requesting that connector (`connect`/`bind`) relationships be +traversed in addition to resolved reference edges. Only attached to +`QueryOptions.IncludeConnections` when `SelectedQueryType` is `Impact`; +defaults to `false`, matching the engine's own default. Interacts with +`WalkDepthText`: a blank walk depth normally means unlimited, but when this +flag is set it instead bounds connector hops along a single traversal path +to one, which the walk-depth label states explicitly. + **CurrentResult**: `QueryResult?` — the most recently produced `QueryResult` (either List's client-built list result or the engine's response), or `null` when an element-scoped Query Type has no selection yet. Feeds the @@ -95,8 +111,10 @@ shared results panel. **CurrentResultRows**: `IReadOnlyList` — a flattened, view-friendly projection of `CurrentResult.Entries` (empty strings replacing -nullable fields, `Direction` mapped to a human-readable label, notes joined -into a tooltip string), bound directly by the results-panel `ItemsControl`. +nullable fields, `Direction` mapped to a human-readable label, traversal +depth, resolved relation kind, and connection roll-up far endpoint flattened +to plain strings, notes joined into a tooltip string), bound directly by the +results-panel `ItemsControl`. **StatusMessage**: `string?` — user-visible error/hint text set on every recoverable failure or prompt (no workspace, no selection yet, engine @@ -131,7 +149,8 @@ the shell's current workspace, then recomputes the result. **RecomputeResult**: Recomputes `CurrentResult`/`CurrentResultRows` for the currently selected `SelectedQueryType`. This is the redesign's entire "no explicit Run gesture" mechanism, invoked automatically by every relevant -property change (Query Type, Hierarchy direction, Walk depth text, and — via +property change (Query Type, Hierarchy direction, Walk depth text, the +Include-connections toggle, and — via `FilterOnly`'s and `Picker`'s `PropertyChanged` subscriptions — `FilterOnly.DisplayedItems`/`Picker.DisplayedItems`/`Picker.SelectedQualifiedName`). @@ -161,7 +180,24 @@ form's verb-specific state. - *Parameters*: `string qualifiedName` — the resolved target's qualified name. - *Returns*: `QueryOptions` — always carries `IncludeStdlib`; attaches `Direction` only for `Hierarchy`; parses `WalkDepth` only for `Impact` and - only when the text parses cleanly. + only when the text parses cleanly; sets `IncludeConnections` only for + `Impact`, so leaving the checkbox ticked and switching Query Type never + changes another verb's option payload. + +**BuildRow**: Projects one engine-produced result entry into the flattened +`QueryResultRow` the results panel binds to. + +- *Parameters*: `QueryResultEntry entry` — the entry to project; must not be + `null`. +- *Returns*: `QueryResultRow` — every field is a non-null string except the + tooltip `Notes`, so the AXAML binds directly without value converters. + Traversal depth is taken from the entry's machine-readable depth value and + is never parsed out of the entry's human-readable detail text; the resolved + relation kind is rendered as its enum member name (for example `Connect`); + the connection roll-up far endpoint is rendered as its qualified name. + Entries carrying none of these leave all three fields empty. +- *Postconditions*: A pure static function with no view-model state, exposed + publicly so tests can assert the projection contract directly. **CopyResultAsMarkdownAsync** / **CopyResultAsJsonAsync**: Copy `CurrentResult` to the clipboard through `QueryResultRenderer` and @@ -185,6 +221,11 @@ behavior, every one of these paths also clears `CurrentResult`/ `CurrentResultRows` so the results panel never shows a stale result left over from a previous Query Type or selection. +The one deliberate exception is `BuildRow`'s null-entry guard: a `null` +result entry is a programming error in the caller, not a user-reachable +condition, so it throws `ArgumentNullException` rather than being reported +through `StatusMessage`. + #### Dependencies - **MainWindowShell** — supplies `CurrentWorkspace` for candidate resolution diff --git a/docs/reqstream/ots/sysml2-tools.yaml b/docs/reqstream/ots/sysml2-tools.yaml index 5795bc5..bca748b 100644 --- a/docs/reqstream/ots/sysml2-tools.yaml +++ b/docs/reqstream/ots/sysml2-tools.yaml @@ -17,3 +17,10 @@ sections: The product depends on the existing SysML2Tools layout strategies to produce diagram structure for rendering. tests: - 'RenderView_GeneratesLayoutGraph' + + - id: 'SysML2Tools-ConnectionAwareImpactAnalysis' + title: 'The SysML2Workbench shall use SysML2Tools to perform impact analysis that optionally traverses connector (connect/bind) relationships in addition to resolved reference edges.' + justification: | + The impact walk now follows resolved reference edges only by default, so the connector reachability the workbench previously obtained implicitly must be requested explicitly; the dependency's opt-in behavior, and the traversal metadata it reports for connector-reached elements, are therefore the evidence target. + tests: + - 'ImpactQuery_IncludeConnections_TraversesConnectorEdges' diff --git a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml index 130b754..b3d5810 100644 --- a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml +++ b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml @@ -38,9 +38,11 @@ sections: QueryVerb values in addition to "List"; selecting one of them resolves the single picker''s selected qualified name against the current workspace, builds a QueryOptions instance carrying the selected Query Type, the qualified name, IncludeStdlib, and verb-specific inputs - (Direction for Hierarchy, WalkDepth for Impact), and dispatches through QueryEngine.Execute + (Direction for Hierarchy, WalkDepth and IncludeConnections for Impact), and dispatches + through QueryEngine.Execute immediately - with no explicit "Run" gesture anywhere in the dialog. Every subsequent change - to the Query Type, the picker''s selection, the Hierarchy direction, or the Impact walk depth + to the Query Type, the picker''s selection, the Hierarchy direction, the Impact walk depth, + or the Impact include-connections option recomputes the displayed result immediately, without stale data from a previous selection or Query Type persisting on screen.' justification: | @@ -54,6 +56,7 @@ sections: - 'QueryDialogViewModel_SwitchingQueryType_FromListToDescribeNoSelection_ShowsSelectPrompt' - 'QueryDialogViewModel_HierarchyDirectionChange_WithSelection_RecomputesImmediately' - 'QueryDialogViewModel_WalkDepthTextChange_WithSelection_RecomputesImmediately' + - 'QueryDialogViewModel_IncludeConnectionsChange_WithSelection_RecomputesImmediately' - 'QueryDialogViewModel_BuildOptions_HierarchyVerb_AttachesDirection' - 'QueryDialogViewModel_BuildOptions_NonHierarchyVerb_OmitsDirection' - 'QueryDialogViewModel_BuildOptions_ImpactVerbWithWalkDepth_ParsesWalkDepth' @@ -62,6 +65,35 @@ sections: - 'QueryDialogViewModel_QueryTypes_HasExpectedElevenEntries' - 'QueryDialogViewModel_HierarchyDirectionOptions_HasExpectedThree' + - id: 'SysML2Workbench-AppShellSubsystem-QueryDialog-ConnectionAwareImpact' + title: 'The QueryDialog shall offer an Impact-only "Include connections" option that requests + connection-aware impact analysis, shall leave that option unset for every other Query Type, + and shall present the walk-depth control with wording that states a blank value means one + connector hop when connections are included rather than unlimited.' + justification: | + Impact analysis that ignores connect/bind relationships hides real physical and behavioral + coupling from the user, while always following them floods the result with the dense + connector mesh; making it an explicit, clearly-bounded per-query choice lets the user ask + either question deliberately instead of guessing which one the tool answered. + tests: + - 'QueryDialogViewModel_BuildOptions_ImpactVerbWithIncludeConnections_SetsIncludeConnections' + - 'QueryDialogViewModel_BuildOptions_NonImpactVerb_OmitsIncludeConnections' + + - id: 'SysML2Workbench-AppShellSubsystem-QueryDialog-SurfaceTraversalMetadata' + title: 'The QueryDialog shall project each result entry''s traversal depth, resolved relation + kind, and connection roll-up far endpoint into the displayed result row, taking the depth + from the engine''s machine-readable value rather than from the entry''s human-readable + detail text, shall display each of those columns only when at least one entry carries that + value, and shall leave every such field empty for verbs that do not traverse.' + justification: | + A bare list of impacted names does not tell the user how far away an element is, what kind + of relationship reached it, or which port or endpoint actually caused the hit - the three + facts needed to judge whether a reported impact is real, and the reason the same panel must + stay uncluttered for verbs that produce none of them. + tests: + - 'QueryDialogViewModel_BuildRow_TraversalEntry_ProjectsDepthRelationAndVia' + - 'QueryDialogViewModel_BuildRow_NonTraversingEntry_LeavesMetadataEmpty' + - id: 'SysML2Workbench-AppShellSubsystem-QueryDialog-HandleFailuresGracefully' title: 'The QueryDialog shall report every recoverable failure or informational prompt (no workspace, no element selected yet for an element-scoped Query Type, a qualified name that no diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 892f174..8b7c23f 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -171,11 +171,18 @@ combo box below it to choose what to see: Query Type combo - pick an element there to use it as the query's target. Two of them expose extra controls: **Hierarchy** shows a Direction dropdown (*up*, *down*, *both*), and **Impact** shows an optional Walk - Depth text box (leave blank for no bound). + Depth text box plus an **Include connections (connect/bind)** checkbox. + Leave Walk Depth blank for no bound - except when **Include connections** + is ticked, where a blank Walk Depth instead limits the walk to a single + connector hop, because connector graphs are dense meshes. Tick **Include + connections** to follow `connect` and `bind` relationships in addition to + the ordinary reference relationships an Impact query follows by default; + leave it unchecked to see only reference-driven impact. Every change updates the results panel immediately - there is no Run button. Changing the Query Type, adding or removing a chip, editing the -search text, picking an element, changing Direction or Walk Depth, or +search text, picking an element, changing Direction, Walk Depth, or +**Include connections**, or toggling **Include standard library** all recompute the results panel synchronously. If you choose one of the ten element-scoped query types before picking an element, the panel shows a prompt asking you to select @@ -183,8 +190,14 @@ one instead of leaving a stale result on screen. The results panel shows a bullet-list summary and a table of matching entries with Qualified Name, Kind, and Detail columns; the **Dependencies** -verb adds a Direction column. Any per-entry notes appear as a tooltip on -that entry's row. +verb adds a Direction column. Query types that walk the model - **Impact** +in particular - add up to three more columns, each shown only when at least +one entry actually has that information: **Depth** (how many steps away the +entry was reached), **Relation** (the kind of relationship that reached it, +for example *Connect* or *Supertype*), and **Via** (with **Include +connections** ticked, the exact far endpoint - typically a port - behind an +entry that is reported against its owning element). Any per-entry notes +appear as a tooltip on that entry's row. Right-click the results panel and choose **Copy as Markdown** or **Copy as JSON** to place the rendered result on the clipboard for pasting into a diff --git a/docs/verification/ots/sysml2-tools.md b/docs/verification/ots/sysml2-tools.md index 1063bfe..5190c3c 100644 --- a/docs/verification/ots/sysml2-tools.md +++ b/docs/verification/ots/sysml2-tools.md @@ -2,10 +2,12 @@ ### Verification Approach -OTS integration tests in `test/OtsSoftwareTests/SysML2ToolsTests.cs` verify the real parsing, semantic-resolution, standard-library, and rendering APIs consumed by SysML2Workbench (`GlobFileCollector`, `StdlibProvider`, `WorkspaceLoader`, `DiagramRenderer`), exercised against representative workspace fixtures, because vendor behavior is the primary evidence target for this dependency. +OTS integration tests in `test/OtsSoftwareTests/SysML2ToolsTests.cs` verify the real parsing, semantic-resolution, standard-library, querying, and rendering APIs consumed by SysML2Workbench (`GlobFileCollector`, `StdlibProvider`, `WorkspaceLoader`, `QueryEngine`, `DiagramRenderer`), exercised against representative workspace fixtures, because vendor behavior is the primary evidence target for this dependency. ### Test Scenarios **LoadWorkspaceModel_ParsesAndResolvesImports**: A two-file workspace, where one file imports a definition declared in the other, is discovered and loaded through the same `GlobFileCollector`/`StdlibProvider`/`WorkspaceLoader` pipeline the workbench's `WorkspaceModel` uses. The dependency proves it discovers both files, parses without diagnostics, and resolves the cross-file import. -**RenderView_GeneratesLayoutGraph**: A loaded workspace containing a named view usage is submitted to `DiagramRenderer.RenderWorkspace`, and the dependency returns non-empty rendered diagram output for that view. The test name predates the empirical discovery that SysML2Tools 0.1.0-beta.7 has no public `LayoutGraph` type or layout-strategy registry (see the planning report's Assumption #1); the name is kept for ReqStream traceability, and the scenario verifies the real single-call `RenderWorkspace` contract instead. This dependency's public surface has since been confirmed unchanged through SysML2Tools 0.1.0-beta.8. +**RenderView_GeneratesLayoutGraph**: A loaded workspace containing a named view usage is submitted to `DiagramRenderer.RenderWorkspace`, and the dependency returns non-empty rendered diagram output for that view. The test name predates the empirical discovery that SysML2Tools has no public `LayoutGraph` type or layout-strategy registry (see the planning report's Assumption #1); the name is kept for ReqStream traceability, and the scenario verifies the real single-call `RenderWorkspace` contract instead. This dependency's parsing and rendering surface has since been confirmed unchanged through SysML2Tools 0.2.0-beta.1, which required no source changes in the workbench. + +**ImpactQuery_IncludeConnections_TraversesConnectorEdges**: A workspace containing two sibling parts joined only by a `connect` is loaded, and the same impact query is run against one of them twice - once with connection awareness off and once on. The dependency proves that the connector-reached sibling is absent from the default result and present when connections are included, and that the entry produced by the connector traversal carries both a traversal depth and a resolved relation kind. This scenario also pins the behavioral change introduced in SysML2Tools 0.2.0-beta.1: the impact walk is now restricted to resolved reference edges by default, so a model containing `connect`/`bind` yields fewer default impact results than before and connector reachability must be requested explicitly. diff --git a/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md b/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md index 0871adc..64efd59 100644 --- a/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md +++ b/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md @@ -9,8 +9,13 @@ on-disk workspace - it has no Avalonia dependency itself, so no UI thread or dia needed to exercise its state, `BuildOptions` shape, or clipboard composition. This design has no explicit "Run" method: every scenario drives the auto-recompute contract by assigning the property that should trigger it (`SelectedQueryType`, `Picker.SelectedQualifiedName`, `HierarchyDirection`, -`WalkDepthText`, `IncludeStdlib`) and asserting `CurrentResult`/`StatusMessage` update immediately, -with no intervening method call anywhere in the test bodies. A single Avalonia-headless end-to-end +`WalkDepthText`, `IncludeConnections`, `IncludeStdlib`) and asserting `CurrentResult`/`StatusMessage` +update immediately, +with no intervening method call anywhere in the test bodies. The pure row-projection scenarios +instead call the public `BuildRow` function directly with a hand-built result entry, so the mapping +from engine metadata to displayed columns can be asserted exactly - including that the depth comes +from the entry's machine-readable value rather than from its human-readable detail text - without +depending on which entries a particular engine version happens to return. A single Avalonia-headless end-to-end test in `test/OtsSoftwareTests/AvaloniaTests.cs` opens the real dialog through the real Query menu item, confirms the dialog opens on "List" with the selection-free filter control visible and the element picker hidden, selects the Describe Query Type (confirming the visibility flip) and an @@ -126,6 +131,36 @@ to that integer. Verified by flag to `QueryOptions.IncludeStdlib`, regardless of Query Type. Verified by `QueryDialogViewModelTests.BuildOptions_PropagatesIncludeStdlib`. +**BuildOptions_ImpactVerbWithIncludeConnections_SetsIncludeConnections**: With +`SelectedQueryType=Impact` and `IncludeConnections` set, `BuildOptions` requests connection-aware +impact analysis, so the user's opt-in to connector (`connect`/`bind`) traversal actually reaches the +engine. Verified by +`QueryDialogViewModelTests.BuildOptions_ImpactVerbWithIncludeConnections_SetsIncludeConnections`. + +**BuildOptions_NonImpactVerb_OmitsIncludeConnections**: With any Query Type other than Impact +(exercised over Describe, Uses, and Hierarchy), `BuildOptions` leaves connection awareness unset even +when the checkbox was left ticked from a prior Impact query, so switching Query Type never changes +another verb's option payload. Verified by +`QueryDialogViewModelTests.BuildOptions_NonImpactVerb_OmitsIncludeConnections`. + +**IncludeConnectionsChange_WithSelection_RecomputesImmediately**: With `SelectedQueryType=Impact` and +an active selection, toggling `IncludeConnections` recomputes `CurrentResult` immediately, with no +intervening method call and no stale state or thrown exception. Verified by +`QueryDialogViewModelTests.IncludeConnectionsChange_WithSelection_RecomputesImmediately`. + +**BuildRow_TraversalEntry_ProjectsDepthRelationAndVia**: A result entry carrying a traversal depth, a +resolved relation kind, and a connection roll-up far endpoint projects all three into the displayed +row (as `"2"`, `"Connect"`, and the far endpoint's qualified name). The entry's detail text +deliberately claims a different depth, so an implementation that parsed the depth out of the detail +text instead of reading the engine's machine-readable value would fail this scenario. Verified by +`QueryDialogViewModelTests.BuildRow_TraversalEntry_ProjectsDepthRelationAndVia`. + +**BuildRow_NonTraversingEntry_LeavesMetadataEmpty**: A result entry from a verb that does not +traverse projects empty strings (never the text `"null"`) for depth, relation, and far endpoint, and +a null notes tooltip - the regression guard that keeps the data-driven column visibility hiding all +three and the results panel rendering exactly as it did before those columns existed. Verified by +`QueryDialogViewModelTests.BuildRow_NonTraversingEntry_LeavesMetadataEmpty`. + **IncludeStdlibToggle_RefreshesPickerAndRecomputesResult**: Toggling `IncludeStdlib` refreshes both `FilterOnly`'s and `Picker`'s candidate lists to reflect the new stdlib-inclusion rule, and recomputes the current result (clearing a now-unselected Describe result to the select-element diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml index 25c8d32..5f201b3 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml @@ -47,10 +47,14 @@ - - - + + + + @@ -99,6 +103,9 @@ + + + diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml.cs b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml.cs index cafa652..00ed275 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml.cs +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml.cs @@ -138,8 +138,9 @@ private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.Pr } /// - /// Toggles the Hierarchy / Impact per-verb control panels' visibility based on the currently - /// selected Query Type. + /// Toggles the Hierarchy / Impact per-verb option panels' visibility based on the currently + /// selected Query Type. The Impact panel is a group (walk depth plus the include-connections + /// checkbox), so it is named for the verb rather than for any single input it hosts. /// private void ApplyQueryTypeVisibility() { @@ -149,7 +150,7 @@ private void ApplyQueryTypeVisibility() } HierarchyDirectionPanel.IsVisible = _viewModel.SelectedQueryType == QueryVerb.Hierarchy; - WalkDepthPanel.IsVisible = _viewModel.SelectedQueryType == QueryVerb.Impact; + ImpactOptionsPanel.IsVisible = _viewModel.SelectedQueryType == QueryVerb.Impact; // "List" has no target-element concept - its result is a purely client-side filter over the // filter-only control's chip/search state - so ListFilterView is shown and ElementQueryPickerView @@ -166,9 +167,11 @@ private void ApplyQueryTypeVisibility() /// /// Toggles the results panel's visibility based on the current - /// . Copy-menu-item enablement is handled purely - /// by the AXAML's IsEnabled="{Binding HasCurrentResult}" bindings, so this method no longer - /// needs to imperatively toggle any button/menu-item state (unlike the old toolbar-button design). + /// , and the visibility of each optional column + /// so a column is only shown when it carries meaning for the current result. Copy-menu-item + /// enablement is handled purely by the AXAML's IsEnabled="{Binding HasCurrentResult}" + /// bindings, so this method no longer needs to imperatively toggle any button/menu-item state + /// (unlike the old toolbar-button design). /// private void ApplyResultVisibility() { @@ -185,10 +188,17 @@ private void ApplyResultVisibility() SummaryItemsControl.ItemsSource = result?.Summary; EntriesTableBorder.IsVisible = hasEntries; - // The Direction column is dependency-verb specific per the plan; only show it when the current - // result is a dependency-verb result. Indexed rather than named: DataGridColumn does not - // participate in Avalonia's compiled-bindings field generation the way a Control does. + // Optional columns, in AXAML declaration order: [3] Direction, [4] Depth, [5] Relation, + // [6] Via. Indexed rather than named: DataGridColumn does not participate in Avalonia's + // compiled-bindings field generation the way a Control does. The Direction column is + // dependency-verb specific per the plan; the three traversal-metadata columns are instead + // data-driven, shown only when at least one row actually carries that value, so non-traversing + // verbs (whose rows leave all three empty) render exactly the same panel as before. + var rows = _viewModel.CurrentResultRows; EntriesDataGrid.Columns[3].IsVisible = string.Equals(result?.Verb, "dependencies", StringComparison.Ordinal); + EntriesDataGrid.Columns[4].IsVisible = rows.Any(row => row.Depth.Length > 0); + EntriesDataGrid.Columns[5].IsVisible = rows.Any(row => row.Relation.Length > 0); + EntriesDataGrid.Columns[6].IsVisible = rows.Any(row => row.Via.Length > 0); } private void OnQueryTypeSelectionChanged(object? sender, SelectionChangedEventArgs e) diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogViewModel.cs b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogViewModel.cs index f6eb2e4..b7b3a07 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogViewModel.cs +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogViewModel.cs @@ -15,8 +15,9 @@ namespace DemaConsulting.SysML2Workbench.AppShellSubsystem; /// user-facing options (a merged entry plus the ten element-scoped /// operations dispatched through ). Every /// relevant change - Query Type, chip/search edits, element selection, Hierarchy direction, Impact -/// walk depth, or the Include-standard-library toggle - immediately recomputes -/// ; there is no explicit "Run" gesture anywhere in this design. +/// walk depth, the Impact include-connections toggle, or the Include-standard-library toggle - +/// immediately recomputes ; there is no explicit "Run" gesture anywhere +/// in this design. /// /// /// Deliberately parallels 's "dialog owned by the shell, @@ -89,6 +90,18 @@ public sealed partial class QueryDialogViewModel : ObservableObject [ObservableProperty] public partial string? WalkDepthText { get; set; } + /// + /// Whether an walk should also traverse connector + /// (connect/bind) relationships in addition to resolved reference edges. Only + /// meaningful for - leaves + /// unset for every other Query Type. Starts + /// , matching the engine's own default. Note the interaction with + /// : a blank walk depth normally means "unlimited", but when this + /// flag is set a blank walk depth bounds connector hops along a single traversal path to one. + /// + [ObservableProperty] + public partial bool IncludeConnections { get; set; } + [ObservableProperty] public partial QueryResult? CurrentResult { get; set; } @@ -350,9 +363,9 @@ public async Task CopyResultAsJsonAsync() /// /// Builds a instance for that /// reflects the current form's verb-specific state: is only - /// attached for , and is only - /// parsed for . Every option unconditionally carries - /// . + /// attached for , and and + /// are only attached for . + /// Every option unconditionally carries . /// /// The resolved target element's qualified name. /// The to pass to . @@ -374,6 +387,11 @@ public QueryOptions BuildOptions(string qualifiedName) IncludeStdlib = IncludeStdlib, Direction = SelectedQueryType == QueryVerb.Hierarchy ? HierarchyDirection : null, WalkDepth = walkDepth, + + // Connector traversal is an Impact-only concern; gating it here (rather than passing the + // raw flag through) keeps every other verb's option payload byte-for-byte unchanged when + // the user leaves the checkbox ticked and switches Query Type. + IncludeConnections = SelectedQueryType == QueryVerb.Impact && IncludeConnections, }; } @@ -417,6 +435,18 @@ partial void OnWalkDepthTextChanged(string? value) RecomputeResult(); } + /// + /// Recomputes immediately whenever the Impact include-connections toggle changes, so a selection + /// already made for stays live as the user opts connector + /// (connect/bind) traversal in or out - the same "no explicit Run gesture" contract + /// the walk-depth and Query Type inputs follow. + /// + /// The new value of the property. + partial void OnIncludeConnectionsChanged(bool value) + { + RecomputeResult(); + } + /// /// Raises the computed property's change notification whenever /// changes, so the results panel's context-menu IsEnabled @@ -459,14 +489,20 @@ private void OnFilterOnlyPropertyChanged(object? sender, System.ComponentModel.P /// /// Converts a into a display-friendly row for the results panel, - /// eagerly flattening to a human-readable string and + /// eagerly flattening to a human-readable string, + /// // + /// to plain traversal-metadata strings, and /// to a newline-joined tooltip string so the view can bind - /// directly to string properties without additional converters. + /// directly to string properties without additional converters. Exposed publicly (like + /// ) so tests can assert the projection contract directly without + /// spinning up an Avalonia view; it is a pure function of . /// /// The engine-produced entry to project. /// A ready for the results-panel ItemsControl. - private static QueryResultRow BuildRow(QueryResultEntry entry) + public static QueryResultRow BuildRow(QueryResultEntry entry) { + ArgumentNullException.ThrowIfNull(entry); + return new QueryResultRow( entry.QualifiedName, entry.Kind ?? string.Empty, @@ -477,6 +513,13 @@ private static QueryResultRow BuildRow(QueryResultEntry entry) QueryEntryDirection.Incoming => "incoming", _ => string.Empty, }, + + // Depth is read from the engine's machine-readable property, never parsed out of the + // human-readable "depth N" text carried in Detail - the upstream API documents that + // consumers shall read this property instead. + entry.Depth?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, + entry.Relation?.ToString() ?? string.Empty, + entry.ViaQualifiedName ?? string.Empty, entry.Notes.Count > 0 ? string.Join("\n", entry.Notes) : null); } } @@ -495,6 +538,21 @@ private static QueryResultRow BuildRow(QueryResultEntry entry) /// other verb. Rendered in a dedicated column shown only when the current verb is /// dependencies. /// +/// +/// The entry's 1-based traversal depth rendered as an invariant-culture integer, or empty for verbs +/// that do not traverse. Rendered in a dedicated column shown only when at least one row carries a +/// depth. +/// +/// +/// The resolved semantic edge kind that reached this entry (for example "Supertype" or +/// "Connect"), or empty when the entry was not produced by traversing a resolved edge. +/// Rendered in a dedicated column shown only when at least one row carries a relation. +/// +/// +/// For connection roll-up, the qualified name of the actual far endpoint whose nearest owning +/// declaration is reported as ; empty when no roll-up occurred. Rendered +/// in a dedicated column shown only when at least one row carries a far endpoint. +/// /// /// Newline-joined additional notes attached to the entry, or when the entry /// has no notes. Bound to the row's tooltip. @@ -504,4 +562,7 @@ public sealed record QueryResultRow( string Kind, string Detail, string Direction, + string Depth, + string Relation, + string Via, string? Notes); diff --git a/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj b/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj index abdd86a..6ddb22d 100644 --- a/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj +++ b/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj @@ -18,9 +18,9 @@ - - - + + + diff --git a/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/QueryDialogViewModelTests.cs b/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/QueryDialogViewModelTests.cs index d15aa56..a056c5a 100644 --- a/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/QueryDialogViewModelTests.cs +++ b/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/QueryDialogViewModelTests.cs @@ -451,6 +451,135 @@ public void QueryDialogViewModel_BuildOptions_ImpactVerbWithInvalidWalkDepth_Lea Assert.Null(options.WalkDepth); } + /// + /// Validates that reaches + /// when the Query Type is + /// , so the user's opt-in to connector (connect/bind) traversal + /// actually reaches the engine. + /// + [Fact] + public void QueryDialogViewModel_BuildOptions_ImpactVerbWithIncludeConnections_SetsIncludeConnections() + { + // Arrange + using var shell = CreateShell(); + var viewModel = new QueryDialogViewModel(shell); + viewModel.SelectedQueryType = QueryVerb.Impact; + viewModel.IncludeConnections = true; + + // Act + var options = viewModel.BuildOptions("Some::Element"); + + // Assert + Assert.Equal(QueryVerb.Impact, options.Verb); + Assert.True(options.IncludeConnections); + } + + /// + /// Validates that stays unset for every non-Impact + /// Query Type even when the checkbox was left ticked from a prior Impact query, so switching verb + /// never changes another verb's option payload. + /// + /// A Query Type for which connector traversal has no meaning. + [Theory] + [InlineData(QueryVerb.Describe)] + [InlineData(QueryVerb.Uses)] + [InlineData(QueryVerb.Hierarchy)] + public void QueryDialogViewModel_BuildOptions_NonImpactVerb_OmitsIncludeConnections(QueryVerb queryType) + { + // Arrange + using var shell = CreateShell(); + var viewModel = new QueryDialogViewModel(shell); + viewModel.IncludeConnections = true; + viewModel.SelectedQueryType = queryType; + + // Act + var options = viewModel.BuildOptions("Some::Element"); + + // Assert + Assert.False(options.IncludeConnections); + } + + /// + /// Validates that toggling while + /// is selected with an active selection recomputes immediately, + /// with no intervening method call - the dialog's "no explicit Run gesture" contract. + /// + [Fact] + public async Task QueryDialogViewModel_IncludeConnectionsChange_WithSelection_RecomputesImmediately() + { + // Arrange + await WriteSampleWorkspaceAsync(); + using var shell = CreateShell(); + await shell.AddFolderSourceAsync(_tempRoot); + var viewModel = new QueryDialogViewModel(shell); + viewModel.SelectedQueryType = QueryVerb.Impact; + viewModel.Picker.SelectedQualifiedName = "Sample::Engine"; + Assert.NotNull(viewModel.CurrentResult); + + // Act + viewModel.IncludeConnections = true; + + // Assert + Assert.NotNull(viewModel.CurrentResult); + Assert.Equal("impact", viewModel.CurrentResult!.Verb); + Assert.Null(viewModel.StatusMessage); + } + + /// + /// Validates that projects a traversal entry's + /// machine-readable depth, resolved relation kind, and connection roll-up far endpoint into the + /// row - reading directly rather than parsing the + /// human-readable "depth N" text out of , which the entry + /// here deliberately contradicts so a parsing implementation would fail this test. + /// + [Fact] + public void QueryDialogViewModel_BuildRow_TraversalEntry_ProjectsDepthRelationAndVia() + { + // Arrange: Detail claims a different depth than the Depth property carries + var entry = new QueryResultEntry + { + QualifiedName = "Sample::Engine", + Kind = "part def", + Detail = "depth 9", + Depth = 2, + Relation = SysmlEdgeKind.Connect, + ViaQualifiedName = "Sample::Car::enginePort", + }; + + // Act + var row = QueryDialogViewModel.BuildRow(entry); + + // Assert + Assert.Equal("2", row.Depth); + Assert.Equal("Connect", row.Relation); + Assert.Equal("Sample::Car::enginePort", row.Via); + } + + /// + /// Regression guard for the results panel of non-traversing verbs: an entry carrying no traversal + /// metadata projects empty strings (never "null" text) for all three new columns, so the + /// data-driven column visibility keeps them hidden and the panel renders exactly as before. + /// + [Fact] + public void QueryDialogViewModel_BuildRow_NonTraversingEntry_LeavesMetadataEmpty() + { + // Arrange + var entry = new QueryResultEntry + { + QualifiedName = "Sample::Engine", + Kind = "part def", + }; + + // Act + var row = QueryDialogViewModel.BuildRow(entry); + + // Assert + Assert.Equal(string.Empty, row.Depth); + Assert.Equal(string.Empty, row.Relation); + Assert.Equal(string.Empty, row.Via); + Assert.Null(row.Notes); + } + /// /// Validates that flows through into /// for every verb. diff --git a/test/OtsSoftwareTests/OtsSoftwareTests.csproj b/test/OtsSoftwareTests/OtsSoftwareTests.csproj index a0098d4..71e8433 100644 --- a/test/OtsSoftwareTests/OtsSoftwareTests.csproj +++ b/test/OtsSoftwareTests/OtsSoftwareTests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/test/OtsSoftwareTests/SysML2ToolsTests.cs b/test/OtsSoftwareTests/SysML2ToolsTests.cs index a7e446f..8a46f63 100644 --- a/test/OtsSoftwareTests/SysML2ToolsTests.cs +++ b/test/OtsSoftwareTests/SysML2ToolsTests.cs @@ -1,5 +1,6 @@ using DemaConsulting.SysML2Tools.Io; using DemaConsulting.SysML2Tools.Parser; +using DemaConsulting.SysML2Tools.Query; using DemaConsulting.SysML2Tools.Semantic; using DemaConsulting.SysML2Tools.Stdlib; using DemaConsulting.SysML2Workbench; @@ -99,4 +100,63 @@ await File.WriteAllTextAsync( // Assert: SysML2Tools produced concrete, renderable diagram output for the requested view Assert.NotEmpty(outputs); } + + /// + /// Validates that the impact analysis in traverses connector + /// (connect) relationships only when is set, + /// and that entries reached that way carry the machine-readable traversal metadata + /// ( and ) the + /// workbench's results panel displays. + /// + /// + /// This pins the dependency's narrowed default: the impact walk now follows resolved reference + /// edges only, so connector reachability the workbench previously got implicitly must be + /// requested explicitly. + /// + [Fact] + public async Task ImpactQuery_IncludeConnections_TraversesConnectorEdges() + { + // Arrange: two sibling parts joined by nothing but a connector + await File.WriteAllTextAsync( + PathHelpers.SafePathCombine(_tempRoot, "Connected.sysml"), + "package Connected {\n" + + " part def Engine;\n" + + " part def Gearbox;\n" + + " part def Car {\n" + + " part engine : Engine;\n" + + " part gearbox : Gearbox;\n" + + " connect engine to gearbox;\n" + + " }\n" + + "}\n", + TestContext.Current.CancellationToken); + var discoveredFiles = GlobFileCollector.Collect(["**/*.sysml"], [], _tempRoot); + var (symbolTable, _) = StdlibProvider.GetSymbolTable(); + var loadResult = await WorkspaceLoader.LoadAsync(discoveredFiles, symbolTable); + var workspace = loadResult.Workspace!; + var target = workspace.Declarations["Connected::Car::engine"]; + + // Act: the same impact query with connector traversal off, then on + var withoutConnections = QueryEngine.Impact( + workspace, + target, + new QueryOptions { Verb = QueryVerb.Impact, Element = "Connected::Car::engine" }); + var withConnections = QueryEngine.Impact( + workspace, + target, + new QueryOptions + { + Verb = QueryVerb.Impact, + Element = "Connected::Car::engine", + IncludeConnections = true, + }); + + // Assert: the connector-reached sibling appears only when connections are included, and its + // entry carries the traversal metadata the workbench surfaces + Assert.DoesNotContain(withoutConnections.Entries, entry => entry.QualifiedName == "Connected::Car::gearbox"); + var reached = Assert.Single( + withConnections.Entries, + entry => entry.QualifiedName == "Connected::Car::gearbox"); + Assert.NotNull(reached.Depth); + Assert.NotNull(reached.Relation); + } } From 502e5b16f04f993eae3dcd72992507c49c0ee968 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Wed, 29 Jul 2026 23:12:57 -0400 Subject: [PATCH 2/6] fix: open files added via "Add File..." on double-click A file added through "Add File..." is represented in the workspace tree as a WorkspaceSourceNode of kind File - a leaf with no WorkspaceFileNode beneath it, since WorkspaceFileNode is only produced for files discovered under a folder source. The DoubleTapped handler matched WorkspaceFileNode alone, so double-clicking a directly-added file fell through as a no-op while the same file opened correctly when reached through an added folder. Resolve both representations. The "which node opens what" decision moves out of the Avalonia code-behind into a pure static WorkspacePanelToolViewModel.ResolveOpenableFilePath, so it is unit testable without a view; the handler now just forwards a resolved path. The .sysml filter is applied once, after mapping, so it gates both openable node kinds uniformly. Folder groupings and folder-kind sources still resolve to null, keeping a double-tap on them a safe no-op. Covered by six regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app-shell-subsystem/workspace-panel.md | 45 +++++-- .../app-shell-subsystem/workspace-panel.md | 40 +++++- .../WorkspacePanelToolView.axaml.cs | 14 +- .../WorkspacePanelToolViewModel.cs | 63 ++++++++- .../WorkspacePanelToolViewModelTests.cs | 123 ++++++++++++++++++ 5 files changed, 258 insertions(+), 27 deletions(-) diff --git a/docs/design/sysml2-workbench/app-shell-subsystem/workspace-panel.md b/docs/design/sysml2-workbench/app-shell-subsystem/workspace-panel.md index d7f290d..eeb6d69 100644 --- a/docs/design/sysml2-workbench/app-shell-subsystem/workspace-panel.md +++ b/docs/design/sysml2-workbench/app-shell-subsystem/workspace-panel.md @@ -27,7 +27,10 @@ and `required IReadOnlyList Children` — a source's node. `Folder`-kind source it holds one entry per top-level subfolder (`WorkspaceFolderNode`) or file (`WorkspaceFileNode`) directly under that folder, preserving its on-disk hierarchy rather than flattening every -discovered file into a single list. +discovered file into a single list. Because a `File`-kind source has no +`WorkspaceFileNode` beneath it, its source node is itself the only tree +representation of that file, and is therefore openable in its own right (see +`ResolveOpenableFilePath`). **WorkspaceFolderNode**: `WorkspaceTreeNode` with `required string Name` (the folder's own last path segment, not its full path) and @@ -40,11 +43,13 @@ already covers everything beneath it). **WorkspaceFileNode**: `WorkspaceTreeNode` with `required string FilePath`, `required string SourceId`, and a computed `Name` (`Path.GetFileName(FilePath)`, shown in the tree since ancestor nodes already convey the file's directory) — -a leaf node for one file. `FilePath` is a stable identity used by -`WorkspacePanelToolView`'s double-click handler to open a `.sysml` file's +a leaf node for one file discovered under a `Folder`-kind source. `FilePath` is +a stable identity used by `ResolveOpenableFilePath` (and through it +`WorkspacePanelToolView`'s double-click handler) to open a `.sysml` file's read-only source-text tab via `MainWindowShell.OpenSourceTextTab`; it is preserved even though nothing else currently reads it beyond display and -that handler. +that handler. It is not the only openable node kind — a `File`-kind +`WorkspaceSourceNode` is openable too, via its `Source.Path`. **SelectedNode**: `WorkspaceTreeNode?` — the tree node currently selected by the user, used to resolve which source `RemoveSelected` acts on. @@ -96,13 +101,31 @@ into `MainWindowShell.AddFileSourceAsync` / `AddFolderSourceAsync`. and surfaced via `StatusMessage` rather than propagated, since a failed remove should not crash the shell. -`WorkspacePanelToolView`'s code-behind also wires a `DoubleTapped` handler on -its `TreeView` directly to `MainWindowShell.OpenSourceTextTab`: when the -selected item is a `WorkspaceFileNode` whose `FilePath` ends in `.sysml` -(case-insensitive), double-clicking it opens the file's read-only -source-text tab. No view-model method backs this - it follows the same -"view calls `Shell` directly" pattern already used by the Add File/Add -Folder picker flows, and is thin enough that it is not unit tested (see +**ResolveOpenableFilePath**: Decides which file, if any, a selected workspace +tree node should open in a read-only source-text tab. + +- *Parameters*: `node` (`WorkspaceTreeNode?`) — the currently selected node, or + `null` when nothing is selected. +- *Returns*: `string?` — the absolute path to open, or `null` when the node + denotes no single file or denotes a non-`.sysml` file. +- *Postconditions*: A `WorkspaceFileNode` resolves to its `FilePath`, and a + `File`-kind `WorkspaceSourceNode` resolves to its `Source.Path` — a workspace + file has two possible tree representations and both must be openable. A + `WorkspaceFolderNode` and a `Folder`-kind `WorkspaceSourceNode` resolve to + `null`, since they denote a set of files rather than one. The `.sysml` + extension filter (case-insensitive) is applied uniformly to both openable + kinds because the source-text viewer only renders SysML v2 textual sources. + Pure and side-effect free — it reads only its argument and touches neither + the file system nor view-model state. + +`WorkspacePanelToolView`'s code-behind wires a `DoubleTapped` handler on its +`TreeView` that calls `ResolveOpenableFilePath` for the currently selected node +and forwards any non-null result to `MainWindowShell.OpenSourceTextTab`. The +decision lives on the view model - rather than inline in the handler as it did +originally - so it is a pure function unit-testable without an Avalonia view; +the handler itself remains thin view-layer wiring following the same "view +calls `Shell` directly" pattern already used by the Add File/Add Folder picker +flows, and is not itself unit tested (see `docs/verification/sysml2-workbench/app-shell-subsystem/workspace-panel.md`). #### Error Handling diff --git a/docs/verification/sysml2-workbench/app-shell-subsystem/workspace-panel.md b/docs/verification/sysml2-workbench/app-shell-subsystem/workspace-panel.md index 05ad861..059b3af 100644 --- a/docs/verification/sysml2-workbench/app-shell-subsystem/workspace-panel.md +++ b/docs/verification/sysml2-workbench/app-shell-subsystem/workspace-panel.md @@ -20,12 +20,13 @@ folders and files, plus real collaborator units. No external services are requir independently unit-tested: both handlers only branch on `File.Exists`/`Directory.Exists` for each dropped path and then call the exact same `MainWindowShell.AddFileSourceAsync`/`AddFolderSourceAsync` methods already covered by `MainWindowShellTests`, so drag-and-drop is verified by code review rather than a dedicated Avalonia UI-automation -test. The double-click-to-view-source gesture added to `WorkspacePanelToolView`'s code-behind is likewise not -independently unit-tested: it is thin view-layer wiring that resolves the tapped node's `FilePath` and calls the -exact same `MainWindowShell.OpenSourceTextTab` already covered end-to-end by `MainWindowShellTests`, so it too is -verified by code review rather than a dedicated Avalonia UI-automation test - consistent with this codebase's -established convention that no view-layer gesture (drag-and-drop, pointer pan/zoom, and now double-click) has a -headless-Avalonia test in the main test project. +test. The double-click-to-view-source gesture in `WorkspacePanelToolView`'s code-behind is not itself independently +unit-tested, but its decision logic is: the handler now only calls +`WorkspacePanelToolViewModel.ResolveOpenableFilePath` and forwards a non-null result to the exact same +`MainWindowShell.OpenSourceTextTab` already covered end-to-end by `MainWindowShellTests`, so the remaining +view-layer wiring is verified by code review rather than a dedicated Avalonia UI-automation test - consistent with +this codebase's established convention that no view-layer gesture (drag-and-drop, pointer pan/zoom, and +double-click) has a headless-Avalonia test in the main test project. #### Acceptance Criteria @@ -87,3 +88,30 @@ selected resolves and removes that file's owning source via `MainWindowShell.Rem **RemoveSelectedCommand_NoSelection_IsNoOp**: Invoking Remove with no node selected does not call `MainWindowShell.RemoveSourceAsync` and leaves the tree unchanged. Verified by `WorkspacePanelToolViewModelTests.RemoveSelectedCommand_NoSelection_IsNoOp`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileSourceNode_ReturnsSourcePath**: A `File`-kind +`WorkspaceSourceNode` with a `.sysml` path resolves to that path, so double-clicking a file added via +"Add File..." opens its source-text tab. Regression test for the defect where only `WorkspaceFileNode` was +openable and such a double-click was a no-op. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileSourceNode_ReturnsSourcePath`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderSourceNode_ReturnsNull**: A `Folder`-kind +`WorkspaceSourceNode` resolves to nothing, keeping a double-tap on it a safe no-op. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderSourceNode_ReturnsNull`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderNode_ReturnsNull**: An intermediate +`WorkspaceFolderNode` resolves to nothing, keeping a double-tap on it a safe no-op. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderNode_ReturnsNull`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileNode_ReturnsFilePath**: A `WorkspaceFileNode` with a +`.sysml` path still resolves to that path, confirming the pre-existing openable kind did not regress. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileNode_ReturnsFilePath`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_NonSysmlPath_ReturnsNull**: A non-`.sysml` path resolves to +nothing for both openable node kinds, while a mixed-case `.SysML` extension still resolves, confirming the filter +is applied uniformly and case-insensitively. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_NonSysmlPath_ReturnsNull`. + +**WorkspacePanelToolViewModel_ResolveOpenableFilePath_NullNode_ReturnsNull**: A null selection resolves to nothing, +so a double-tap with no node selected cannot throw out of the view's event handler. Verified by +`WorkspacePanelToolViewModelTests.WorkspacePanelToolViewModel_ResolveOpenableFilePath_NullNode_ReturnsNull`. diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolView.axaml.cs b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolView.axaml.cs index 2e97717..d29f377 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolView.axaml.cs +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolView.axaml.cs @@ -160,11 +160,13 @@ private async void OnDrop(object? sender, DragEventArgs e) /// /// Handles a double-tap anywhere on WorkspaceTreeView by opening a read-only source-text tab for - /// the currently selected node, if it is a whose - /// ends in .sysml. Avalonia's updates - /// on the same click that raises DoubleTapped, so no custom hit-testing of the - /// event's source is needed. A double-tap on any other node kind (a source or folder node), or a - /// non-.sysml file, is a safe no-op. + /// the currently selected node, when + /// resolves that node to a file path. + /// Avalonia's updates on the same click that + /// raises DoubleTapped, so no custom hit-testing of the event's source is needed. All of the + /// "which node opens what" logic lives on the view model so it is unit testable without a view; this + /// handler only forwards a resolved path. A double-tap on a node that resolves to nothing - a folder + /// grouping, a folder-kind source, or a non-.sysml file - is a safe no-op. /// private void OnWorkspaceTreeViewDoubleTapped(object? sender, TappedEventArgs e) { @@ -173,7 +175,7 @@ private void OnWorkspaceTreeViewDoubleTapped(object? sender, TappedEventArgs e) return; } - if (viewModel.SelectedNode is WorkspaceFileNode { FilePath: { } filePath } && filePath.EndsWith(".sysml", StringComparison.OrdinalIgnoreCase)) + if (WorkspacePanelToolViewModel.ResolveOpenableFilePath(viewModel.SelectedNode) is { } filePath) { viewModel.Shell.OpenSourceTextTab(filePath); } diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolViewModel.cs b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolViewModel.cs index 6f1d554..dd9dda0 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolViewModel.cs +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/WorkspacePanelToolViewModel.cs @@ -28,6 +28,13 @@ public abstract class WorkspaceTreeNode /// preserving that folder's on-disk hierarchy rather than flattening every discovered file into a single /// list (see ). /// +/// +/// Because a source has no beneath +/// it, this node is itself the only tree representation of that file. It is therefore openable in its own +/// right: resolves a +/// node to , so double-clicking +/// an "Add File..."-added file behaves the same as double-clicking a file discovered under a folder source. +/// public sealed class WorkspaceSourceNode : WorkspaceTreeNode { /// @@ -70,11 +77,17 @@ public sealed class WorkspaceFolderNode : WorkspaceTreeNode } /// -/// A leaf tree node representing one file contributed by a source. Its is a stable -/// identity used by 's double-click handler to open a read-only -/// source-text tab via ; it must not be erased or aggregated -/// away even though nothing else currently reads it beyond display. +/// A leaf tree node representing one file discovered under a +/// source. Its is a stable identity used by +/// - and through it +/// 's double-click handler - to open a read-only source-text tab via +/// ; it must not be erased or aggregated away even though +/// nothing else currently reads it beyond display. /// +/// +/// This is not the only openable node kind: a +/// is openable too, via its . +/// public sealed class WorkspaceFileNode : WorkspaceTreeNode { /// @@ -176,6 +189,48 @@ public void RebuildTree() IsEmpty = RootNodes.Count == 0; } + /// + /// Decides which file, if any, a selected workspace tree node should open in a read-only source-text tab. + /// + /// + /// This lives on the view model rather than inline in 's + /// DoubleTapped handler so the decision is a pure function that can be unit tested without an + /// Avalonia view; the handler is reduced to calling this and forwarding a non-null result to + /// . Two node kinds are openable, because a workspace + /// file has two possible tree representations: a when it was discovered + /// under a folder source, and a of kind + /// when the user added the file directly (that node is a leaf + /// with no beneath it). Nodes that do not denote a single file - a + /// and a + /// - resolve to null so a double-tap on them is a safe no-op. + /// The .sysml filter is applied uniformly to both openable kinds, since the source-text viewer + /// only handles SysML v2 textual sources. Pure and side-effect free: it reads nothing but + /// and touches neither the file system nor view-model state, and is therefore + /// safe to call from any thread. + /// + /// + /// The currently selected tree node, or null when nothing is selected. Any + /// subtype is accepted; unrecognized kinds resolve to null. + /// + /// + /// The absolute path of the file to open, or null when denotes no single + /// file or denotes a file whose extension is not .sysml (compared case-insensitively). + /// + public static string? ResolveOpenableFilePath(WorkspaceTreeNode? node) + { + // Map the node to the single file it denotes, if any. Folder groupings and folder-kind sources denote a + // set of files rather than one, so they intentionally fall through to null. + var filePath = node switch + { + WorkspaceFileNode fileNode => fileNode.FilePath, + WorkspaceSourceNode { Source.Kind: WorkspaceSourceKind.File } sourceNode => sourceNode.Source.Path, + _ => null, + }; + + // Only SysML v2 textual sources can be shown in the read-only source-text viewer. + return filePath?.EndsWith(".sysml", StringComparison.OrdinalIgnoreCase) == true ? filePath : null; + } + /// /// Builds the nested subfolder/file children of a source, /// grouping the flat list (absolute paths) by their position relative to diff --git a/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/WorkspacePanelToolViewModelTests.cs b/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/WorkspacePanelToolViewModelTests.cs index 7ef318f..bf66307 100644 --- a/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/WorkspacePanelToolViewModelTests.cs +++ b/test/DemaConsulting.SysML2Workbench.Tests/AppShellSubsystem/WorkspacePanelToolViewModelTests.cs @@ -328,4 +328,127 @@ public async Task RemoveSelectedCommand_NoSelection_IsNoOp() Assert.Null(exception); Assert.Single(shell.CurrentWorkspace.Sources); } + + /// + /// Regression test for the defect where double-clicking a file added via "Add File..." did nothing: such + /// a file is represented by a leaf of kind + /// (there is no beneath it), so + /// it must resolve to its own Source.Path. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileSourceNode_ReturnsSourcePath() + { + // Arrange: a file-kind source node exactly as RebuildTree produces for an "Add File..."-added .sysml file + var filePath = PathHelpers.SafePathCombine(_tempRoot, "Sample.sysml"); + var node = new WorkspaceSourceNode + { + Id = "source-1", + Source = new WorkspaceSource("source-1", WorkspaceSourceKind.File, filePath), + Children = [], + }; + + // Act + var resolved = WorkspacePanelToolViewModel.ResolveOpenableFilePath(node); + + // Assert + Assert.Equal(filePath, resolved); + } + + /// + /// Validates that a folder-kind resolves to nothing, since it denotes + /// a set of files rather than one openable file - double-tapping it must remain a safe no-op. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderSourceNode_ReturnsNull() + { + // Arrange + var node = new WorkspaceSourceNode + { + Id = "source-1", + Source = new WorkspaceSource("source-1", WorkspaceSourceKind.Folder, _tempRoot), + Children = [], + }; + + // Act + var resolved = WorkspacePanelToolViewModel.ResolveOpenableFilePath(node); + + // Assert + Assert.Null(resolved); + } + + /// + /// Validates that an intermediate resolves to nothing, since it is a + /// purely-display grouping with no file of its own. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderNode_ReturnsNull() + { + // Arrange + var node = new WorkspaceFolderNode { Id = "source-1::Sub", Name = "Sub", Children = [] }; + + // Act + var resolved = WorkspacePanelToolViewModel.ResolveOpenableFilePath(node); + + // Assert + Assert.Null(resolved); + } + + /// + /// Validates that the pre-existing openable node kind - a discovered + /// under a folder source - still resolves to its own path, so the defect fix did not regress it. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileNode_ReturnsFilePath() + { + // Arrange + var filePath = PathHelpers.SafePathCombine(_tempRoot, "Sub", "Nested.sysml"); + var node = new WorkspaceFileNode { Id = $"source-1::{filePath}", FilePath = filePath, SourceId = "source-1" }; + + // Act + var resolved = WorkspacePanelToolViewModel.ResolveOpenableFilePath(node); + + // Assert + Assert.Equal(filePath, resolved); + } + + /// + /// Validates that the case-insensitive .sysml filter is applied to both openable node kinds, so a + /// non-SysML file the source-text viewer cannot render is never opened, while an unusually-cased + /// .SysML extension still is. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_NonSysmlPath_ReturnsNull() + { + // Arrange: the same non-.sysml file expressed as each openable node kind, plus a mixed-case .sysml path + var textPath = PathHelpers.SafePathCombine(_tempRoot, "Notes.txt"); + var mixedCasePath = PathHelpers.SafePathCombine(_tempRoot, "Sample.SysML"); + var textSourceNode = new WorkspaceSourceNode + { + Id = "source-1", + Source = new WorkspaceSource("source-1", WorkspaceSourceKind.File, textPath), + Children = [], + }; + var textFileNode = new WorkspaceFileNode { Id = "source-2::text", FilePath = textPath, SourceId = "source-2" }; + var mixedCaseFileNode = new WorkspaceFileNode { Id = "source-2::mixed", FilePath = mixedCasePath, SourceId = "source-2" }; + + // Act / Assert: neither openable kind opens a non-.sysml file, but casing alone does not block a .sysml one + Assert.Multiple( + () => Assert.Null(WorkspacePanelToolViewModel.ResolveOpenableFilePath(textSourceNode)), + () => Assert.Null(WorkspacePanelToolViewModel.ResolveOpenableFilePath(textFileNode)), + () => Assert.Equal(mixedCasePath, WorkspacePanelToolViewModel.ResolveOpenableFilePath(mixedCaseFileNode))); + } + + /// + /// Validates that a null selection resolves to nothing, so a double-tap with no node selected cannot + /// throw out of the view's event handler. + /// + [Fact] + public void WorkspacePanelToolViewModel_ResolveOpenableFilePath_NullNode_ReturnsNull() + { + // Arrange / Act + var resolved = WorkspacePanelToolViewModel.ResolveOpenableFilePath(null); + + // Assert + Assert.Null(resolved); + } } From adf829fcd5ff5060c0e1c2ad6cd8faa2c32e393a Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 30 Jul 2026 11:26:47 -0400 Subject: [PATCH 3/6] feat: upgrade to SysML2Tools 0.2.0-beta.2 for unified impact walk depth 0.2.0-beta.1 bounded connector traversal separately from reference traversal: a blank walk depth meant unlimited for reference edges but exactly one hop for connectors, so the full connector closure was unreachable from the Query dialog at any setting - the user had to guess progressively larger depths to widen the blast radius, and "all of it" was not expressible. 0.2.0-beta.2 (upstream PR #55) unifies the bound: one relationship is one unit of depth regardless of kind, and blank is truly unlimited everywhere. The API surface is unchanged, so this is a version bump plus the removal of the caveat the old semantics forced us to document. The walk-depth label returns to the plain "Walk depth (blank = unlimited)", and the design, requirements, and user-guide text describing the connector-hop exception is retired. Pin the new behavior with an OTS regression test that walks a three-hop connector chain: unbounded reaches the whole chain reporting depths 1, 2, 3, while explicit depths truncate it at exactly that many hops. Verified adversarial - it fails against 0.2.0-beta.1. The dialog's label is only truthful while the dependency behaves this way, so the guard belongs with the other OTS evidence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- .../app-shell-subsystem/query-dialog.md | 12 ++-- docs/reqstream/ots/sysml2-tools.yaml | 5 +- .../app-shell-subsystem/query-dialog.yaml | 10 +-- docs/user_guide/getting_started.md | 9 +-- docs/verification/ots/sysml2-tools.md | 4 +- .../AppShellSubsystem/QueryDialogView.axaml | 3 +- .../AppShellSubsystem/QueryDialogViewModel.cs | 7 +- .../DemaConsulting.SysML2Workbench.csproj | 6 +- test/OtsSoftwareTests/OtsSoftwareTests.csproj | 4 +- test/OtsSoftwareTests/SysML2ToolsTests.cs | 67 +++++++++++++++++++ 11 files changed, 101 insertions(+), 28 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7a8ba73..e6f6e3e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -66,7 +66,7 @@ "rollForward": false }, "demaconsulting.sysml2tools.tool": { - "version": "0.2.0-beta.1", + "version": "0.2.0-beta.2", "commands": [ "sysml2tools" ], diff --git a/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md b/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md index ceacad9..f9e89c6 100644 --- a/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md +++ b/docs/design/sysml2-workbench/app-shell-subsystem/query-dialog.md @@ -26,8 +26,8 @@ workspace through a single, always-visible adaptive form — there is no - Verb-specific extra controls appear only when relevant: a "Direction" combo for `Hierarchy`, and — for `Impact` — a "Walk depth" text box plus an "Include connections (connect/bind)" checkbox. The walk-depth label - states both meanings of a blank value: unlimited normally, or a single - connector hop when connections are included. + states the single meaning of a blank value: unlimited, for every + relationship kind the walk traverses. - Every interaction — Query Type selection, chip add/remove, search text, element selection, Direction change, Walk depth text change, the Include-connections toggle, or the Include-standard-library toggle — @@ -99,10 +99,10 @@ cleanly as a non-negative integer. checkbox state, requesting that connector (`connect`/`bind`) relationships be traversed in addition to resolved reference edges. Only attached to `QueryOptions.IncludeConnections` when `SelectedQueryType` is `Impact`; -defaults to `false`, matching the engine's own default. Interacts with -`WalkDepthText`: a blank walk depth normally means unlimited, but when this -flag is set it instead bounds connector hops along a single traversal path -to one, which the walk-depth label states explicitly. +defaults to `false`, matching the engine's own default. Independent of +`WalkDepthText`: the flag selects only which edges the walk may traverse, +never how far it goes, and a connector edge costs the same single unit of +depth as any other relationship. **CurrentResult**: `QueryResult?` — the most recently produced `QueryResult` (either List's client-built list result or the engine's response), or diff --git a/docs/reqstream/ots/sysml2-tools.yaml b/docs/reqstream/ots/sysml2-tools.yaml index bca748b..4df5307 100644 --- a/docs/reqstream/ots/sysml2-tools.yaml +++ b/docs/reqstream/ots/sysml2-tools.yaml @@ -19,8 +19,9 @@ sections: - 'RenderView_GeneratesLayoutGraph' - id: 'SysML2Tools-ConnectionAwareImpactAnalysis' - title: 'The SysML2Workbench shall use SysML2Tools to perform impact analysis that optionally traverses connector (connect/bind) relationships in addition to resolved reference edges.' + title: 'The SysML2Workbench shall use SysML2Tools to perform impact analysis that optionally traverses connector (connect/bind) relationships in addition to resolved reference edges, bounded by a walk depth that applies uniformly to every relationship kind.' justification: | - The impact walk now follows resolved reference edges only by default, so the connector reachability the workbench previously obtained implicitly must be requested explicitly; the dependency's opt-in behavior, and the traversal metadata it reports for connector-reached elements, are therefore the evidence target. + The impact walk now follows resolved reference edges only by default, so the connector reachability the workbench previously obtained implicitly must be requested explicitly; the dependency's opt-in behavior, and the traversal metadata it reports for connector-reached elements, are therefore the evidence target. The workbench's walk-depth control tells the user a blank value means unlimited, so the dependency's uniform treatment of connector edges under that bound is itself part of the contract being relied upon. tests: - 'ImpactQuery_IncludeConnections_TraversesConnectorEdges' + - 'ImpactQuery_WalkDepth_BoundsConnectorTraversalUniformly' diff --git a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml index b3d5810..a8f17dc 100644 --- a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml +++ b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml @@ -68,13 +68,15 @@ sections: - id: 'SysML2Workbench-AppShellSubsystem-QueryDialog-ConnectionAwareImpact' title: 'The QueryDialog shall offer an Impact-only "Include connections" option that requests connection-aware impact analysis, shall leave that option unset for every other Query Type, - and shall present the walk-depth control with wording that states a blank value means one - connector hop when connections are included rather than unlimited.' + and shall present the walk-depth control with wording that states a blank value means an + unlimited walk across every relationship kind the query traverses.' justification: | Impact analysis that ignores connect/bind relationships hides real physical and behavioral coupling from the user, while always following them floods the result with the dense - connector mesh; making it an explicit, clearly-bounded per-query choice lets the user ask - either question deliberately instead of guessing which one the tool answered. + connector mesh; making it an explicit per-query choice lets the user ask either question + deliberately instead of guessing which one the tool answered. Keeping the walk-depth + wording free of connector-specific exceptions matches the engine, where depth bounds all + edge kinds uniformly. tests: - 'QueryDialogViewModel_BuildOptions_ImpactVerbWithIncludeConnections_SetsIncludeConnections' - 'QueryDialogViewModel_BuildOptions_NonImpactVerb_OmitsIncludeConnections' diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 8b7c23f..1c287b8 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -172,12 +172,13 @@ combo box below it to choose what to see: Two of them expose extra controls: **Hierarchy** shows a Direction dropdown (*up*, *down*, *both*), and **Impact** shows an optional Walk Depth text box plus an **Include connections (connect/bind)** checkbox. - Leave Walk Depth blank for no bound - except when **Include connections** - is ticked, where a blank Walk Depth instead limits the walk to a single - connector hop, because connector graphs are dense meshes. Tick **Include + Leave Walk Depth blank for no bound. Tick **Include connections** to follow `connect` and `bind` relationships in addition to the ordinary reference relationships an Impact query follows by default; - leave it unchecked to see only reference-driven impact. + leave it unchecked to see only reference-driven impact. Walk Depth bounds + every relationship kind alike, so a connector hop costs the same one step + as a reference hop - use it when you want proximity rather than full + reachability, since connector graphs are dense meshes. Every change updates the results panel immediately - there is no Run button. Changing the Query Type, adding or removing a chip, editing the diff --git a/docs/verification/ots/sysml2-tools.md b/docs/verification/ots/sysml2-tools.md index 5190c3c..8d4c7bb 100644 --- a/docs/verification/ots/sysml2-tools.md +++ b/docs/verification/ots/sysml2-tools.md @@ -8,6 +8,8 @@ OTS integration tests in `test/OtsSoftwareTests/SysML2ToolsTests.cs` verify the **LoadWorkspaceModel_ParsesAndResolvesImports**: A two-file workspace, where one file imports a definition declared in the other, is discovered and loaded through the same `GlobFileCollector`/`StdlibProvider`/`WorkspaceLoader` pipeline the workbench's `WorkspaceModel` uses. The dependency proves it discovers both files, parses without diagnostics, and resolves the cross-file import. -**RenderView_GeneratesLayoutGraph**: A loaded workspace containing a named view usage is submitted to `DiagramRenderer.RenderWorkspace`, and the dependency returns non-empty rendered diagram output for that view. The test name predates the empirical discovery that SysML2Tools has no public `LayoutGraph` type or layout-strategy registry (see the planning report's Assumption #1); the name is kept for ReqStream traceability, and the scenario verifies the real single-call `RenderWorkspace` contract instead. This dependency's parsing and rendering surface has since been confirmed unchanged through SysML2Tools 0.2.0-beta.1, which required no source changes in the workbench. +**RenderView_GeneratesLayoutGraph**: A loaded workspace containing a named view usage is submitted to `DiagramRenderer.RenderWorkspace`, and the dependency returns non-empty rendered diagram output for that view. The test name predates the empirical discovery that SysML2Tools has no public `LayoutGraph` type or layout-strategy registry (see the planning report's Assumption #1); the name is kept for ReqStream traceability, and the scenario verifies the real single-call `RenderWorkspace` contract instead. This dependency's parsing and rendering surface has since been confirmed unchanged through SysML2Tools 0.2.0-beta.2, which required no source changes in the workbench. **ImpactQuery_IncludeConnections_TraversesConnectorEdges**: A workspace containing two sibling parts joined only by a `connect` is loaded, and the same impact query is run against one of them twice - once with connection awareness off and once on. The dependency proves that the connector-reached sibling is absent from the default result and present when connections are included, and that the entry produced by the connector traversal carries both a traversal depth and a resolved relation kind. This scenario also pins the behavioral change introduced in SysML2Tools 0.2.0-beta.1: the impact walk is now restricted to resolved reference edges by default, so a model containing `connect`/`bind` yields fewer default impact results than before and connector reachability must be requested explicitly. + +**ImpactQuery_WalkDepth_BoundsConnectorTraversalUniformly**: A workspace containing a chain of four parts joined only by `connect` relationships is loaded, and a connection-aware impact query is run against one end of the chain three times - unbounded, bounded to one hop, and bounded to two. The dependency proves that an unbounded walk reaches the entire chain and reports each element's distance from the subject (1, 2, 3), and that an explicit walk depth truncates that same chain at exactly that many connector hops. This scenario pins the correction made in SysML2Tools 0.2.0-beta.2: 0.2.0-beta.1 capped an unbounded connector walk at a single hop, which made the full connector closure unreachable at any setting and contradicted the workbench's "blank = unlimited" walk-depth label. The reported depths are also what let the results panel present impact as an increasing blast radius rather than a flat set. diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml index 5f201b3..e3917be 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/QueryDialogView.axaml @@ -49,8 +49,7 @@ - + connect/bind) relationships in addition to resolved reference edges. Only /// meaningful for - leaves /// unset for every other Query Type. Starts - /// , matching the engine's own default. Note the interaction with - /// : a blank walk depth normally means "unlimited", but when this - /// flag is set a blank walk depth bounds connector hops along a single traversal path to one. + /// , matching the engine's own default. Independent of + /// : this flag selects only which edges the walk may traverse, + /// never how far it goes, and connector edges cost the same one unit of depth as any other + /// relationship. /// [ObservableProperty] public partial bool IncludeConnections { get; set; } diff --git a/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj b/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj index 6ddb22d..8bfa4ba 100644 --- a/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj +++ b/src/DemaConsulting.SysML2Workbench/DemaConsulting.SysML2Workbench.csproj @@ -18,9 +18,9 @@ - - - + + + diff --git a/test/OtsSoftwareTests/OtsSoftwareTests.csproj b/test/OtsSoftwareTests/OtsSoftwareTests.csproj index 71e8433..56ed525 100644 --- a/test/OtsSoftwareTests/OtsSoftwareTests.csproj +++ b/test/OtsSoftwareTests/OtsSoftwareTests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/test/OtsSoftwareTests/SysML2ToolsTests.cs b/test/OtsSoftwareTests/SysML2ToolsTests.cs index 8a46f63..57578c5 100644 --- a/test/OtsSoftwareTests/SysML2ToolsTests.cs +++ b/test/OtsSoftwareTests/SysML2ToolsTests.cs @@ -159,4 +159,71 @@ await File.WriteAllTextAsync( Assert.NotNull(reached.Depth); Assert.NotNull(reached.Relation); } + + /// + /// Validates that bounds connector traversal on exactly the + /// same terms as any other relationship: one connector is one unit of depth, and a + /// walk depth is unlimited for connector edges too. + /// + /// + /// The workbench's Impact walk-depth control tells the user that a blank value means unlimited, + /// with no connector-specific exception. That wording is only truthful while the dependency + /// treats depth uniformly - SysML2Tools 0.2.0-beta.1 instead capped an unbounded connector walk + /// at a single hop, which made the full connector closure unreachable from the dialog at any + /// setting. This pins the 0.2.0-beta.2 behavior so a regression is caught here rather than + /// silently turning the dialog's label into a lie. + /// + [Fact] + public async Task ImpactQuery_WalkDepth_BoundsConnectorTraversalUniformly() + { + // Arrange: a connector chain three hops long, joined by nothing but connectors + await File.WriteAllTextAsync( + PathHelpers.SafePathCombine(_tempRoot, "Chain.sysml"), + "package Chain {\n" + + " part def Node;\n" + + " part def Assembly {\n" + + " part a : Node;\n" + + " part b : Node;\n" + + " part c : Node;\n" + + " part d : Node;\n" + + " connect a to b;\n" + + " connect b to c;\n" + + " connect c to d;\n" + + " }\n" + + "}\n", + TestContext.Current.CancellationToken); + var discoveredFiles = GlobFileCollector.Collect(["**/*.sysml"], [], _tempRoot); + var (symbolTable, _) = StdlibProvider.GetSymbolTable(); + var loadResult = await WorkspaceLoader.LoadAsync(discoveredFiles, symbolTable); + var workspace = loadResult.Workspace!; + var target = workspace.Declarations["Chain::Assembly::a"]; + + QueryResult Impact(int? walkDepth) => QueryEngine.Impact( + workspace, + target, + new QueryOptions + { + Verb = QueryVerb.Impact, + Element = "Chain::Assembly::a", + WalkDepth = walkDepth, + IncludeConnections = true, + }); + + // Act: the chain walked unbounded, then bounded to one hop and to two + var unbounded = Impact(null); + var depthOne = Impact(1); + var depthTwo = Impact(2); + + // Assert: an unbounded walk reaches the whole chain, reporting each element's distance so the + // results panel can present impact as an increasing blast radius + Assert.Equal( + [("Chain::Assembly::b", 1), ("Chain::Assembly::c", 2), ("Chain::Assembly::d", 3)], + unbounded.Entries.Select(entry => (entry.QualifiedName, entry.Depth)).OrderBy(row => row.Depth)); + + // Assert: an explicit depth truncates that same chain at exactly that many connector hops + Assert.Equal(["Chain::Assembly::b"], depthOne.Entries.Select(entry => entry.QualifiedName)); + Assert.Equal( + ["Chain::Assembly::b", "Chain::Assembly::c"], + depthTwo.Entries.Select(entry => entry.QualifiedName).Order()); + } } From f69f2ee0837954d5f70c1834b4475f1d5b2e7aed Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 30 Jul 2026 11:47:16 -0400 Subject: [PATCH 4/6] test: close review findings on traceability and column-index coupling Formal review of the WorkspacePanel set found the new open-on-double-click behavior had no requirement: six regression tests existed with nothing in the reqstream YAML referencing them, and the verification document's lead-in claim that its scenarios follow the authoritative reqstream mappings was therefore untrue. Add the missing ResolveOpenableFile requirement covering both openable node kinds and all four non-openable cases, which restores that claim rather than weakening it. Change review of the QueryDialog set raised the same gap from two directions: ApplyResultVisibility toggles results-grid columns by index because DataGridColumn gets no compiled-binding field, yet no test asserted the mapping, and the SurfaceTraversalMetadata requirement's "show a column only when an entry carries that value" clause had no verifying test. Cover both with headless UI tests, since neither is reachable from a view-model-only test. One pins the column header order the code-behind assumes - confirmed adversarial by swapping two columns in the AXAML, which fails it - so a future reorder becomes a test failure instead of a panel that silently toggles the wrong column. The other drives the dialog with rows carrying partial metadata, proving each column tracks its own value rather than switching as a group. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app-shell-subsystem/query-dialog.yaml | 2 + .../app-shell-subsystem/workspace-panel.yaml | 19 +++ .../app-shell-subsystem/query-dialog.md | 23 +++- .../AppShellSubsystem/QueryDialogUiTests.cs | 120 ++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/QueryDialogUiTests.cs diff --git a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml index a8f17dc..96eb782 100644 --- a/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml +++ b/docs/reqstream/sysml2-workbench/app-shell-subsystem/query-dialog.yaml @@ -95,6 +95,8 @@ sections: tests: - 'QueryDialogViewModel_BuildRow_TraversalEntry_ProjectsDepthRelationAndVia' - 'QueryDialogViewModel_BuildRow_NonTraversingEntry_LeavesMetadataEmpty' + - 'QueryDialogView_TraversalColumns_AreShownOnlyWhenRowsCarryThoseValues' + - 'QueryDialogView_EntriesDataGrid_DeclaresColumnsInIndexOrderCodeBehindAssumes' - id: 'SysML2Workbench-AppShellSubsystem-QueryDialog-HandleFailuresGracefully' title: 'The QueryDialog shall report every recoverable failure or informational prompt (no diff --git a/docs/reqstream/sysml2-workbench/app-shell-subsystem/workspace-panel.yaml b/docs/reqstream/sysml2-workbench/app-shell-subsystem/workspace-panel.yaml index db4ac85..d02d684 100644 --- a/docs/reqstream/sysml2-workbench/app-shell-subsystem/workspace-panel.yaml +++ b/docs/reqstream/sysml2-workbench/app-shell-subsystem/workspace-panel.yaml @@ -40,3 +40,22 @@ sections: - 'RemoveSelectedCommand_WithSourceNodeSelected_RemovesOwningSource' - 'RemoveSelectedCommand_WithFileNodeSelected_RemovesOwningSource' - 'RemoveSelectedCommand_NoSelection_IsNoOp' + + - id: 'SysML2Workbench-AppShellSubsystem-WorkspacePanel-ResolveOpenableFile' + title: 'The WorkspacePanel shall resolve a selected tree node to the SysML file it opens, treating + both a file source node and a file node beneath a folder source as openable, and treating folder + sources, folder groupings, non-SysML files, and no selection as not openable.' + justification: | + Opening a file is driven by the selected node, but the two node types that can denote a file are + produced by different tree-building paths - a file added directly is a childless source node, + whereas a file discovered under a folder source is a file node - so an implementation that + recognizes only one of them silently refuses to open half the files in the tree, which is exactly + the defect this requirement exists to prevent. Stating the non-openable cases explicitly keeps a + double-click on a folder or an unsupported file a safe no-op rather than an error. + tests: + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileSourceNode_ReturnsSourcePath' + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_FileNode_ReturnsFilePath' + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderSourceNode_ReturnsNull' + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_FolderNode_ReturnsNull' + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_NonSysmlPath_ReturnsNull' + - 'WorkspacePanelToolViewModel_ResolveOpenableFilePath_NullNode_ReturnsNull' diff --git a/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md b/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md index 64efd59..848da69 100644 --- a/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md +++ b/docs/verification/sysml2-workbench/app-shell-subsystem/query-dialog.md @@ -15,7 +15,11 @@ with no intervening method call anywhere in the test bodies. The pure row-projec instead call the public `BuildRow` function directly with a hand-built result entry, so the mapping from engine metadata to displayed columns can be asserted exactly - including that the depth comes from the entry's machine-readable value rather than from its human-readable detail text - without -depending on which entries a particular engine version happens to return. A single Avalonia-headless end-to-end +depending on which entries a particular engine version happens to return. Two scenarios cannot be +reached from a view-model-only test at all, because the optional-column logic lives in the view's +code-behind and addresses the grid's columns by index; those live in +`test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/QueryDialogUiTests.cs`, which shows +the real dialog `Window` on the Avalonia headless platform. A single Avalonia-headless end-to-end test in `test/OtsSoftwareTests/AvaloniaTests.cs` opens the real dialog through the real Query menu item, confirms the dialog opens on "List" with the selection-free filter control visible and the element picker hidden, selects the Describe Query Type (confirming the visibility flip) and an @@ -161,6 +165,23 @@ a null notes tooltip - the regression guard that keeps the data-driven column vi three and the results panel rendering exactly as it did before those columns existed. Verified by `QueryDialogViewModelTests.BuildRow_NonTraversingEntry_LeavesMetadataEmpty`. +**QueryDialogView_TraversalColumns_AreShownOnlyWhenRowsCarryThoseValues**: The real dialog `Window` +is shown on the Avalonia headless platform and given first a result whose rows carry no traversal +metadata, then one carrying a depth and a relation but no roll-up far endpoint. The Depth, Relation, +and Via columns stay hidden for the first, and for the second only Depth and Relation appear - so +each column is proven to track its own value independently rather than switching as a group. This +covers the requirement's display clause, which the row-projection scenarios above cannot reach +because the visibility logic lives in the view's code-behind. Verified by +`QueryDialogUiTests.QueryDialogView_TraversalColumns_AreShownOnlyWhenRowsCarryThoseValues`. + +**QueryDialogView_EntriesDataGrid_DeclaresColumnsInIndexOrderCodeBehindAssumes**: The results grid's +column headers are asserted to appear in the exact order the code-behind assumes when it toggles +columns positionally. `DataGridColumn` does not participate in Avalonia's compiled-bindings field +generation, so the code-behind has no alternative to index-based access; a reordering or insertion in +the AXAML would otherwise silently toggle the wrong column, raising no exception and producing no +binding error. This scenario converts that failure mode into a test failure. Verified by +`QueryDialogUiTests.QueryDialogView_EntriesDataGrid_DeclaresColumnsInIndexOrderCodeBehindAssumes`. + **IncludeStdlibToggle_RefreshesPickerAndRecomputesResult**: Toggling `IncludeStdlib` refreshes both `FilterOnly`'s and `Picker`'s candidate lists to reflect the new stdlib-inclusion rule, and recomputes the current result (clearing a now-unselected Describe result to the select-element diff --git a/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/QueryDialogUiTests.cs b/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/QueryDialogUiTests.cs new file mode 100644 index 0000000..15fc63e --- /dev/null +++ b/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/QueryDialogUiTests.cs @@ -0,0 +1,120 @@ +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.Threading; +using DemaConsulting.SysML2Workbench.AppShellSubsystem; +using DemaConsulting.SysML2Workbench.DiagnosticsPanelSubsystem; +using DemaConsulting.SysML2Workbench.LayoutRenderingSubsystem; +using DemaConsulting.SysML2Workbench.LoggingSubsystem; +using DemaConsulting.SysML2Workbench.ViewBuilderSubsystem; +using DemaConsulting.SysML2Workbench.ViewCatalogSubsystem; +using DemaConsulting.SysML2Workbench.WorkspaceSubsystem; + +namespace DemaConsulting.SysML2Workbench.UiTests.AppShellSubsystem; + +/// +/// Local view/view-model interaction tests for , covering the results +/// grid's optional-column behavior. That logic lives in the view's code-behind and addresses the +/// columns by index, so it cannot be reached from a view-model-only test. +/// +public sealed class QueryDialogUiTests : IDisposable +{ + private readonly string _tempLogRoot = Directory.CreateTempSubdirectory("sysml2workbench-ui-tests-query-logs-").FullName; + + /// + public void Dispose() + { + if (Directory.Exists(_tempLogRoot)) + { + Directory.Delete(_tempLogRoot, recursive: true); + } + } + + private MainWindowShell CreateShell() + { + return new MainWindowShell( + new WorkspaceModel(), + new FileWatcher(TimeSpan.FromMilliseconds(1)), + new DiagnosticsAggregator(), + new ViewCatalogPresenter(), + new LayoutInvoker(), + new DiagnosticsListView(), + new SysmlSnippetGenerator(), + new RollingFileLogger(_tempLogRoot)); + } + + /// + /// Validates that the results grid declares its columns in the exact order the code-behind's + /// ApplyResultVisibility assumes when it toggles them by index. + /// + /// + /// DataGridColumn does not participate in Avalonia's compiled-bindings field generation, so + /// the code-behind has no choice but to address columns positionally. That makes a reordering or + /// insertion in the AXAML silently toggle the wrong column - no exception, no binding error, just a + /// wrong panel. This test is the guard that turns such a change into a build-time failure instead. + /// + [AvaloniaFact] + public void QueryDialogView_EntriesDataGrid_DeclaresColumnsInIndexOrderCodeBehindAssumes() + { + // Arrange + using var shell = CreateShell(); + var view = new QueryDialogView { DataContext = new QueryDialogViewModel(shell) }; + view.Show(); + Dispatcher.UIThread.RunJobs(); + + // Act + var grid = view.FindControl("EntriesDataGrid"); + Assert.NotNull(grid); + + // Assert + Assert.Equal( + ["Qualified Name", "Kind", "Detail", "Direction", "Depth", "Relation", "Via"], + grid.Columns.Select(column => column.Header as string)); + + view.Close(); + } + + /// + /// Validates that the three traversal-metadata columns are shown only when at least one row in the + /// current result actually carries that value, so a verb that walks nothing renders exactly the + /// panel it did before those columns existed. + /// + [AvaloniaFact] + public void QueryDialogView_TraversalColumns_AreShownOnlyWhenRowsCarryThoseValues() + { + // Arrange + using var shell = CreateShell(); + var viewModel = new QueryDialogViewModel(shell); + var view = new QueryDialogView { DataContext = viewModel }; + view.Show(); + Dispatcher.UIThread.RunJobs(); + + var grid = view.FindControl("EntriesDataGrid"); + Assert.NotNull(grid); + + // Act: a result whose rows carry no traversal metadata at all + viewModel.CurrentResultRows = + [ + new QueryResultRow("A::b", "part", "detail", string.Empty, string.Empty, string.Empty, string.Empty, null), + ]; + Dispatcher.UIThread.RunJobs(); + + // Assert: none of the three optional columns intrude on a non-traversing result + Assert.False(grid.Columns[4].IsVisible); + Assert.False(grid.Columns[5].IsVisible); + Assert.False(grid.Columns[6].IsVisible); + + // Act: a result carrying a depth and relation, but still no "via" roll-up + viewModel.CurrentResultRows = + [ + new QueryResultRow("A::b", "part", "detail", string.Empty, "1", "Connect", string.Empty, null), + ]; + Dispatcher.UIThread.RunJobs(); + + // Assert: each column tracks its own value independently rather than switching as a group + Assert.True(grid.Columns[4].IsVisible); + Assert.True(grid.Columns[5].IsVisible); + Assert.False(grid.Columns[6].IsVisible); + + view.Close(); + } +} From 7703b6feeee064ce31078a575bd88b8b73489124 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 30 Jul 2026 12:32:21 -0400 Subject: [PATCH 5/6] fix: correct review-set paths so OTS and UI-test files are covered Two OTS review sets referenced filenames that have never existed: the DemaConsulting.SysML2Tools and DemaConsulting.Rendering sets used demaconsulting-sysml2tools.* and demaconsulting-rendering.*, while the real documents are named sysml2-tools.* and rendering.*. Six OTS documents were therefore in no review set at all - including the SysML2Tools design, requirements, and verification artifacts this branch modifies for the 0.2.0-beta.2 upgrade, whose evidence was consequently unreviewable. This was silent: reviewmark --lint exits 0 because a path pattern matching no file is not itself an error, so CI could not detect the gap. Also add the new QueryDialogUiTests.cs to the QueryDialog set, matching the existing convention where WorkspacePanelUiTests.cs sits in the WorkspacePanel set - without it, the tests covering the results grid's column behavior would have landed outside any review set too. Only path corrections; no review set is added, removed, or re-scoped, and the file's documented structure is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .reviewmark.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.reviewmark.yaml b/.reviewmark.yaml index e6c8581..0c84179 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -378,6 +378,7 @@ reviews: - src/**/AppShellSubsystem/QueryDialogView.axaml - src/**/AppShellSubsystem/QueryDialogView.axaml.cs - test/**/AppShellSubsystem/QueryDialogViewModelTests.cs + - test/**/AppShellSubsystem/QueryDialogUiTests.cs - id: SysML2Workbench-ElementPickerSubsystem title: Review that SysML2Workbench ElementPickerSubsystem Implementation is Correct context: @@ -426,15 +427,15 @@ reviews: - id: OTS-DemaConsultingSysML2Tools title: Review that DemaConsulting.SysML2Tools Provides Required Functionality paths: - - docs/reqstream/ots/demaconsulting-sysml2tools.yaml - - docs/design/ots/demaconsulting-sysml2tools.md - - docs/verification/ots/demaconsulting-sysml2tools.md + - docs/reqstream/ots/sysml2-tools.yaml + - docs/design/ots/sysml2-tools.md + - docs/verification/ots/sysml2-tools.md - id: OTS-DemaConsultingRendering title: Review that DemaConsulting.Rendering Provides Required Functionality paths: - - docs/reqstream/ots/demaconsulting-rendering.yaml - - docs/design/ots/demaconsulting-rendering.md - - docs/verification/ots/demaconsulting-rendering.md + - docs/reqstream/ots/rendering.yaml + - docs/design/ots/rendering.md + - docs/verification/ots/rendering.md - id: OTS-Avalonia title: Review that Avalonia Provides Required Functionality paths: From 03f24daf9c81851614978bd455054a21085ac7df Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 30 Jul 2026 14:21:22 -0400 Subject: [PATCH 6/6] docs: state the SysML2Tools rendering requirement in verifiable terms SysML2Tools-GenerateViewLayouts required the dependency to "generate layout data" from its "layout strategies", but the workbench never receives either: DiagramRenderer.RenderWorkspace turns a named view usage directly into finished output, and the library exposes no intermediate layout graph or public layout-strategy registry. The requirement therefore contradicted the design document in its own review set and could not be verified as written - its one test asserts render output, not layout data. Restate it as the contract actually relied upon: a named view usage in, finished diagram output out, in a single call. The requirement id is left unchanged so existing traceability anchors still resolve. Found by the first formal review of the OTS-DemaConsultingSysML2Tools set, which only became possible once that set's file paths were corrected; the defect is pre-existing and unrelated to the 0.2.0-beta.2 upgrade. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/reqstream/ots/sysml2-tools.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reqstream/ots/sysml2-tools.yaml b/docs/reqstream/ots/sysml2-tools.yaml index 4df5307..99d5f8c 100644 --- a/docs/reqstream/ots/sysml2-tools.yaml +++ b/docs/reqstream/ots/sysml2-tools.yaml @@ -12,9 +12,9 @@ sections: - 'LoadWorkspaceModel_ParsesAndResolvesImports' - id: 'SysML2Tools-GenerateViewLayouts' - title: 'The SysML2Workbench shall use SysML2Tools to generate layout data for supported predefined and custom views.' + title: 'The SysML2Workbench shall use SysML2Tools to render a named predefined or custom view usage into finished diagram output in a single call.' justification: | - The product depends on the existing SysML2Tools layout strategies to produce diagram structure for rendering. + The workbench deliberately holds no layout or diagram-rendering implementation of its own, so what it actually requires of the dependency is the observable end product: a named view usage in, finished renderable output out. Stating it that way keeps the requirement verifiable against the real DiagramRenderer.RenderWorkspace contract - the library exposes no intermediate layout graph or public layout-strategy registry for the workbench to consume, so a requirement phrased in terms of "layout data" could never be verified as written. tests: - 'RenderView_GeneratesLayoutGraph'