From 816fed4caec2ffa6fab2e1bc47caa093ddc417fc Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 14:50:19 -0400 Subject: [PATCH 1/7] docs(roadmap): scope Action Flow control-node AST bug and split Sequence View item Investigation found actionBodyItem has the same combined-shape AST bug as stateBodyItem (fixed in #30): the compact 'action a; then b;' idiom drops both nodes, and fork/join/decide/merge/accept/send control nodes are entirely uncaptured. Scopes this branch to the AST correctness fix plus fork/join (thick bar) and decision/merge (diamond) rendering using existing Rendering-package BadgeShape primitives, explicitly deferring a true pentagon accept/send shape (needs an external Rendering package change), swim-lanes, and item-flow annotations. Split Sequence View dynamics into its own item since it is an unrelated subsystem. --- ROADMAP.md | 57 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e26d005..638f6c5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,12 +80,50 @@ implemented first). and renderers; possibly `LayoutLabel`/compartment tweaks. **Visual gate:** a documented requirement/part renders its note and full compartments. -### View dynamics refinements - -- **Sequence View:** populate `LayoutActivation` execution bars; combined-fragment boxes - (alt/opt/loop); async/reply message styling. -- **Action Flow View:** **fork/join** thick bars, **decision/merge** diamonds, accept/send - action shapes; optional **swim-lanes** via `LayoutBand`; item-flow edge annotations. +### Action Flow View: control-node/successor AST correctness + fork/join/decision/merge shapes + +Investigation (mirroring the pattern found in the State Transition View item above — same grammar +family, same class of bug) found `actionBodyItem`'s combined shape has the identical AST gap, plus +control nodes are entirely uncaptured today: + +1. **Attached/implicit-source successions are silently dropped (both the action AND its successor).** + `actionBodyItem: (sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)*` + has no `AstBuilder` visitor, so the compact, common idiom `action a1; then a2;` (successor + implied by adjacency, no explicit `succession`/`first` statement) falls through to ANTLR's + default aggregation and loses both nodes — the same failure mode fixed for `stateBodyItem` in + the State Transition View branch. +2. **Fork/join/decide/merge/accept/send control nodes are entirely unmodeled.** The grammar's + `actionNode: controlNode | sendNode | acceptNode | assignmentNode | terminateNode | ifNode | + whileLoopNode | forLoopNode` (`controlNode: mergeNode | decisionNode | joinNode | forkNode`) has + zero `AstBuilder` visitors — not even a bare `fork f1;` registers today, so every Action Flow + View renders every node as an identical rounded-rectangle "action" box regardless of its real + control-node kind, and successions through them are lost too (compounding problem 1). +3. **Renderer shape primitives already exist for the two structural kinds, just unused.** The + `DemaConsulting.Rendering` package already defines `BadgeShape.HorizontalBar`/`VerticalBar` + (usable for fork/join thick bars) and `BadgeShape.Diamond` (usable for decision/merge) — + `ActionFlowViewLayoutStrategy` just never produces a `LayoutBadge` for anything but the + start/done markers today. There is **no pentagon primitive** in the Rendering package's + `BoxShape`/`BadgeShape` enums for a true UML accept/send action shape — adding one requires a + change to the separate `DemaConsulting.Rendering` package, out of scope for this repo/branch. + +**Scope (this branch):** `AstBuilder` (new handling for the `actionBehaviorMember +(actionTargetSuccessionMember)*` combined shape, mirroring the state-body fix; new visitors for +`controlNode`/`sendNode`/`acceptNode` registering a distinguishable node kind); +`ActionFlowViewLayoutStrategy` (render fork/join as `LayoutBadge(BadgeShape.HorizontalBar | +VerticalBar)`, decision/merge as `LayoutBadge(BadgeShape.Diamond)`, keeping accept/send as +keyword-labelled rounded-rectangle boxes for now, pending a future pentagon primitive). +**Explicitly deferred (separate future items, not this branch):** a true pentagon accept/send +shape (needs a `DemaConsulting.Rendering` package change); swim-lanes via `LayoutBand`; item-flow +edge annotations (no item-flow/payload capture exists in the AST yet); the Sequence View dynamics +item below (different subsystem, separate branch). +**Visual gate:** an action flow with a fork feeding two parallel actions that rejoin renders a +thick bar in each direction; a guarded decision/merge renders diamonds; the compact `action a; +then b;` idiom renders correctly with no dropped nodes. + +### Sequence View dynamics + +- Populate `LayoutActivation` execution bars; combined-fragment boxes (alt/opt/loop); async/reply + message styling. - **Sequence dynamic-view compatibility check (known limitation, carried over from "Dynamic (ad-hoc) views", done):** `DynamicViewSynthesizer`'s `--view-type sequence` pre-check accepts any target with at least one nested `message` usage (the cheap, necessary-but-not-sufficient @@ -96,10 +134,9 @@ and renderers; possibly `LayoutLabel`/compartment tweaks. `DynamicViewSynthesizer` or surfacing `SequenceViewLayoutStrategy`'s own lifeline-resolution result back to the synthesizer. -**Scope:** `SequenceViewLayoutStrategy`, `ActionFlowViewLayoutStrategy`, renderer shape -primitives (bar, diamond, pentagon, note). `LayoutActivation`/`LayoutBand` already defined. -**Visual gate:** sequence shows activation bars + a fragment; action flow shows a fork/join and -a decision/merge with correct shapes. +**Scope:** `SequenceViewLayoutStrategy`, renderer shape primitives (note). `LayoutActivation` +already defined. +**Visual gate:** sequence shows activation bars + a fragment. ### State Transition View: attached-transition states, entry/exit actions, inherited pseudostate features From 0a67cae21f5c16a03cb3af495429a6e295162db7 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 15:55:17 -0400 Subject: [PATCH 2/7] Support attached-succession action bodies and control-node features - AstBuilder: add VisitActionBodyItem synthesizing SysmlTransitionNode(s) for the initialNodeMember/actionTargetSuccessionMember and actionBehaviorMember/ actionTargetSuccessionMember combined shapes, reusing the MultiNodeCapture sentinel; add BuildActionTargetSuccession covering targetSuccession/ guardedTargetSuccession/defaultTargetSuccession. - Add VisitMergeNode/VisitDecisionNode/VisitJoinNode/VisitForkNode/ VisitAcceptNode/VisitSendNode building minimal SysmlFeatureNodes via a shared BuildActionNodeFeature helper; anonymous control nodes get a synthesized internal \\$\ name so their successions still wire correctly. - Add WorkspaceLoader and OMG corpus regression tests covering the compact action idiom, multiple attached successions, bare/attached first markers, anonymous/named control nodes, and guarded/default target successions. --- .../Semantic/Model/AstBuilder.cs | 211 +++++++++++- .../Parser/OmgModelsTests.cs | 85 +++++ .../Semantic/WorkspaceLoaderTests.cs | 308 ++++++++++++++++++ 3 files changed, 600 insertions(+), 4 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index f20ab96..5eaf98d 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -250,10 +250,13 @@ private sealed class AnnotationCapture : SysmlNode /// generic ANTLR Visit pipeline from a single grammar alternative that actually /// produces several sibling AST nodes (e.g. a stateBodyItem's attached-transition /// shapes: the preceding state/entry-action usage plus one - /// per attached transition). Never appears in a real - /// list — always intercepts it and flattens its - /// into the owning node's children instead, mirroring how - /// is intercepted and routed into + /// per attached transition; or an actionBodyItem's analogous combined-succession + /// shapes: (sourceSuccessionMember)? actionBehaviorMember + /// (actionTargetSuccessionMember)* and initialNodeMember + /// (actionTargetSuccessionMember)*). Never appears in a real + /// list — always intercepts + /// it and flattens its into the owning node's children instead, + /// mirroring how is intercepted and routed into /// rather than becoming a child itself. /// private sealed class MultiNodeCapture : SysmlNode @@ -261,6 +264,17 @@ private sealed class MultiNodeCapture : SysmlNode public required IReadOnlyList Nodes { get; init; } } + /// + /// Monotonically-increasing counter used to synthesize a unique internal + /// $<keyword><n> name for an anonymous control/accept/send node (see + /// ). A synthetic name is required (rather than leaving + /// null, as plain anonymous actions do) because anonymous + /// fork/decide/send nodes are the dominant idiom in the real OMG + /// corpus and must still render as a distinct shape and act as the implicit + /// of their attached successions. + /// + private int _anonymousNodeCounter; + /// public override SysmlNode? VisitPartDefinition(SysMLv2Parser.PartDefinitionContext context) { @@ -409,6 +423,195 @@ private sealed class MultiNodeCapture : SysmlNode }; } + /// + /// Dispatches a single actionBodyItem alternative to the appropriate builder logic. + /// Mirrors 's dispatch shape. nonBehaviorBodyItem and + /// guardedSuccessionMember already produce exactly one AST node via default ANTLR + /// visitor dispatch, so they are passed through unchanged (the latter's own internal shape + /// is left untouched here, exactly as leaves its own + /// untouched alternatives unhandled). The two combined-succession shapes — + /// initialNodeMember (actionTargetSuccessionMember)* (e.g. first start; or + /// first start then off;) and (sourceSuccessionMember)? actionBehaviorMember + /// (actionTargetSuccessionMember)* (e.g. the compact action a1; then a2; idiom) — + /// each may produce more than one sibling node (the referenced/behavior node itself, plus + /// one per attached target succession whose + /// Source is implicit); without this override, ANTLR's default VisitChildren + /// aggregation silently discards every child result but the last, dropping the earlier + /// nodes and losing successions entirely. + /// + public override SysmlNode? VisitActionBodyItem(SysMLv2Parser.ActionBodyItemContext context) + { + if (context.nonBehaviorBodyItem() is { } nonBehaviorBodyItem) + { + return Visit(nonBehaviorBodyItem); + } + + if (context.guardedSuccessionMember() is { } guardedSuccessionMember) + { + return Visit(guardedSuccessionMember); + } + + if (context.initialNodeMember() is { } initialNodeMember) + { + var targets = context.actionTargetSuccessionMember(); + if (targets.Length == 0) + { + // Bare `first start;` carries no attached succession. Unlike state-transition + // pseudostates, ActionFlowViewLayoutStrategy infers its start/done markers purely + // from succession topology (no declarative initial-marker concept exists for + // actions), so there is nothing useful to synthesize here — unchanged from today. + return null; + } + + var sourceName = initialNodeMember.qualifiedName()?.GetText(); + var nodes = targets + .Select(target => (SysmlNode)BuildActionTargetSuccession(sourceName, target.actionTargetSuccession())) + .ToList(); + + return nodes.Count == 1 ? nodes[0] : new MultiNodeCapture { Nodes = nodes }; + } + + if (context.actionBehaviorMember() is { } actionBehaviorMember) + { + var behaviorNode = Visit(actionBehaviorMember); + var targets = context.actionTargetSuccessionMember(); + if (behaviorNode is null || targets.Length == 0) + { + return behaviorNode; + } + + var sourceName = behaviorNode.Name; + var nodes = new List { behaviorNode }; + foreach (var target in targets) + { + nodes.Add(BuildActionTargetSuccession(sourceName, target.actionTargetSuccession())); + } + + return new MultiNodeCapture { Nodes = nodes }; + } + + return null; + } + + /// + /// Builds the for one actionTargetSuccession + /// attached after an action-flow node (e.g. then a2;, if g then a3;, or + /// else a4;), whose Source is implicitly the preceding node's name (never + /// present in the grammar itself). Covers all three alternatives: targetSuccession + /// (unguarded, target via a bare ), + /// guardedTargetSuccession (if ... then ..., guard captured), and + /// defaultTargetSuccession (else ..., no guard expression exists in the + /// grammar for this alternative — a known, documented simplification). The trailing + /// usageBody() is ignored, mirroring 's + /// treatment of targetTransitionUsage's own trailing actionBody(). + /// + private SysmlTransitionNode BuildActionTargetSuccession( + string? sourceName, + SysMLv2Parser.ActionTargetSuccessionContext? succession) + { + if (succession?.targetSuccession() is { } targetSuccession) + { + return new SysmlTransitionNode + { + Source = sourceName, + Target = ConnectorEndReference(targetSuccession.connectorEndMember()), + }; + } + + if (succession?.guardedTargetSuccession() is { } guardedTargetSuccession) + { + return new SysmlTransitionNode + { + Source = sourceName, + Target = ConnectorEndReference( + guardedTargetSuccession.transitionSuccessionMember()?.transitionSuccession()?.connectorEndMember()), + Guard = guardedTargetSuccession.guardExpressionMember()?.ownedExpression()?.GetText(), + }; + } + + if (succession?.defaultTargetSuccession() is { } defaultTargetSuccession) + { + return new SysmlTransitionNode + { + Source = sourceName, + Target = ConnectorEndReference( + defaultTargetSuccession.transitionSuccessionMember()?.transitionSuccession()?.connectorEndMember()), + }; + } + + return new SysmlTransitionNode { Source = sourceName, Target = null }; + } + + /// + public override SysmlNode? VisitMergeNode(SysMLv2Parser.MergeNodeContext context) + { + return BuildActionNodeFeature(context.usageDeclaration(), "merge"); + } + + /// + public override SysmlNode? VisitDecisionNode(SysMLv2Parser.DecisionNodeContext context) + { + return BuildActionNodeFeature(context.usageDeclaration(), "decide"); + } + + /// + public override SysmlNode? VisitJoinNode(SysMLv2Parser.JoinNodeContext context) + { + return BuildActionNodeFeature(context.usageDeclaration(), "join"); + } + + /// + public override SysmlNode? VisitForkNode(SysMLv2Parser.ForkNodeContext context) + { + return BuildActionNodeFeature(context.usageDeclaration(), "fork"); + } + + /// + public override SysmlNode? VisitAcceptNode(SysMLv2Parser.AcceptNodeContext context) + { + var declaration = context.acceptNodeDeclaration()?.actionNodeUsageDeclaration(); + return BuildActionNodeFeature(declaration?.usageDeclaration(), "accept"); + } + + /// + public override SysmlNode? VisitSendNode(SysMLv2Parser.SendNodeContext context) + { + var declaration = context.actionNodeUsageDeclaration()?.usageDeclaration() + ?? context.actionUsageDeclaration()?.usageDeclaration(); + return BuildActionNodeFeature(declaration, "send"); + } + + /// + /// Builds a minimal, deliberately non-behavioral for a + /// merge/decide/join/fork/accept/send action-flow + /// control node, registering only its (possibly synthesized) name with no children. When + /// yields no declared name — the dominant real-world idiom for + /// fork/decide/send per the OMG training corpus (e.g. then fork; + /// immediately followed by several then <name>; successions) — a synthetic + /// $<keyword><n> name is assigned via + /// so the node still renders as a distinct shape and can still act as those successions' + /// implicit source; its stays + /// (this is purely local succession-wiring data, never registered in the symbol table or + /// referenced across files/scopes). The nested actionBody's internal semantics are + /// deliberately NOT modeled, mirroring . + /// assignmentNode/terminateNode/ifNode/whileLoopNode/ + /// forLoopNode remain an intentional, out-of-scope gap — not handled here or + /// anywhere else in . + /// + private SysmlFeatureNode BuildActionNodeFeature(SysMLv2Parser.UsageDeclarationContext? decl, string keyword) + { + var name = GetDeclaredName(decl?.identification()) ?? $"${keyword}{_anonymousNodeCounter++}"; + + return new SysmlFeatureNode + { + Name = name, + QualifiedName = null, + FeatureKeyword = keyword, + Children = Array.Empty(), + Annotations = Array.Empty(), + }; + } + /// public override SysmlNode? VisitStateDefinition(SysMLv2Parser.StateDefinitionContext context) { diff --git a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs index 899bb32..2a8f8c5 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs @@ -235,4 +235,89 @@ public async Task Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions() Assert.Contains(actionFeatures, f => f.FeatureKeyword == "do" && f.Name == "providePower"); Assert.Contains(actionFeatures, f => f.FeatureKeyword == "exit" && f.Name == "applyParkingBrake"); } + + /// + /// The dedicated 17.Control corpus fixtures exercise the two Action Flow View + /// sub-problems together: the combined-succession actionBodyItem shapes (the compact + /// action a1; then a2; idiom, plus first start;/first start then X;) + /// and the six control-node kinds (merge/decide/join/fork/ + /// accept/send), including the dominant real-world anonymous-node idiom + /// (then fork;/then decide; with no declared name, immediately followed by + /// several then <name>;/guarded target successions). Assertions are on raw + /// / text + /// and feature kind/name — not full reference resolution — per the design decision that an + /// anonymous control node's synthesized $-prefixed name is expected to produce an + /// "Unresolved reference" warning (cosmetic only: ActionFlowViewLayoutStrategy reads + /// the raw text directly, never ResolvedEdges). + /// + [Fact] + public async Task ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge() + { + var omgRoot = FindOmgModelsRoot(); + var forkJoinFile = Path.Combine(omgRoot, "training", "17.Control", "ForkJoinExample.sysml"); + var decisionFile = Path.Combine(omgRoot, "training", "17.Control", "DecisionExample.sysml"); + var controlNodeFile = Path.Combine(omgRoot, "examples", "SimpleTests", "ControlNodeTest.sysml"); + Assert.All( + [forkJoinFile, decisionFile, controlNodeFile], + f => Assert.True(File.Exists(f), $"Expected fixture not found: {f}")); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([forkJoinFile, decisionFile, controlNodeFile], stdlibTable); + + // ForkJoinExample.sysml: an anonymous fork feeding 3 branches that all join back together. + var brake = (SysmlDefinitionNode)result.Workspace!.Declarations["'Fork Join Example'::Brake"]; + var brakeFork = Assert.Single( + brake.Children.OfType(), f => f.FeatureKeyword == "fork"); + Assert.NotNull(brakeFork.Name); + Assert.StartsWith("$", brakeFork.Name, StringComparison.Ordinal); + + var forkSuccessions = brake.Children + .OfType() + .Where(t => t.Source == brakeFork.Name) + .ToList(); + Assert.Equal(3, forkSuccessions.Count); + Assert.Contains(forkSuccessions, t => t.Target == "monitorBrakePedal"); + Assert.Contains(forkSuccessions, t => t.Target == "monitorTraction"); + Assert.Contains(forkSuccessions, t => t.Target == "braking"); + + var joinNode = Assert.Single( + brake.Children.OfType(), f => f.FeatureKeyword == "join"); + Assert.Equal("joinNode", joinNode.Name); + Assert.Equal(3, brake.Children.OfType().Count(t => t.Target == "joinNode")); + + // DecisionExample.sysml: an anonymous decide with two guarded successions, plus a named + // merge that the decide's default-path action succession eventually rejoins. + var chargeBattery = (SysmlDefinitionNode)result.Workspace!.Declarations["'Decision Example'::ChargeBattery"]; + var decide = Assert.Single( + chargeBattery.Children.OfType(), f => f.FeatureKeyword == "decide"); + Assert.NotNull(decide.Name); + Assert.StartsWith("$", decide.Name, StringComparison.Ordinal); + + var decideSuccessions = chargeBattery.Children + .OfType() + .Where(t => t.Source == decide.Name) + .ToList(); + Assert.Equal(2, decideSuccessions.Count); + Assert.Contains(decideSuccessions, t => t.Target == "addCharge" && t.Guard == "monitor.batteryCharge<100"); + Assert.Contains(decideSuccessions, t => t.Target == "endCharging" && t.Guard == "monitor.batteryCharge>=100"); + + Assert.Contains( + chargeBattery.Children.OfType(), + f => f.FeatureKeyword == "merge" && f.Name == "continueCharging"); + + // ControlNodeTest.sysml: fully named fork/join/merge — the richest, most reliable fixture. + var controlNodeTest = (SysmlDefinitionNode)result.Workspace!.Declarations["ControlNodeTest"]; + var controlFeatures = controlNodeTest.Children.OfType().ToList(); + Assert.Contains(controlFeatures, f => f.FeatureKeyword == "join" && f.Name == "J"); + Assert.Contains(controlFeatures, f => f.FeatureKeyword == "fork" && f.Name == "F"); + Assert.Contains(controlFeatures, f => f.FeatureKeyword == "merge" && f.Name == "M"); + + var controlTransitions = controlNodeTest.Children.OfType().ToList(); + Assert.Contains(controlTransitions, t => t.Source == "A1" && t.Target == "J"); + Assert.Contains(controlTransitions, t => t.Source == "A2" && t.Target == "J"); + Assert.Contains(controlTransitions, t => t.Source == "F" && t.Target == "B1"); + Assert.Contains(controlTransitions, t => t.Source == "F" && t.Target == "B2"); + Assert.Contains(controlTransitions, t => t.Source == "B1" && t.Target == "M"); + Assert.Contains(controlTransitions, t => t.Source == "B2" && t.Target == "M"); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index f880f95..62e21e0 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -3908,6 +3908,314 @@ view def V {} } } + /// + /// The compact action a1; then a2; idiom (the + /// (sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)* + /// actionBodyItem alternative) resolves both the action and its attached succession. + /// Before this fix, ANTLR's default VisitChildren aggregation silently dropped the + /// action, keeping only the succession (or vice versa). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_CompactActionThenIdiom_ResolvesBothNodes() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action a1; + then a2; + action a2; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var actions = flow.Children + .OfType() + .Count(f => f.FeatureKeyword == "action"); + Assert.Equal(2, actions); + + var succession = flow.Children + .OfType() + .Single(); + Assert.Equal("a1", succession.Source); + Assert.Equal("a2", succession.Target); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Multiple actionTargetSuccessionMembers attached after a single + /// actionBehaviorMember (e.g. a fork's three outgoing branches) are all captured, + /// each sharing the preceding node's name as their implicit Source. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action a; + then fork f; + then b1; + then b2; + action b1; + action b2; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var successions = flow.Children + .OfType() + .Where(t => t.Source == "f") + .ToList(); + Assert.Equal(2, successions.Count); + Assert.Contains(successions, t => t.Target == "b1"); + Assert.Contains(successions, t => t.Target == "b2"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A bare first start; (no attached target succession) produces no succession node + /// — unchanged from today, since ActionFlowViewLayoutStrategy infers its start/done + /// markers purely from succession topology, not a declarative initial marker. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_BareInitialNodeMember_ProducesNoSuccession() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action a; + first a; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + Assert.Empty(flow.Children.OfType()); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// first start then off; (an initialNodeMember with an attached target + /// succession) synthesizes a + /// whose Source is the referenced qualified name and whose Target is the + /// attached succession's target. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_InitialNodeMemberWithAttachedSuccession_SynthesizesTransition() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + first start then off; + action off; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var succession = flow.Children + .OfType() + .Single(); + Assert.Equal("start", succession.Source); + Assert.Equal("off", succession.Target); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Anonymous fork/decide/join/merge/accept/send + /// control nodes each register a + /// with the correct FeatureKeyword and a non-null synthetic $-prefixed + /// Name (rather than the name left by a genuinely anonymous + /// plain action), since fork/decide/send are the dominant real-world idiom for anonymous + /// control nodes per the OMG training corpus. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_AnonymousControlNodes_SynthesizeNames() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action a; + then fork; + then b1; + then b2; + action b1; + action b2; + then join; + then decide; + if true then b1; + else b2; + then merge; + accept sig; + then send; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var features = flow.Children.OfType().ToList(); + + foreach (var keyword in new[] { "fork", "join", "decide", "merge" }) + { + var feature = features.Single(f => f.FeatureKeyword == keyword); + Assert.NotNull(feature.Name); + Assert.StartsWith("$", feature.Name, StringComparison.Ordinal); + Assert.Null(feature.QualifiedName); + } + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Named control nodes (e.g. fork f;, join j;) keep their declared name + /// instead of a synthesized one. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_NamedControlNodes_KeepDeclaredName() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action a; + then fork f; + then b1; + then b2; + action b1; + action b2; + then join j; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var features = flow.Children.OfType().ToList(); + + Assert.Contains(features, f => f.FeatureKeyword == "fork" && f.Name == "f"); + Assert.Contains(features, f => f.FeatureKeyword == "join" && f.Name == "j"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Both the guarded (if ... then ...) and default (else ...) + /// actionTargetSuccession variants extract a target; only the guarded form captures + /// a guard expression (the grammar provides none for the else alternative). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_GuardedAndDefaultActionTargetSuccession_ExtractTargets() + { + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package AF { + action def Flow { + action monitor; + then decide d; + if true then addCharge; + else endCharging; + action addCharge; + action endCharging; + } + } + """, TestContext.Current.CancellationToken); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + Assert.NotNull(result.Workspace); + var flow = Assert.IsType( + result.Workspace!.Declarations["AF::Flow"]); + var successions = flow.Children + .OfType() + .Where(t => t.Source == "d") + .ToList(); + Assert.Equal(2, successions.Count); + + var guarded = successions.Single(t => t.Target == "addCharge"); + Assert.Equal("true", guarded.Guard); + + var defaulted = successions.Single(t => t.Target == "endCharging"); + Assert.Null(defaulted.Guard); + } + finally + { + File.Delete(tempFile); + } + } + /// /// Finds the test/SysMLModels directory relative to the test assembly. /// From b5943cedaca65a6b1558df43dbd89eead0f6ab5b Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 15:55:28 -0400 Subject: [PATCH 3/7] Render fork/join/decision/merge control nodes as badges in Action Flow View - ActionFlowViewLayoutStrategy: widen CollectActions/FindRoot to recognize the merge/decide/join/fork/accept/send FeatureKeyword values in addition to action; add ComputeActionSize/MakeActionNode routing so fork/join render as a LayoutBadge(BadgeShape.HorizontalBar), decision/merge as LayoutBadge(BadgeShape.Diamond), and accept/send keep the rounded-rectangle box shape but show their own Keyword instead of the hard-coded action text. - Blank rendered labels derived from AstBuilder-synthesized \\$\-prefixed internal names. - Ordinary-action rendering path left unchanged to avoid regressing existing gallery/test fixtures. - Add layout tests covering fork/join bars, decision/merge diamonds, accept/send boxes, synthetic-name blank labels, and the compact idiom. --- .../Internal/ActionFlowViewLayoutStrategy.cs | 126 ++++++++--- .../ActionFlowViewLayoutStrategyTests.cs | 197 ++++++++++++++++++ 2 files changed, 297 insertions(+), 26 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ActionFlowViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ActionFlowViewLayoutStrategy.cs index 8aa2a8d..43c034a 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ActionFlowViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/ActionFlowViewLayoutStrategy.cs @@ -35,6 +35,13 @@ internal sealed class ActionFlowViewLayoutStrategy : ILayoutStrategy /// Diameter of the start and done markers. private const double MarkerSize = 20.0; + /// + /// Intrinsic size (both width and height) of a control-node badge (fork/join bar, decision/merge + /// diamond), independent of the box-sizing heuristics used for ordinary actions and + /// accept/send boxes. + /// + private const double ControlNodeBadgeSize = 24.0; + /// /// Gap between a start/done marker and the adjacent action layer. Mirrors the layered algorithm's /// between-layer corridor width (70 px) so the control markers keep the same vertical rhythm as @@ -42,8 +49,14 @@ internal sealed class ActionFlowViewLayoutStrategy : ILayoutStrategy /// private const double MarkerLayerGap = 70.0; - /// An action with its computed box size. - private sealed record ActionItem(string Name, double Width, double Height); + /// + /// An action-flow node with its computed intrinsic size and kind. is one + /// of "action" (ordinary action box, or a succession-endpoint-only fallback — the default), + /// "merge"/"decide"/"join"/"fork" (rendered as a badge), or + /// "accept"/"send" (rendered as a rounded-rectangle box, like an ordinary action, + /// but labelled with its own distinct keyword). + /// + private sealed record ActionItem(string Name, double Width, double Height, string Kind = "action"); /// public LayoutTree BuildLayout(ViewContext context, RenderOptions options) @@ -102,7 +115,7 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) var nodes = new List(); for (var i = 0; i < actions.Count; i++) { - nodes.Add(MakeActionBox(actions[i], rects[i])); + nodes.Add(MakeActionNode(actions[i], rects[i])); } var crossings = AddSuccessionEdges(edges, placed.EdgePolylines, rects, offsetX, offsetY, nodes); @@ -151,7 +164,7 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) } var successions = def.Children.OfType().Count(); - var actions = def.Children.OfType().Count(f => f.FeatureKeyword == "action"); + var actions = def.Children.OfType().Count(f => IsActionFlowKeyword(f.FeatureKeyword)); var score = (successions * 100) + actions; var scoreBetter = score > bestScore; var isBetter = scope is not null @@ -185,7 +198,7 @@ private static (IReadOnlyList Actions, Dictionary Index var actions = new List(); var index = new Dictionary(StringComparer.Ordinal); - void Add(string name) + void Add(string name, string kind) { if (index.ContainsKey(name)) { @@ -193,13 +206,13 @@ void Add(string name) } index[name] = actions.Count; - var (width, height) = ComputeActionSize(name, theme); - actions.Add(new ActionItem(name, width, height)); + var (width, height) = ComputeActionSize(name, kind, theme); + actions.Add(new ActionItem(name, width, height, kind)); } foreach (var feature in root.Children.OfType()) { - if (feature.FeatureKeyword != "action" || feature.Name is null) + if (!IsActionFlowKeyword(feature.FeatureKeyword) || feature.Name is null) { continue; } @@ -210,19 +223,19 @@ void Add(string name) continue; } - Add(feature.Name); + Add(feature.Name, feature.FeatureKeyword); } foreach (var succession in root.Children.OfType()) { if (LastSegment(succession.Source) is { } s) { - Add(s); + Add(s, "action"); } if (LastSegment(succession.Target) is { } t) { - Add(t); + Add(t, "action"); } } @@ -247,28 +260,89 @@ void Add(string name) return result; } - /// Computes the intrinsic size of an action box. - private static (double Width, double Height) ComputeActionSize(string name, Theme theme) + /// Computes the intrinsic size of an action-flow node for the given kind. + /// + /// Ordinary actions (kind "action") and accept/send boxes use the same name-based sizing + /// heuristic as before this branch (accept/send previously always used kind "action" + /// sizing too, so this is unchanged for them). Badge kinds (fork/join/decide/merge) use a fixed + /// square instead, independent of the node's name length. + /// + private static (double Width, double Height) ComputeActionSize(string name, string kind, Theme theme) { + if (IsBadgeKind(kind)) + { + return (ControlNodeBadgeSize, ControlNodeBadgeSize); + } + var labelWidth = (name.Length * theme.FontSizeTitle * CharWidthFactor) + (4.0 * theme.LabelPadding); var width = Math.Max(MinActionWidth, labelWidth); var height = BoxMetrics.TitleAreaHeight(theme, hasLabel: true, hasKeyword: true) + theme.LabelPadding; return (width, height); } - /// Creates a rounded-rectangle action box at the given position. - private static LayoutBox MakeActionBox(ActionItem action, Rect rect) => - new( - X: rect.X, - Y: rect.Y, - Width: rect.Width, - Height: rect.Height, - Label: action.Name, - Depth: 1, - Shape: BoxShape.RoundedRectangle, - Compartments: [], - Children: [], - Keyword: "action"); + /// + /// Returns whether names an action-flow node kind recognized by this + /// layout strategy: the original ordinary "action" plus the control-node/accept/send + /// keywords added for fork/join/decision/merge/accept/send support. + /// + private static bool IsActionFlowKeyword(string keyword) => + keyword is "action" or "merge" or "decide" or "join" or "fork" or "accept" or "send"; + + /// Returns whether renders as a rather than a box. + private static bool IsBadgeKind(string kind) => kind is "merge" or "decide" or "join" or "fork"; + + /// + /// Creates the layout node for an action-flow item at the given placed rectangle: a + /// badge for decision/merge, a + /// badge for fork/join, or a rounded-rectangle box (labelled with its own distinct keyword) for + /// ordinary actions and accept/send nodes. Ordinary-action ("action") rendering is + /// byte-for-byte identical to before this branch. A synthetic $-prefixed internal name + /// (assigned by AstBuilder to an anonymous control node) is never shown as a label. + /// + private static LayoutNode MakeActionNode(ActionItem action, Rect rect) + { + var centreX = rect.X + (rect.Width / 2.0); + var centreY = rect.Y + (rect.Height / 2.0); + var label = action.Name.StartsWith("$", StringComparison.Ordinal) ? null : action.Name; + + switch (action.Kind) + { + case "merge": + case "decide": + return new LayoutBadge(centreX, centreY, rect.Width, BadgeShape.Diamond, label); + + case "fork": + case "join": + return new LayoutBadge(centreX, centreY, rect.Width, BadgeShape.HorizontalBar, label); + + case "accept": + case "send": + return new LayoutBox( + X: rect.X, + Y: rect.Y, + Width: rect.Width, + Height: rect.Height, + Label: label ?? string.Empty, + Depth: 1, + Shape: BoxShape.RoundedRectangle, + Compartments: [], + Children: [], + Keyword: action.Kind); + + default: + return new LayoutBox( + X: rect.X, + Y: rect.Y, + Width: rect.Width, + Height: rect.Height, + Label: action.Name, + Depth: 1, + Shape: BoxShape.RoundedRectangle, + Compartments: [], + Children: [], + Keyword: "action"); + } + } /// /// Adds the succession flow edges (top-to-bottom) between action boxes, using the orthogonal diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs index dd57784..fda2cba 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/ActionFlowViewLayoutStrategyTests.cs @@ -666,4 +666,201 @@ public void ActionFlowView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot( Assert.Contains(boxes, b => b.Label == "b1"); Assert.Contains(boxes, b => b.Label == "b2"); } + + /// + /// A named fork feeding two actions that later join renders a + /// badge at both the fork and the join, with the + /// succession edges wired between them (regression-guarding the ordinary-action rendering + /// path: b1/b2 still render as plain boxes). + /// + [Fact] + public void ActionFlowView_BuildLayout_ForkAndJoin_RenderHorizontalBarBadges() + { + var strategy = new ActionFlowViewLayoutStrategy(); + var process = new SysmlDefinitionNode + { + Name = "Process", + QualifiedName = "P::Process", + DefinitionKeyword = "action def", + Children = + [ + new SysmlFeatureNode { Name = "a", QualifiedName = "P::Process::a", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "f", QualifiedName = null, FeatureKeyword = "fork" }, + new SysmlFeatureNode { Name = "b1", QualifiedName = "P::Process::b1", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "b2", QualifiedName = "P::Process::b2", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "j", QualifiedName = null, FeatureKeyword = "join" }, + new SysmlTransitionNode { Source = "a", Target = "f" }, + new SysmlTransitionNode { Source = "f", Target = "b1" }, + new SysmlTransitionNode { Source = "f", Target = "b2" }, + new SysmlTransitionNode { Source = "b1", Target = "j" }, + new SysmlTransitionNode { Source = "b2", Target = "j" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::Process"] = process } + }; + var context = new ViewContext("ActionFlow", workspace); + var options = new RenderOptions(Themes.Light); + + var layout = strategy.BuildLayout(context, options); + + var bars = layout.Nodes.OfType().Where(b => b.Shape == BadgeShape.HorizontalBar).ToList(); + Assert.Equal(2, bars.Count); + Assert.Contains(bars, b => b.Label == "f"); + Assert.Contains(bars, b => b.Label == "j"); + + var actionBoxes = layout.Nodes.OfType().Where(b => b.Keyword == "action").ToList(); + Assert.Equal(3, actionBoxes.Count); + Assert.Contains(actionBoxes, b => b.Label == "a"); + Assert.Contains(actionBoxes, b => b.Label == "b1"); + Assert.Contains(actionBoxes, b => b.Label == "b2"); + } + + /// + /// A guarded decision feeding a merge renders a badge for + /// both the decision and the merge node. + /// + [Fact] + public void ActionFlowView_BuildLayout_DecisionAndMerge_RenderDiamondBadges() + { + var strategy = new ActionFlowViewLayoutStrategy(); + var process = new SysmlDefinitionNode + { + Name = "Process", + QualifiedName = "P::Process", + DefinitionKeyword = "action def", + Children = + [ + new SysmlFeatureNode { Name = "monitor", QualifiedName = "P::Process::monitor", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "d", QualifiedName = null, FeatureKeyword = "decide" }, + new SysmlFeatureNode { Name = "addCharge", QualifiedName = "P::Process::addCharge", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "m", QualifiedName = null, FeatureKeyword = "merge" }, + new SysmlTransitionNode { Source = "monitor", Target = "d" }, + new SysmlTransitionNode { Source = "d", Target = "addCharge", Guard = "monitor.charge<100" }, + new SysmlTransitionNode { Source = "d", Target = "m" }, + new SysmlTransitionNode { Source = "addCharge", Target = "m" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::Process"] = process } + }; + var context = new ViewContext("ActionFlow", workspace); + var options = new RenderOptions(Themes.Light); + + var layout = strategy.BuildLayout(context, options); + + var diamonds = layout.Nodes.OfType().Where(b => b.Shape == BadgeShape.Diamond).ToList(); + Assert.Equal(2, diamonds.Count); + Assert.Contains(diamonds, b => b.Label == "d"); + Assert.Contains(diamonds, b => b.Label == "m"); + } + + /// + /// Accept/send nodes render as rounded-rectangle boxes (like ordinary actions) but keep + /// their own distinct keyword instead of the hard-coded "action" keyword. + /// + [Fact] + public void ActionFlowView_BuildLayout_AcceptAndSend_RenderBoxesWithDistinctKeyword() + { + var strategy = new ActionFlowViewLayoutStrategy(); + var process = new SysmlDefinitionNode + { + Name = "Process", + QualifiedName = "P::Process", + DefinitionKeyword = "action def", + Children = + [ + new SysmlFeatureNode { Name = "sig", QualifiedName = "P::Process::sig", FeatureKeyword = "accept" }, + new SysmlFeatureNode { Name = "msg", QualifiedName = "P::Process::msg", FeatureKeyword = "send" }, + new SysmlTransitionNode { Source = "sig", Target = "msg" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::Process"] = process } + }; + var context = new ViewContext("ActionFlow", workspace); + var options = new RenderOptions(Themes.Light); + + var layout = strategy.BuildLayout(context, options); + + var boxes = layout.Nodes.OfType().ToList(); + Assert.Contains(boxes, b => b.Keyword == "accept" && b.Label == "sig"); + Assert.Contains(boxes, b => b.Keyword == "send" && b.Label == "msg"); + Assert.DoesNotContain(layout.Nodes.OfType(), b => b.Shape is BadgeShape.Diamond or BadgeShape.HorizontalBar); + } + + /// + /// A synthetic $-prefixed anonymous control-node name (as synthesized by + /// AstBuilder for an unnamed fork/decide/etc.) is never shown as the badge's label. + /// + [Fact] + public void ActionFlowView_BuildLayout_SyntheticControlNodeName_RendersBlankLabel() + { + var strategy = new ActionFlowViewLayoutStrategy(); + var process = new SysmlDefinitionNode + { + Name = "Process", + QualifiedName = "P::Process", + DefinitionKeyword = "action def", + Children = + [ + new SysmlFeatureNode { Name = "a", QualifiedName = "P::Process::a", FeatureKeyword = "action" }, + new SysmlFeatureNode { Name = "$fork0", QualifiedName = null, FeatureKeyword = "fork" }, + new SysmlFeatureNode { Name = "b1", QualifiedName = "P::Process::b1", FeatureKeyword = "action" }, + new SysmlTransitionNode { Source = "a", Target = "$fork0" }, + new SysmlTransitionNode { Source = "$fork0", Target = "b1" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::Process"] = process } + }; + var context = new ViewContext("ActionFlow", workspace); + var options = new RenderOptions(Themes.Light); + + var layout = strategy.BuildLayout(context, options); + + var bar = Assert.Single(layout.Nodes.OfType(), b => b.Shape == BadgeShape.HorizontalBar); + Assert.Null(bar.Label); + } + + /// + /// The compact action a; then b; idiom (an action followed directly by an attached + /// succession, as produced by AstBuilder.VisitActionBodyItem) resolves both nodes and + /// wires the succession between them. + /// + [Fact] + public void ActionFlowView_BuildLayout_CompactActionThenIdiom_ResolvesBothNodes() + { + var strategy = new ActionFlowViewLayoutStrategy(); + var process = new SysmlDefinitionNode + { + Name = "Process", + QualifiedName = "P::Process", + DefinitionKeyword = "action def", + Children = + [ + new SysmlFeatureNode { Name = "a", QualifiedName = "P::Process::a", FeatureKeyword = "action" }, + new SysmlTransitionNode { Source = "a", Target = "b" }, + new SysmlFeatureNode { Name = "b", QualifiedName = "P::Process::b", FeatureKeyword = "action" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::Process"] = process } + }; + var context = new ViewContext("ActionFlow", workspace); + var options = new RenderOptions(Themes.Light); + + var layout = strategy.BuildLayout(context, options); + + var boxes = layout.Nodes.OfType().Where(b => b.Keyword == "action").ToList(); + Assert.Equal(2, boxes.Count); + Assert.Contains(boxes, b => b.Label == "a"); + Assert.Contains(boxes, b => b.Label == "b"); + Assert.Contains(layout.Nodes.OfType(), l => l.LineStyle == LineStyle.Dashed); + } } From 4770dd8a15e3826dd438d1adee5f8db4db6912c4 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 15:55:37 -0400 Subject: [PATCH 4/7] Document attached-succession action-body and control-node support - Add SysML2Tools-Language-Semantic-Model-AstBuilder-ActionBodyAttachedSuccession and -ActionControlNodes requirements with test traces. - Add SysML2Tools-Core-Layout-Internal-ActionFlowViewLayoutStrategy- ControlNodeShapes requirement with test traces. - Update AstBuilder and ActionFlowViewLayoutStrategy design docs' Key Methods/Data Model sections to describe the new visitors, synthetic-naming convention, and kind-routed badge/box construction. - Add Test Scenarios entries to the corresponding verification docs. --- .../action-flow-view-layout-strategy.md | 31 ++++++++-- .../semantic/model/ast-builder.md | 60 +++++++++++++++++++ .../action-flow-view-layout-strategy.yaml | 27 +++++++++ .../semantic/model/ast-builder.yaml | 50 ++++++++++++++++ .../action-flow-view-layout-strategy.md | 5 ++ .../semantic/model/ast-builder.md | 8 +++ 6 files changed, 177 insertions(+), 4 deletions(-) diff --git a/docs/design/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md b/docs/design/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md index 66d9c66..9cd71d3 100644 --- a/docs/design/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md +++ b/docs/design/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md @@ -10,9 +10,11 @@ that have no successor, and successions drawn as dashed downward flow arrows. ##### Data Model `ActionFlowViewLayoutStrategy` has no instance state; all input arrives through the `BuildLayout` -parameters. Layout constants (`MinActionWidth`, `CharWidthFactor`, `MarkerSize`, `MarkerBand`) are -declared as `private const double` fields. A private `ActionItem` record carries each action with -its computed box size; successions are carried as `(int From, int To)` index pairs. +parameters. Layout constants (`MinActionWidth`, `CharWidthFactor`, `MarkerSize`, `MarkerBand`, +`ControlNodeBadgeSize`) are declared as `private const double` fields. A private `ActionItem` +record carries each action with its computed box size and a `Kind` field (`"action"` by default, +or `"merge"`/`"decide"`/`"join"`/`"fork"`/`"accept"`/`"send"` for control nodes); successions are +carried as `(int From, int To)` index pairs. ##### Key Methods @@ -39,7 +41,28 @@ by this change — it is applied regardless of specificity. `CollectActions` gat qualified name fails `ExposeScopeResolver.IsInSubjectScope`; it then adds any additional action named only by a succession endpoint **unconditionally** (this second pass has no independent qualified name of its own to scope against, since it exists solely because a succession names it), -building a name → index lookup. +building a name → index lookup. Both the primary loop and `FindRoot`'s succession/action scoring +recognize a 7-way `IsActionFlowKeyword` allow-list (`"action"`, `"merge"`, `"decide"`, `"join"`, +`"fork"`, `"accept"`, `"send"`) rather than only `"action"`, so control-node-heavy action defs +(e.g. one dominated by fork/join/decide/merge nodes) are still favored as root candidates; each +collected item records its real `Kind` alongside its name. + +###### `ComputeActionSize(name, kind, theme)` and `MakeActionNode(item, rect)` + +`ComputeActionSize` sizes badge-kind items (`IsBadgeKind`: `"merge"`, `"decide"`, `"join"`, +`"fork"`) to a fixed `ControlNodeBadgeSize` square, independent of the name's length; box-kind +items (`"action"`, `"accept"`, `"send"`) keep the existing name-length-driven +`MinActionWidth`/`CharWidthFactor` sizing. `MakeActionNode` is a router that replaces the former +`MakeActionBox`: `"merge"`/`"decide"` map their placed rectangle's centre to a +`LayoutBadge(CentreX, CentreY, Size, BadgeShape.Diamond, Label)`; `"fork"`/`"join"` map to a +`LayoutBadge(..., BadgeShape.HorizontalBar, Label)`; `"accept"`/`"send"` and ordinary `"action"` +items map to an unchanged `LayoutBox(BoxShape.RoundedRectangle, ...)`, except that `"accept"`/ +`"send"` set `Keyword` to their own kind instead of the hard-coded `"action"` text so the +distinct node kind remains visible. For every kind, a badge or box `Label` derived from an +AstBuilder-synthesized `$`-prefixed internal name (anonymous control nodes) is blanked to +`string.Empty`, since synthetic internal identifiers are never meant to be user-visible. The +ordinary-`"action"` rendering path is unchanged byte-for-byte from before this routing was +introduced, since many existing gallery/test fixtures depend on today's plain-box behavior. ###### `ResolveSuccessions(root, index)` diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index 64e6fa4..f25672b 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -33,6 +33,13 @@ stack with `::` to form the fully-qualified name. | `VisitEntryActionMember` | `EntryActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "entry"`) | | `VisitDoActionMember` | `DoActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "do"`) | | `VisitExitActionMember` | `ExitActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "exit"`) | +| `VisitActionBodyItem` | `ActionBodyItemContext` | `SysmlNode` or `MultiNodeCapture` (node + successions) | +| `VisitMergeNode` | `MergeNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "merge"`) | +| `VisitDecisionNode` | `DecisionNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "decide"`) | +| `VisitJoinNode` | `JoinNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "join"`) | +| `VisitForkNode` | `ForkNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "fork"`) | +| `VisitAcceptNode` | `AcceptNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "accept"`) | +| `VisitSendNode` | `SendNodeContext` | `SysmlFeatureNode` (`FeatureKeyword = "send"`) | `GetDeclaredName(IdentificationContext)` handles the three grammar alternatives: @@ -304,6 +311,59 @@ the resulting feature node still registers as an (unnamed, unregistered) AST chi information is lost, but it is not itself a resolvable symbol. This scope boundary is intentional and matches the ROADMAP's framing of entry/do/exit action support as "minimal, non-behavioral." +**Attached-succession action bodies and control-node features.** The `actionBodyItem` grammar +rule has an analogous combined-shape problem to `stateBodyItem`: two of its alternatives attach +a succession directly onto the immediately preceding node within the same alternative rather than +exposing it as a separate `successionAsUsage` — +`initialNodeMember (actionTargetSuccessionMember)*` (e.g. `first start; then off;`) and +`(sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)*` (e.g. the compact +`action a1; then a2;` idiom). Before `VisitActionBodyItem` existed, `AstBuilder` only ever +visited the leading node of each alternative and silently dropped every attached succession, so +the compact idiom resolved both action nodes but produced no succession edge linking them. + +`VisitActionBodyItem` dispatches all four alternatives explicitly: + +- `nonBehaviorBodyItem`, `guardedSuccessionMember` — passed straight through to `Visit(...)` (no + attached-succession shape applies). +- `initialNodeMember (actionTargetSuccessionMember)*` — when one or more + `actionTargetSuccessionMember`s are attached, synthesizes a `SysmlTransitionNode` per entry + (via `BuildActionTargetSuccession`) sourced from the `qualifiedName` referenced by the + `initialNodeMember`; the bare `first start;` form (no attached succession) remains a no-op, + unchanged from today, since `ActionFlowViewLayoutStrategy` infers start/done markers from + succession topology rather than a declarative initial-marker concept. +- `(sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)*` — visits + `actionBehaviorMember` (which delegates to the existing `actionNodeMember`/`behaviorUsageMember` + handling) to obtain the main node, then calls `BuildActionTargetSuccession` once per + `actionTargetSuccessionMember` entry, each producing a `SysmlTransitionNode` whose `Source` is + the main node's `Name`. + +`BuildActionTargetSuccession` handles all three `actionTargetSuccession` grammar forms: +unguarded `targetSuccession` (`sourceEndMember THEN connectorEndMember`), guarded +`guardedTargetSuccession` (`if guardExpressionMember then connectorEndMember`, capturing the +guard's expression text), and `defaultTargetSuccession` (`else then connectorEndMember`, which the +grammar provides no guard expression for). + +When one or more attached successions are produced, `VisitActionBodyItem` returns the same +`MultiNodeCapture` sentinel used by `VisitStateBodyItem`, wrapping the node plus its +succession(s); when none are produced, it returns the visited node/pass-through result directly. + +`VisitMergeNode`, `VisitDecisionNode`, `VisitJoinNode`, `VisitForkNode`, `VisitAcceptNode`, and +`VisitSendNode` each delegate to a shared `BuildActionNodeFeature(usage, keyword)` helper that +builds a **minimal** `SysmlFeatureNode` — `FeatureKeyword` set to +`"merge"`/`"decide"`/`"join"`/`"fork"`/`"accept"`/`"send"` respectively, `Children` always empty. +Unlike ordinary anonymous actions (which are left nameless), an anonymous control node is given a +synthesized internal name of the form `$` (via a monotonically increasing +`_anonymousNodeCounter` field) rather than `null`. This is a deliberate deviation from the +State Transition View precedent: anonymous fork/decide/send is the *dominant* real-world idiom in +the OMG training corpus (e.g. `then fork;` immediately followed by several `then` successions), +so leaving these nodes nameless would make it impossible to wire their successions or render a +distinct badge for them. The synthetic name is never registered in the symbol table +(`QualifiedName` stays `null`, so `$`-prefixed names never resolve and surface only as cosmetic +"unresolved reference" warnings) and is blanked from rendered labels by +`ActionFlowViewLayoutStrategy`; it exists purely as an internal succession-wiring mechanism. +`assignmentNode`, `terminateNode`, `ifNode`, `whileLoopNode`, and `forLoopNode` remain +intentionally unhandled — a pre-existing gap, not introduced by this change. + ##### Error Handling Anonymous elements (null declared names) are silently skipped — visitor methods return `null` diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.yaml index e7ef696..ffc9062 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.yaml @@ -148,3 +148,30 @@ sections: defeating the intended scoping. tests: - ActionFlowView_BuildLayout_ExposeInnerActionOfNestedDefinition_SelectsNestedDefinitionNotAncestor + + - id: SysML2Tools-Core-Layout-Internal-ActionFlowViewLayoutStrategy-ControlNodeShapes + title: >- + ActionFlowViewLayoutStrategy shall render "fork"/"join" action nodes as a + LayoutBadge with BadgeShape.HorizontalBar, "decide"/"merge" action nodes as a + LayoutBadge with BadgeShape.Diamond, and "accept"/"send" action nodes as a + LayoutBox with BoxShape.RoundedRectangle whose Keyword reflects the distinct + "accept"/"send" node kind, while leaving ordinary "action" node rendering + unchanged. + justification: | + Control nodes are semantically distinct from ordinary actions and the existing + State Transition View precedent already separates pseudostate markers from real + state boxes; Action Flow View needs the analogous visual separation so fork/join + branch points and decision/merge points are recognizable at a glance. No pentagon + primitive exists in the referenced rendering package, so accept/send nodes keep the + rounded-rectangle box shape but gain a distinguishing Keyword label instead of the + hard-coded "action" text. Badge-kind nodes use a fixed intrinsic size + (ControlNodeBadgeSize) rather than name-based sizing, and their rendered Label is + blanked when the underlying name is an AstBuilder-synthesized internal identifier. + Ordinary-action rendering is required to stay byte-for-byte unchanged, since many + existing gallery/test fixtures depend on today's plain-box behavior. + tests: + - ActionFlowView_BuildLayout_ForkAndJoin_RenderHorizontalBarBadges + - ActionFlowView_BuildLayout_DecisionAndMerge_RenderDiamondBadges + - ActionFlowView_BuildLayout_AcceptAndSend_RenderBoxesWithDistinctKeyword + - ActionFlowView_BuildLayout_SyntheticControlNodeName_RendersBlankLabel + - ActionFlowView_BuildLayout_CompactActionThenIdiom_ResolvesBothNodes diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml index 3b8865f..baf1183 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -272,3 +272,53 @@ sections: - WorkspaceLoader_LoadAsync_UnnamedEntryActionReferenceForm_NoNameNoCrash - WorkspaceLoader_LoadAsync_StateUsageWithExplicitTyping_RecordsTypingEdge - Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ActionBodyAttachedSuccession + title: >- + AstBuilder shall synthesize a SysmlTransitionNode for each attached-succession + `actionBodyItem` shape — an `initialNodeMember` followed by zero or more + `actionTargetSuccessionMember`s, and an `actionBehaviorMember` followed by zero or + more `actionTargetSuccessionMember`s — sourced from the preceding node's name, in + addition to building the preceding node itself. + justification: | + SysML v2's grammar attaches a `then`/guarded/default succession directly onto the + immediately preceding action node or `first` marker within the same + `actionBodyItem` alternative, rather than as a standalone + `successionAsUsage`/`transitionUsageMember`. Previously, AstBuilder only visited the + leading node and silently dropped every attached succession, so the compact idiom + `action a1; then a2;` produced both nodes but no succession edge between them. The + existing `MultiNodeCapture` sentinel (introduced for the analogous State Transition + View fix) is reused so a single `actionBodyItem` visit can yield both the node and + one or more synthesized transition nodes. A bare `initialNodeMember` with no + attached succession remains a no-op, unchanged from today, since + `ActionFlowViewLayoutStrategy` infers start/done markers from succession topology + rather than a declarative initial-marker concept. + tests: + - WorkspaceLoader_LoadAsync_CompactActionThenIdiom_ResolvesBothNodes + - WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll + - WorkspaceLoader_LoadAsync_BareInitialNodeMember_ProducesNoSuccession + - WorkspaceLoader_LoadAsync_InitialNodeMemberWithAttachedSuccession_SynthesizesTransition + - WorkspaceLoader_LoadAsync_GuardedAndDefaultActionTargetSuccession_ExtractTargets + - ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ActionControlNodes + title: >- + AstBuilder shall build a minimal SysmlFeatureNode (with Children always empty and + FeatureKeyword "merge", "decide", "join", "fork", "accept", or "send") for each + mergeNode, decisionNode, joinNode, forkNode, acceptNode, and sendNode, synthesizing + an internal `$` name when the node is anonymous (no declared name), so + it can still act as the implicit source of its attached successions. + justification: | + Anonymous control nodes (e.g. a bare `then fork;` immediately followed by several + `then` successions) are the dominant idiom for fork/decide/send in the real OMG + training corpus, not a rare edge case; leaving them nameless (as ordinary anonymous + actions are) would make it impossible to wire their successions or render a distinct + badge for them. The synthetic name is never added to the symbol table (QualifiedName + stays null) and is blanked from rendered labels, so it is purely an internal wiring + mechanism. `assignmentNode`, `terminateNode`, `ifNode`, `whileLoopNode`, and + `forLoopNode` remain intentionally unhandled — a pre-existing gap, not introduced by + this change. + tests: + - WorkspaceLoader_LoadAsync_AnonymousControlNodes_SynthesizeNames + - WorkspaceLoader_LoadAsync_NamedControlNodes_KeepDeclaredName + - ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge diff --git a/docs/verification/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md b/docs/verification/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md index f9913ae..5336865 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md +++ b/docs/verification/sysml2-tools-core/layout/internal/action-flow-view-layout-strategy.md @@ -65,3 +65,8 @@ configuration are required beyond a standard .NET SDK installation. | `ActionFlowView_BuildLayout_ExposeSingleAction_DropsOutOfScopeAction` | Isolated action dropped; referenced kept | | `ActionFlowView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot` | Usage resolves via `Typing` to root | | `ActionFlowView_BuildLayout_ExposeInnerActionOfNestedDefinition_SelectsNestedDefinitionNotAncestor` | Nested wins | +| `ActionFlowView_BuildLayout_ForkAndJoin_RenderHorizontalBarBadges` | Fork/join render horizontal-bar badges | +| `ActionFlowView_BuildLayout_DecisionAndMerge_RenderDiamondBadges` | Decision/merge render diamond badges | +| `ActionFlowView_BuildLayout_AcceptAndSend_RenderBoxesWithDistinctKeyword` | Accept/send keep own `Keyword` | +| `ActionFlowView_BuildLayout_SyntheticControlNodeName_RendersBlankLabel` | `$`-prefixed synthetic names render blank | +| `ActionFlowView_BuildLayout_CompactActionThenIdiom_ResolvesBothNodes` | Compact `action a; then b;` idiom resolves | diff --git a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md index cd92c63..1ea2f39 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -112,3 +112,11 @@ external services or additional configuration are required beyond a standard .NE | State usage explicit typing | `WorkspaceLoader_LoadAsync_StateUsageWithExplicitTyping_RecordsTypingEdge` | | Transition source resolves | `WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember` | | OMG corpus fixture | `Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions` | +| Compact `action a; then b;` idiom | `WorkspaceLoader_LoadAsync_CompactActionThenIdiom_ResolvesBothNodes` | +| Multiple attached target successions | `WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll` | +| Bare `first start;`, no succession | `WorkspaceLoader_LoadAsync_BareInitialNodeMember_ProducesNoSuccession` | +| Attached first-then edge | `WorkspaceLoader_LoadAsync_InitialNodeMemberWithAttachedSuccession_SynthesizesTransition` | +| Anonymous control nodes synthesize names | `WorkspaceLoader_LoadAsync_AnonymousControlNodes_SynthesizeNames` | +| Named control nodes keep declared name | `WorkspaceLoader_LoadAsync_NamedControlNodes_KeepDeclaredName` | +| Guarded/default succession | `WorkspaceLoader_LoadAsync_GuardedAndDefaultActionTargetSuccession_ExtractTargets` | +| Fork/join/decision/merge OMG corpus fixtures | `ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge` | From c827f0cb383fd5bdb2473ee167c1c24a8c0ab57e Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 15:55:48 -0400 Subject: [PATCH 5/7] Add fork/join branch and compact idiom to pipeline gallery model - 04-pipeline-action-flow.sysml: convert checkout/restoreDependencies to the compact 'action a; then b;' idiom, and replace the implicit dual-successor build branch with an explicit named fork/join pair (buildFork/testJoin) feeding parallel unitTest/securityScan branches that rejoin before qualityGate. - Regenerate PipelineActionFlowView.svg/.png; visually verified per diagram-quality.md's Agent Checklist (horizontal-bar badges at the fork/join positions, no overlaps). - Update the gallery README's caption for this example. - Mark the Action Flow View control-node/successor AST correctness item in ROADMAP.md as delivered, listing the pentagon accept/send shape, swim-lanes, item-flow annotations, assignment/terminate/if/while/for-loop node AST support, and actionUsage-level nested action bodies as explicitly deferred future work. --- ROADMAP.md | 64 ++++----- docs/gallery/README.md | 8 +- .../models/04-pipeline-action-flow.sysml | 25 +++- docs/gallery/png/PipelineActionFlowView.png | Bin 41441 -> 46862 bytes docs/gallery/svg/PipelineActionFlowView.svg | 124 +++++++++--------- 5 files changed, 115 insertions(+), 106 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 638f6c5..dd39f2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,45 +80,33 @@ implemented first). and renderers; possibly `LayoutLabel`/compartment tweaks. **Visual gate:** a documented requirement/part renders its note and full compartments. -### Action Flow View: control-node/successor AST correctness + fork/join/decision/merge shapes - -Investigation (mirroring the pattern found in the State Transition View item above — same grammar -family, same class of bug) found `actionBodyItem`'s combined shape has the identical AST gap, plus -control nodes are entirely uncaptured today: - -1. **Attached/implicit-source successions are silently dropped (both the action AND its successor).** - `actionBodyItem: (sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)*` - has no `AstBuilder` visitor, so the compact, common idiom `action a1; then a2;` (successor - implied by adjacency, no explicit `succession`/`first` statement) falls through to ANTLR's - default aggregation and loses both nodes — the same failure mode fixed for `stateBodyItem` in - the State Transition View branch. -2. **Fork/join/decide/merge/accept/send control nodes are entirely unmodeled.** The grammar's - `actionNode: controlNode | sendNode | acceptNode | assignmentNode | terminateNode | ifNode | - whileLoopNode | forLoopNode` (`controlNode: mergeNode | decisionNode | joinNode | forkNode`) has - zero `AstBuilder` visitors — not even a bare `fork f1;` registers today, so every Action Flow - View renders every node as an identical rounded-rectangle "action" box regardless of its real - control-node kind, and successions through them are lost too (compounding problem 1). -3. **Renderer shape primitives already exist for the two structural kinds, just unused.** The - `DemaConsulting.Rendering` package already defines `BadgeShape.HorizontalBar`/`VerticalBar` - (usable for fork/join thick bars) and `BadgeShape.Diamond` (usable for decision/merge) — - `ActionFlowViewLayoutStrategy` just never produces a `LayoutBadge` for anything but the - start/done markers today. There is **no pentagon primitive** in the Rendering package's - `BoxShape`/`BadgeShape` enums for a true UML accept/send action shape — adding one requires a - change to the separate `DemaConsulting.Rendering` package, out of scope for this repo/branch. - -**Scope (this branch):** `AstBuilder` (new handling for the `actionBehaviorMember -(actionTargetSuccessionMember)*` combined shape, mirroring the state-body fix; new visitors for -`controlNode`/`sendNode`/`acceptNode` registering a distinguishable node kind); -`ActionFlowViewLayoutStrategy` (render fork/join as `LayoutBadge(BadgeShape.HorizontalBar | -VerticalBar)`, decision/merge as `LayoutBadge(BadgeShape.Diamond)`, keeping accept/send as -keyword-labelled rounded-rectangle boxes for now, pending a future pentagon primitive). -**Explicitly deferred (separate future items, not this branch):** a true pentagon accept/send +### Action Flow View: control-node/successor AST correctness + fork/join/decision/merge shapes — delivered + +**Delivered.** `AstBuilder.VisitActionBodyItem` now synthesizes a `SysmlTransitionNode` for both +combined-shape `actionBodyItem` alternatives — `initialNodeMember (actionTargetSuccessionMember)*` +and `(sourceSuccessionMember)? actionBehaviorMember (actionTargetSuccessionMember)*` — reusing the +`MultiNodeCapture` sentinel introduced for the State Transition View fix, so the compact `action +a1; then a2;` idiom and multiple attached successions now resolve correctly (previously both the +node and its successor were silently dropped). `VisitMergeNode`/`VisitDecisionNode`/ +`VisitJoinNode`/`VisitForkNode`/`VisitAcceptNode`/`VisitSendNode` build minimal `SysmlFeatureNode`s +with `FeatureKeyword` values `"merge"`/`"decide"`/`"join"`/`"fork"`/`"accept"`/`"send"`, giving +anonymous control nodes (the dominant idiom in the OMG training corpus) a synthesized `$ +` internal name so their successions still wire correctly. +`ActionFlowViewLayoutStrategy` now renders fork/join as a `LayoutBadge(BadgeShape.HorizontalBar)` +and decision/merge as a `LayoutBadge(BadgeShape.Diamond)`; accept/send keep the rounded-rectangle +box shape (no pentagon primitive exists in the referenced `DemaConsulting.Rendering` package) but +now show their own `Keyword` instead of the hard-coded `"action"` text. Ordinary-action rendering +is unchanged. The CI/CD Pipeline gallery model (`docs/gallery/models/04-pipeline-action-flow.sysml`) +was extended with a named `fork`/`join` pair and the compact succession idiom, and regenerated. + +**Explicitly deferred (separate future items, not delivered here):** a true pentagon accept/send shape (needs a `DemaConsulting.Rendering` package change); swim-lanes via `LayoutBand`; item-flow -edge annotations (no item-flow/payload capture exists in the AST yet); the Sequence View dynamics -item below (different subsystem, separate branch). -**Visual gate:** an action flow with a fork feeding two parallel actions that rejoin renders a -thick bar in each direction; a guarded decision/merge renders diamonds; the compact `action a; -then b;` idiom renders correctly with no dropped nodes. +edge annotations (no item-flow/payload capture exists in the AST yet); `assignmentNode`/ +`terminateNode`/`ifNode`/`whileLoopNode`/`forLoopNode` AST support (still entirely unmodeled); an +`actionUsage`-level (rather than `action def`-level) nested `actionBody` — `VisitActionUsage` +still doesn't collect nested action-body children, so control nodes/successions nested inside a +`action x : T { ... }` *usage* (as opposed to an `action def X { ... }` body) remain invisible; +the Sequence View dynamics item below (different subsystem, separate branch). ### Sequence View dynamics diff --git a/docs/gallery/README.md b/docs/gallery/README.md index 440d40c..6570ac1 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -113,8 +113,12 @@ SVG: [`svg/ElevatorStateTransitionView.svg`](svg/ElevatorStateTransitionView.svg ## 4. Action Flow View — CI/CD Pipeline -Shows actions arranged top-to-bottom by the layered layout pipeline, -with a start node, a done node, and a quality-gate branch and join. +Shows actions arranged top-to-bottom by the layered layout pipeline, with a start node, a done +node, and a quality-gate branch and join. The `build` step fans out through a named `fork` +(rendered as a horizontal-bar badge) into parallel `unitTest`/`securityScan` branches that rejoin +through a named `join` (also a horizontal-bar badge) before `qualityGate`. The `checkout` step +uses the compact `action checkout; then restoreDependencies;` idiom instead of a standalone +`first ... then ...;` succession statement. Model: [`models/04-pipeline-action-flow.sysml`](models/04-pipeline-action-flow.sysml) · SVG: [`svg/PipelineActionFlowView.svg`](svg/PipelineActionFlowView.svg) diff --git a/docs/gallery/models/04-pipeline-action-flow.sysml b/docs/gallery/models/04-pipeline-action-flow.sysml index 2a9f555..14b7dbf 100644 --- a/docs/gallery/models/04-pipeline-action-flow.sysml +++ b/docs/gallery/models/04-pipeline-action-flow.sysml @@ -3,11 +3,29 @@ package CiCdPipeline { // A continuous-integration / deployment action flow with a quality gate branch. action def Pipeline { action checkout; + then restoreDependencies; + action restoreDependencies; action build; + first restoreDependencies then build; + first build then buildFork; + + fork buildFork; + then unitTest; + then securityScan; + action unitTest; + then integrationTest; + action integrationTest; + then testJoin; + action securityScan; + then testJoin; + + join testJoin; + then qualityGate; + action qualityGate; action deployStaging; action smokeTest; @@ -16,13 +34,6 @@ package CiCdPipeline { action notifySuccess; action notifyFailure; - first checkout then restoreDependencies; - first restoreDependencies then build; - first build then unitTest; - first build then securityScan; - first unitTest then integrationTest; - first integrationTest then qualityGate; - first securityScan then qualityGate; first qualityGate then deployStaging; first deployStaging then smokeTest; first smokeTest then deployProduction; diff --git a/docs/gallery/png/PipelineActionFlowView.png b/docs/gallery/png/PipelineActionFlowView.png index ebe7b33838e9b500920327fc9f2a74f149a5db88..ee1fa123202db36cfa5e0dce8b9e70bf6ba0e604 100644 GIT binary patch literal 46862 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 i|DVSj{hyBrbT)n1mra@g literal 41441 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 diff --git a/docs/gallery/svg/PipelineActionFlowView.svg b/docs/gallery/svg/PipelineActionFlowView.svg index 51d910f..432ad46 100644 --- a/docs/gallery/svg/PipelineActionFlowView.svg +++ b/docs/gallery/svg/PipelineActionFlowView.svg @@ -1,4 +1,4 @@ - + @@ -30,62 +30,68 @@ - - «action» - checkout - - «action» - restoreDependencies - - «action» - build - - «action» - unitTest - - «action» - integrationTest - - «action» - securityScan - - «action» - qualityGate - - «action» - deployStaging - - «action» - smokeTest - - «action» - deployProduction - - «action» - rollback - - «action» - notifySuccess - - «action» - notifyFailure - - - - - - - - - - - - - - - - - - - + + «action» + checkout + + «action» + restoreDependencies + + «action» + build + + buildFork + + «action» + unitTest + + «action» + integrationTest + + «action» + securityScan + + testJoin + + «action» + qualityGate + + «action» + deployStaging + + «action» + smokeTest + + «action» + deployProduction + + «action» + rollback + + «action» + notifySuccess + + «action» + notifyFailure + + + + + + + + + + + + + + + + + + + + + From 22497e03fbc47511567ef73a92363cea396194be Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 16:56:36 -0400 Subject: [PATCH 6/7] Fix missing incoming edge for leading sourceSuccessionMember in action bodies - AstBuilder.VisitActionBodyItem now reads context.sourceSuccessionMember() in the actionBehaviorMember branch and synthesizes the implicit incoming SysmlTransitionNode when a preceding flow position is known. - Added CollectActionBodyChildren (parallel to CollectChildren, which stays untouched for state bodies) plus DetermineFlowPositionName and the _actionBodyPreviousNodeName field to track the current flow position while iterating an action body's actionBodyItems in source order. - VisitActionDefinition now calls CollectActionBodyChildren for action bodies. - Extended WorkspaceLoaderTests and OmgModelsTests to assert the previously missing incoming edges (a->f, TurnOn->\, J->F, start->continueCharging, continueCharging->monitor, monitor->\). - Updated ast-builder reqstream requirement, design doc, and verification doc to describe the new incoming-edge mechanism and note the two discovered but out-of-scope analogous gaps (nonBehaviorBodyItem structureUsageMember and VisitStateBodyItem) as known follow-ups. --- .../semantic/model/ast-builder.md | 44 +++++- .../semantic/model/ast-builder.yaml | 23 ++- .../semantic/model/ast-builder.md | 4 +- .../Semantic/Model/AstBuilder.cs | 139 +++++++++++++++++- .../Parser/OmgModelsTests.cs | 24 ++- .../Semantic/WorkspaceLoaderTests.cs | 10 +- 6 files changed, 226 insertions(+), 18 deletions(-) diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index f25672b..b0d5ea3 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -335,7 +335,25 @@ the compact idiom resolved both action nodes but produced no succession edge lin `actionBehaviorMember` (which delegates to the existing `actionNodeMember`/`behaviorUsageMember` handling) to obtain the main node, then calls `BuildActionTargetSuccession` once per `actionTargetSuccessionMember` entry, each producing a `SysmlTransitionNode` whose `Source` is - the main node's `Name`. + the main node's `Name`. When the optional leading `sourceSuccessionMember` is present (a bare + `then` immediately before the node, e.g. `action a; then fork f; ...` — the dominant real-world + idiom for wiring a control node into a flow) an additional *incoming* `SysmlTransitionNode` is + prepended, whose `Source` is `_actionBodyPreviousNodeName` and whose `Target` is the main node's + `Name`. The grammar's leading marker (`sourceSuccessionMember: THEN sourceSuccession`, where + `sourceSuccession`/`sourceEndMember`/`sourceEnd` carry no name token at all) is a pure marker — + its meaning is "this node's incoming edge comes from whatever immediately preceded it in the + same enclosing action body" — so its `Source` identity cannot be read off the grammar node + itself. It is instead resolved from `_actionBodyPreviousNodeName`, an `AstBuilder` instance + field maintained by `CollectActionBodyChildren` (a body-specific counterpart to the generic + `CollectChildren`, used only for action bodies) as it iterates an action body's + `actionBodyItem`s in source order, updating the tracked position after each item via + `DetermineFlowPositionName` (which resolves to the last synthesized transition's `Target` when + the item produced trailing successions, or the visited node's own `Name` otherwise). When no + previous position is known (e.g. the item is the first thing in the body), no incoming edge is + synthesized rather than fabricating a `Source` from nothing — this matches the safe, + no-op-by-default behavior applied elsewhere in this visitor. `CollectChildren` itself remains + untouched and continues to serve the state-body call sites, since only action bodies need this + order-sensitive bookkeeping. `BuildActionTargetSuccession` handles all three `actionTargetSuccession` grammar forms: unguarded `targetSuccession` (`sourceEndMember THEN connectorEndMember`), guarded @@ -343,9 +361,27 @@ unguarded `targetSuccession` (`sourceEndMember THEN connectorEndMember`), guarde guard's expression text), and `defaultTargetSuccession` (`else then connectorEndMember`, which the grammar provides no guard expression for). -When one or more attached successions are produced, `VisitActionBodyItem` returns the same -`MultiNodeCapture` sentinel used by `VisitStateBodyItem`, wrapping the node plus its -succession(s); when none are produced, it returns the visited node/pass-through result directly. +When one or more attached successions (incoming, trailing, or both) are produced, +`VisitActionBodyItem` returns the same `MultiNodeCapture` sentinel used by +`VisitStateBodyItem`, wrapping the incoming transition (if any), the node, and its trailing +succession(s) in that order; when none are produced, it returns the visited node/pass-through +result directly. + +**Known follow-up gaps (out of scope for this fix).** Two structurally analogous +leading-marker cases were discovered while fixing the above but deliberately left unfixed to keep +this change surgical: + +- `nonBehaviorBodyItem`'s `(sourceSuccessionMember)? structureUsageMember` shape has the same + "leading `then` implies an implicit incoming edge" grammar structure, but there is no + `VisitNonBehaviorBodyItem`/`VisitStructureUsageMember` override at all today — it relies + entirely on ANTLR's default `VisitChildren` aggregation, which drops every child but the last. + This is a broader, pre-existing gap (no `MultiNodeCapture` handling exists there yet at all), + not merely a missing `sourceSuccessionMember` read. +- `VisitStateBodyItem`'s `(sourceSuccessionMember)? behaviorUsageMember + (targetTransitionUsageMember)*` shape (State Transition View) has the identical unread-marker + problem this fix addresses for action bodies, and was not established as a working precedent to + copy — it has the same gap, latent and undetected because no current test exercises a leading + `then` before a `behaviorUsageMember` inside a state body. `VisitMergeNode`, `VisitDecisionNode`, `VisitJoinNode`, `VisitForkNode`, `VisitAcceptNode`, and `VisitSendNode` each delegate to a shared `BuildActionNodeFeature(usage, keyword)` helper that diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml index baf1183..64a01e9 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -277,9 +277,15 @@ sections: title: >- AstBuilder shall synthesize a SysmlTransitionNode for each attached-succession `actionBodyItem` shape — an `initialNodeMember` followed by zero or more - `actionTargetSuccessionMember`s, and an `actionBehaviorMember` followed by zero or - more `actionTargetSuccessionMember`s — sourced from the preceding node's name, in - addition to building the preceding node itself. + `actionTargetSuccessionMember`s, and an optional leading `sourceSuccessionMember` + plus an `actionBehaviorMember` followed by zero or more + `actionTargetSuccessionMember`s — in addition to building the preceding/following + node itself. Trailing successions are sourced from the node's own name; the + optional leading `sourceSuccessionMember` (a bare `then` immediately before an + `actionBehaviorMember`, e.g. `action a; then fork f;`) synthesizes an implicit + *incoming* succession whose `Source` is the name of the immediately preceding + sibling `actionBodyItem` in the same enclosing action body, since the grammar's + leading marker itself carries no name. justification: | SysML v2's grammar attaches a `then`/guarded/default succession directly onto the immediately preceding action node or `first` marker within the same @@ -292,7 +298,16 @@ sections: one or more synthesized transition nodes. A bare `initialNodeMember` with no attached succession remains a no-op, unchanged from today, since `ActionFlowViewLayoutStrategy` infers start/done markers from succession topology - rather than a declarative initial-marker concept. + rather than a declarative initial-marker concept. Additionally, the grammar's + `(sourceSuccessionMember)? actionBehaviorMember` shape's optional leading marker — + the dominant real-world idiom for wiring a control node into a flow, e.g. + `action TurnOn; then fork; then monitorBrakePedal; ...` — previously went entirely + unread, so the fork/decide/merge/join/accept/send node's *incoming* edge from the + preceding sibling was silently dropped even though its outgoing successions worked. + `CollectActionBodyChildren` now tracks the current flow position (the preceding + sibling's name) in source order as it iterates an action body's items, so + `VisitActionBodyItem` can resolve that implicit incoming `Source` when a leading + marker is present. tests: - WorkspaceLoader_LoadAsync_CompactActionThenIdiom_ResolvesBothNodes - WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll diff --git a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md index 1ea2f39..bb3531a 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -113,10 +113,10 @@ external services or additional configuration are required beyond a standard .NE | Transition source resolves | `WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember` | | OMG corpus fixture | `Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions` | | Compact `action a; then b;` idiom | `WorkspaceLoader_LoadAsync_CompactActionThenIdiom_ResolvesBothNodes` | -| Multiple attached target successions | `WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll` | +| Target successions + incoming edge | `WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll` | | Bare `first start;`, no succession | `WorkspaceLoader_LoadAsync_BareInitialNodeMember_ProducesNoSuccession` | | Attached first-then edge | `WorkspaceLoader_LoadAsync_InitialNodeMemberWithAttachedSuccession_SynthesizesTransition` | | Anonymous control nodes synthesize names | `WorkspaceLoader_LoadAsync_AnonymousControlNodes_SynthesizeNames` | | Named control nodes keep declared name | `WorkspaceLoader_LoadAsync_NamedControlNodes_KeepDeclaredName` | | Guarded/default succession | `WorkspaceLoader_LoadAsync_GuardedAndDefaultActionTargetSuccession_ExtractTargets` | -| Fork/join/decision/merge OMG corpus fixtures | `ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge` | +| Fork/join/decision/merge OMG fixtures + incoming | `ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge` | diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index 5eaf98d..5ec4868 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -275,6 +275,23 @@ private sealed class MultiNodeCapture : SysmlNode /// private int _anonymousNodeCounter; + /// + /// Tracks the name of the most recently established "flow position" while + /// iterates an action body's actionBodyItems + /// in source order, so can resolve the implicit + /// Source of a leading bare then (sourceSuccessionMember) on a + /// subsequent sibling. The grammar's sourceSuccessionMember: THEN sourceSuccession + /// carries no name at all (it is just the THEN token plus an always-empty + /// sourceEnd) — its meaning is "this node's incoming edge comes from whatever + /// action/node immediately precedes it in the same enclosing action body." That identity + /// can only come from order-sensitive traversal state maintained by the caller that + /// iterates the sibling actionBodyItems, not from anything inside the grammar node + /// itself. Null outside an active call, or when the + /// preceding sibling did not establish a nameable flow position (e.g. the very first item + /// in the body). + /// + private string? _actionBodyPreviousNodeName; + /// public override SysmlNode? VisitPartDefinition(SysMLv2Parser.PartDefinitionContext context) { @@ -366,9 +383,12 @@ private sealed class MultiNodeCapture : SysmlNode var qualifiedName = QualifyName(name); var supertypeNames = GetSubclassificationSupertypes(decl?.subclassificationPart()); - // Collect the action body (action usages and successions) as children. + // Collect the action body (action usages and successions) as children. Action bodies use + // CollectActionBodyChildren (not the generic CollectChildren) because resolving an + // implicit leading `then` requires order-sensitive tracking of the preceding sibling's + // flow position — see VisitActionBodyItem and _actionBodyPreviousNodeName. _namespaceStack.Add(name); - var (children, annotations) = CollectChildren(context.actionBody()?.actionBodyItem() ?? []); + var (children, annotations) = CollectActionBodyChildren(context.actionBody()?.actionBodyItem() ?? []); _namespaceStack.RemoveAt(_namespaceStack.Count - 1); return new SysmlDefinitionNode @@ -437,7 +457,13 @@ private sealed class MultiNodeCapture : SysmlNode /// one per attached target succession whose /// Source is implicit); without this override, ANTLR's default VisitChildren /// aggregation silently discards every child result but the last, dropping the earlier - /// nodes and losing successions entirely. + /// nodes and losing successions entirely. The actionBehaviorMember alternative's + /// optional leading sourceSuccessionMember (a bare then immediately before + /// the node, e.g. action a; then fork f; ...) also synthesizes an additional + /// incoming whose Source is + /// — the name of the immediately preceding + /// sibling in the enclosing action body, tracked by + /// since the grammar's leading marker itself carries no identity. /// public override SysmlNode? VisitActionBodyItem(SysMLv2Parser.ActionBodyItemContext context) { @@ -474,20 +500,43 @@ private sealed class MultiNodeCapture : SysmlNode if (context.actionBehaviorMember() is { } actionBehaviorMember) { var behaviorNode = Visit(actionBehaviorMember); + if (behaviorNode is null) + { + return null; + } + var targets = context.actionTargetSuccessionMember(); - if (behaviorNode is null || targets.Length == 0) + var hasImplicitSource = context.sourceSuccessionMember() is not null; + if (!hasImplicitSource && targets.Length == 0) { return behaviorNode; } + var nodes = new List(); + if (hasImplicitSource && _actionBodyPreviousNodeName is { } previousName) + { + // A bare leading `then` (sourceSuccessionMember) means this node's incoming edge + // comes from whatever immediately preceded it in the same enclosing action body. + // The grammar gives that marker no name, so the source is resolved from the + // order-sensitive _actionBodyPreviousNodeName tracked by CollectActionBodyChildren. + // When no previous position is known (e.g. this is the first item in the body), no + // incoming edge is synthesized rather than fabricating a Source from nothing. + nodes.Add(new SysmlTransitionNode + { + Source = previousName, + Target = behaviorNode.Name, + }); + } + + nodes.Add(behaviorNode); + var sourceName = behaviorNode.Name; - var nodes = new List { behaviorNode }; foreach (var target in targets) { nodes.Add(BuildActionTargetSuccession(sourceName, target.actionTargetSuccession())); } - return new MultiNodeCapture { Nodes = nodes }; + return nodes.Count == 1 ? nodes[0] : new MultiNodeCapture { Nodes = nodes }; } return null; @@ -2053,6 +2102,84 @@ private static IReadOnlyList GetSubclassificationSupertypes( return (children, annotations); } + /// + /// Collects an action body's child nodes by visiting its actionBodyItems in source + /// order, tracking as it goes so + /// can resolve the implicit incoming Source of a + /// leading bare then (sourceSuccessionMember) on any sibling. This is + /// deliberately kept separate from the generic (which + /// continues to serve state bodies and other item kinds unmodified) because only action + /// bodies currently need this order-sensitive "current flow position" bookkeeping — the + /// grammar's leading marker carries no name of its own, so its identity must come from + /// traversal state maintained here rather than from anything inside the visited node. + /// + private (IReadOnlyList Children, IReadOnlyList Annotations) + CollectActionBodyChildren(IEnumerable items) + { + var children = new List(); + var annotations = new List(); + var savedPreviousName = _actionBodyPreviousNodeName; + _actionBodyPreviousNodeName = null; + try + { + foreach (var item in items) + { + var node = Visit(item); + if (node is AnnotationCapture capture) + { + // A comment/doc annotation does not change the current flow position. + annotations.Add(capture.Annotation); + continue; + } + + if (node is MultiNodeCapture multi) + { + children.AddRange(multi.Nodes); + } + else if (node is not null) + { + children.Add(node); + } + + _actionBodyPreviousNodeName = DetermineFlowPositionName(item, node); + } + } + finally + { + _actionBodyPreviousNodeName = savedPreviousName; + } + + return (children, annotations); + } + + /// + /// Determines the name that represents the "current flow position" after visiting one + /// actionBodyItem, for use as the implicit Source of a subsequent sibling's + /// leading bare then. When the item produced a ending + /// in a synthesized (one or more trailing + /// actionTargetSuccessionMembers), the position moves to that succession's + /// Target (e.g. after first start then off;, the position is "off", + /// not "start"). Otherwise it is the visited node's own Name. A bare + /// first start; (no attached succession) still establishes "start" as the + /// position even though it synthesizes no AST node of its own. + /// + private static string? DetermineFlowPositionName(SysMLv2Parser.ActionBodyItemContext item, SysmlNode? node) + { + if (node is MultiNodeCapture { Nodes.Count: > 0 } multi) + { + return multi.Nodes[^1] is SysmlTransitionNode lastTransition + ? lastTransition.Target + : multi.Nodes[^1].Name; + } + + if (node is not null) + { + return node.Name; + } + + return item.initialNodeMember()?.qualifiedName()?.GetText(); + } + /// /// Collects child nodes from an array of . /// Comment/documentation annotations nested among the elements are separated out into diff --git a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs index 2a8f8c5..02de1d7 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs @@ -248,7 +248,11 @@ public async Task Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions() /// and feature kind/name — not full reference resolution — per the design decision that an /// anonymous control node's synthesized $-prefixed name is expected to produce an /// "Unresolved reference" warning (cosmetic only: ActionFlowViewLayoutStrategy reads - /// the raw text directly, never ResolvedEdges). + /// the raw text directly, never ResolvedEdges). Each fixture's leading `then` + /// (sourceSuccessionMember) is also asserted to synthesize its implicit incoming + /// succession from the immediately preceding sibling (e.g. TurnOn->$fork0, + /// J->F, and the ChargeBattery declare-then-declare chain), since the + /// grammar's leading marker itself carries no name of its own. /// [Fact] public async Task ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge() @@ -280,6 +284,12 @@ public async Task ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge() Assert.Contains(forkSuccessions, t => t.Target == "monitorTraction"); Assert.Contains(forkSuccessions, t => t.Target == "braking"); + // The fork's own leading `then` (sourceSuccessionMember) synthesizes the implicit + // incoming succession from the immediately preceding sibling action `TurnOn`. + Assert.Contains( + brake.Children.OfType(), + t => t.Source == "TurnOn" && t.Target == brakeFork.Name); + var joinNode = Assert.Single( brake.Children.OfType(), f => f.FeatureKeyword == "join"); Assert.Equal("joinNode", joinNode.Name); @@ -305,6 +315,14 @@ public async Task ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge() chargeBattery.Children.OfType(), f => f.FeatureKeyword == "merge" && f.Name == "continueCharging"); + // Each of ChargeBattery's leading-`then` chain items (`first start; then merge + // continueCharging; then action monitor: MonitorBattery{...}; then decide;`) synthesizes + // its own implicit incoming succession from the immediately preceding sibling. + var chargeBatteryTransitions = chargeBattery.Children.OfType().ToList(); + Assert.Contains(chargeBatteryTransitions, t => t.Source == "start" && t.Target == "continueCharging"); + Assert.Contains(chargeBatteryTransitions, t => t.Source == "continueCharging" && t.Target == "monitor"); + Assert.Contains(chargeBatteryTransitions, t => t.Source == "monitor" && t.Target == decide.Name); + // ControlNodeTest.sysml: fully named fork/join/merge — the richest, most reliable fixture. var controlNodeTest = (SysmlDefinitionNode)result.Workspace!.Declarations["ControlNodeTest"]; var controlFeatures = controlNodeTest.Children.OfType().ToList(); @@ -319,5 +337,9 @@ public async Task ControlNode_OmgCorpusFixture_ResolvesForkJoinDecisionMerge() Assert.Contains(controlTransitions, t => t.Source == "F" && t.Target == "B2"); Assert.Contains(controlTransitions, t => t.Source == "B1" && t.Target == "M"); Assert.Contains(controlTransitions, t => t.Source == "B2" && t.Target == "M"); + + // The join's own leading `then` synthesizes the implicit incoming succession from the + // immediately preceding sibling `J`. + Assert.Contains(controlTransitions, t => t.Source == "J" && t.Target == "F"); } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index 62e21e0..302d520 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -3957,7 +3957,10 @@ action def Flow { /// /// Multiple actionTargetSuccessionMembers attached after a single /// actionBehaviorMember (e.g. a fork's three outgoing branches) are all captured, - /// each sharing the preceding node's name as their implicit Source. + /// each sharing the preceding node's name as their implicit Source. This also + /// verifies the fork's own leading then (sourceSuccessionMember) synthesizes + /// the implicit *incoming* succession from the immediately preceding sibling (a), + /// since the grammar's leading marker itself carries no name of its own. /// [Fact] public async Task WorkspaceLoader_LoadAsync_MultipleActionTargetSuccessions_CapturesAll() @@ -3991,6 +3994,11 @@ action def Flow { Assert.Equal(2, successions.Count); Assert.Contains(successions, t => t.Target == "b1"); Assert.Contains(successions, t => t.Target == "b2"); + + var incoming = flow.Children + .OfType() + .Single(t => t.Target == "f"); + Assert.Equal("a", incoming.Source); } finally { From fa3bf22015f30f613899a764be51fdba3261f10e Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 17:25:15 -0400 Subject: [PATCH 7/7] Fix QualifiedName registration for named action-flow control nodes BuildActionNodeFeature previously always set QualifiedName to null, even for explicitly named fork/join/merge/decide/accept/send nodes. This meant named control nodes were never registered in the symbol table and were not subject to expose-scope filtering in ActionFlowViewLayoutStrategy.CollectActions, unlike every other named feature in AstBuilder. Now, when a control node has an explicitly declared name, QualifiedName is populated via QualifyName (mirroring BuildStateActionFeatureNode); only the synthesized anonymous $ names keep QualifiedName = null. --- .../semantic/model/ast-builder.md | 7 ++++++- .../Semantic/Model/AstBuilder.cs | 21 ++++++++++++------- .../Semantic/WorkspaceLoaderTests.cs | 20 +++++++++++++++--- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index b0d5ea3..b5faeda 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -396,7 +396,12 @@ so leaving these nodes nameless would make it impossible to wire their successio distinct badge for them. The synthetic name is never registered in the symbol table (`QualifiedName` stays `null`, so `$`-prefixed names never resolve and surface only as cosmetic "unresolved reference" warnings) and is blanked from rendered labels by -`ActionFlowViewLayoutStrategy`; it exists purely as an internal succession-wiring mechanism. +`ActionFlowViewLayoutStrategy`; it exists purely as an internal succession-wiring mechanism. When +the control node instead has an *explicitly declared* name (e.g. `fork buildFork;`), it is treated +like any other named feature: `QualifiedName` is populated via the same `QualifyName` helper used +by `BuildStateActionFeatureNode`, so it is registered in the symbol table and correctly subject to +expose-scope filtering (`ExposeScopeResolver.IsInSubjectScope`) in +`ActionFlowViewLayoutStrategy.CollectActions`. `assignmentNode`, `terminateNode`, `ifNode`, `whileLoopNode`, and `forLoopNode` remain intentionally unhandled — a pre-existing gap, not introduced by this change. diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index 5ec4868..744e2d2 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -640,21 +640,26 @@ private SysmlTransitionNode BuildActionTargetSuccession( /// $<keyword><n> name is assigned via /// so the node still renders as a distinct shape and can still act as those successions' /// implicit source; its stays - /// (this is purely local succession-wiring data, never registered in the symbol table or - /// referenced across files/scopes). The nested actionBody's internal semantics are - /// deliberately NOT modeled, mirroring . - /// assignmentNode/terminateNode/ifNode/whileLoopNode/ - /// forLoopNode remain an intentional, out-of-scope gap — not handled here or - /// anywhere else in . + /// in that synthesized case (this is purely local succession-wiring data, never registered + /// in the symbol table or referenced across files/scopes). When the node instead has an + /// explicitly declared name (e.g. fork buildFork;), it is treated like any other + /// named feature in this file: its is populated via + /// so it is registered in the symbol table and correctly subject + /// to expose-scope filtering, mirroring . The + /// nested actionBody's internal semantics are deliberately NOT modeled, mirroring + /// . assignmentNode/terminateNode/ + /// ifNode/whileLoopNode/forLoopNode remain an intentional, out-of-scope + /// gap — not handled here or anywhere else in . /// private SysmlFeatureNode BuildActionNodeFeature(SysMLv2Parser.UsageDeclarationContext? decl, string keyword) { - var name = GetDeclaredName(decl?.identification()) ?? $"${keyword}{_anonymousNodeCounter++}"; + var declaredName = GetDeclaredName(decl?.identification()); + var name = declaredName ?? $"${keyword}{_anonymousNodeCounter++}"; return new SysmlFeatureNode { Name = name, - QualifiedName = null, + QualifiedName = declaredName is not null ? QualifyName(name) : null, FeatureKeyword = keyword, Children = Array.Empty(), Annotations = Array.Empty(), diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index 302d520..02fef40 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -4137,7 +4137,10 @@ action def Flow { /// /// Named control nodes (e.g. fork f;, join j;) keep their declared name - /// instead of a synthesized one. + /// instead of a synthesized one, and — unlike anonymous control nodes — are given a + /// non-null, fully-qualified QualifiedName (mirroring BuildStateActionFeatureNode's + /// handling of named entry/do/exit actions), so they are correctly registered in the + /// symbol table and subject to expose-scope filtering. /// [Fact] public async Task WorkspaceLoader_LoadAsync_NamedControlNodes_KeepDeclaredName() @@ -4167,8 +4170,19 @@ action def Flow { result.Workspace!.Declarations["AF::Flow"]); var features = flow.Children.OfType().ToList(); - Assert.Contains(features, f => f.FeatureKeyword == "fork" && f.Name == "f"); - Assert.Contains(features, f => f.FeatureKeyword == "join" && f.Name == "j"); + var fork = Assert.Single(features, f => f.FeatureKeyword == "fork" && f.Name == "f"); + Assert.Equal("AF::Flow::f", fork.QualifiedName); + + var join = Assert.Single(features, f => f.FeatureKeyword == "join" && f.Name == "j"); + Assert.Equal("AF::Flow::j", join.QualifiedName); + + // The named control nodes must actually be registered in the symbol table (this is + // the crux of the fix: previously QualifiedName was always null, so RegisterAll never + // registered named fork/join/merge/decide/accept/send nodes). + Assert.True(result.Workspace!.Declarations.TryGetValue("AF::Flow::f", out var forkSymbol)); + Assert.Same(fork, forkSymbol); + Assert.True(result.Workspace!.Declarations.TryGetValue("AF::Flow::j", out var joinSymbol)); + Assert.Same(join, joinSymbol); } finally {