From 8f6396af70419346cb3240a578719168ceae7280 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Sun, 26 Apr 2026 11:47:07 +0200 Subject: [PATCH 1/3] fix: suppress default anchor fragments in describe Symptom: microflow describe emitted explicit @anchor fragments for flows that used the MDL default connection sides, creating cosmetic drift in round-trip output. Root cause: anchor formatting printed every known from/to side even when the value matched the statement default: regular flows use right-to-left, true branches use right-to-left, and false branches use bottom-to-top. Fix: omit default anchor fragments and skip the annotation entirely when all sides are defaults, while preserving explicit non-default sides. Tests: added regression coverage for omitted regular and split defaults, and adjusted existing split-anchor tests to assert that non-default branch anchors are still emitted. --- .../cmd_microflows_describe_anchor_test.go | 32 ++++++++++ ...cmd_microflows_describe_concurrent_test.go | 6 +- mdl/executor/cmd_microflows_show_helpers.go | 23 +++++-- ...d_microflows_split_incoming_anchor_test.go | 61 ++++++++++++++----- 4 files changed, 98 insertions(+), 24 deletions(-) diff --git a/mdl/executor/cmd_microflows_describe_anchor_test.go b/mdl/executor/cmd_microflows_describe_anchor_test.go index 011365f8..a18da17c 100644 --- a/mdl/executor/cmd_microflows_describe_anchor_test.go +++ b/mdl/executor/cmd_microflows_describe_anchor_test.go @@ -50,6 +50,38 @@ func TestEmitAnchorAnnotation_FromAndTo(t *testing.T) { } } +func TestEmitAnchorAnnotation_OmitsDefaultRightToLeft(t *testing.T) { + activity := µflows.ActionActivity{ + BaseActivity: microflows.BaseActivity{ + BaseMicroflowObject: microflows.BaseMicroflowObject{ + BaseElement: model.BaseElement{ID: "act-default"}, + }, + }, + } + incoming := µflows.SequenceFlow{ + DestinationID: "act-default", + DestinationConnectionIndex: AnchorLeft, + } + outgoing := µflows.SequenceFlow{ + OriginID: "act-default", + OriginConnectionIndex: AnchorRight, + } + + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + "act-default": {outgoing}, + } + flowsByDest := map[model.ID][]*microflows.SequenceFlow{ + "act-default": {incoming}, + } + + var lines []string + emitAnchorAnnotation(activity, flowsByOrigin, flowsByDest, &lines, "") + + if len(lines) != 0 { + t.Fatalf("expected default anchor line to be omitted, got %v", lines) + } +} + func TestEmitAnchorAnnotation_NoFlowsSkipsEmission(t *testing.T) { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ diff --git a/mdl/executor/cmd_microflows_describe_concurrent_test.go b/mdl/executor/cmd_microflows_describe_concurrent_test.go index 9f2f3662..1bf06a8b 100644 --- a/mdl/executor/cmd_microflows_describe_concurrent_test.go +++ b/mdl/executor/cmd_microflows_describe_concurrent_test.go @@ -26,7 +26,7 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) { // Build two distinct microflows whose activities are anchored to // different sides. If the two describe calls share state, one will // emit the other's anchor keyword. - mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorRight) + mfA := mkRaceMicroflow("mfa-start", "mfa-log", "mfa-end", AnchorTop) mfB := mkRaceMicroflow("mfb-start", "mfb-log", "mfb-end", AnchorBottom) e := newTestExecutor() @@ -50,8 +50,8 @@ func TestFormatMicroflowActivities_Concurrent_NoRace(t *testing.T) { } wg.Wait() - wantA := "@anchor(from: right, to: left)" - wantB := "@anchor(from: bottom, to: left)" + wantA := "@anchor(from: top)" + wantB := "@anchor(from: bottom)" for i, got := range resultsA { if !strings.Contains(got, wantA) { t.Errorf("worker %d (A) missing %q in output:\n%s", i, wantA, got) diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index 6c646abb..dfc4b765 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -128,12 +128,15 @@ func emitAnchorAnnotation( return } var parts []string - if from != "" { + if from != "" && from != "right" { parts = append(parts, "from: "+from) } - if to != "" { + if to != "" && to != "left" { parts = append(parts, "to: "+to) } + if len(parts) == 0 { + return + } *lines = append(*lines, indentStr+fmt.Sprintf("@anchor(%s)", strings.Join(parts, ", "))) } @@ -176,13 +179,13 @@ func emitSplitAnchorAnnotation( } var parts []string - if inTo != "" { + if inTo != "" && inTo != "left" { parts = append(parts, "to: "+inTo) } - if p := branchAnchorFragment("true", trueFrom, trueTo); p != "" { + if p := branchAnchorFragmentWithDefaults("true", trueFrom, trueTo, "right", "left"); p != "" { parts = append(parts, p) } - if p := branchAnchorFragment("false", falseFrom, falseTo); p != "" { + if p := branchAnchorFragmentWithDefaults("false", falseFrom, falseTo, "bottom", "top"); p != "" { parts = append(parts, p) } if len(parts) == 0 { @@ -207,6 +210,16 @@ func branchAnchorFragment(label, from, to string) string { return fmt.Sprintf("%s: (%s)", label, strings.Join(inner, ", ")) } +func branchAnchorFragmentWithDefaults(label, from, to, defaultFrom, defaultTo string) string { + if from == defaultFrom { + from = "" + } + if to == defaultTo { + to = "" + } + return branchAnchorFragment(label, from, to) +} + // emitLoopAnchorAnnotation emits the loop form of @anchor for a LoopedActivity. // A LoopedActivity has up to four flows worth describing: // - the incoming flow from the previous activity (normal `to:`) diff --git a/mdl/executor/cmd_microflows_split_incoming_anchor_test.go b/mdl/executor/cmd_microflows_split_incoming_anchor_test.go index 8bb51263..a3ce841f 100644 --- a/mdl/executor/cmd_microflows_split_incoming_anchor_test.go +++ b/mdl/executor/cmd_microflows_split_incoming_anchor_test.go @@ -56,16 +56,16 @@ func TestEmitSplitAnchor_EmitsBranchAnchors(t *testing.T) { trueFlow := µflows.SequenceFlow{ OriginID: splitID, - OriginConnectionIndex: AnchorRight, - DestinationConnectionIndex: AnchorLeft, + OriginConnectionIndex: AnchorTop, + DestinationConnectionIndex: AnchorTop, CaseValue: microflows.EnumerationCase{ Value: "true", }, } falseFlow := µflows.SequenceFlow{ OriginID: splitID, - OriginConnectionIndex: AnchorBottom, - DestinationConnectionIndex: AnchorTop, + OriginConnectionIndex: AnchorLeft, + DestinationConnectionIndex: AnchorRight, CaseValue: microflows.EnumerationCase{ Value: "false", }, @@ -84,8 +84,8 @@ func TestEmitSplitAnchor_EmitsBranchAnchors(t *testing.T) { out := lines[0] for _, want := range []string{ - "true: (from: right, to: left)", - "false: (from: bottom, to: top)", + "true: (from: top, to: top)", + "false: (from: left, to: right)", } { if !strings.Contains(out, want) { t.Errorf("output missing %q\nfull: %s", want, out) @@ -93,6 +93,35 @@ func TestEmitSplitAnchor_EmitsBranchAnchors(t *testing.T) { } } +func TestEmitSplitAnchor_OmitsDefaultBranchAnchors(t *testing.T) { + splitID := model.ID("split-defaults") + split := µflows.ExclusiveSplit{} + split.ID = splitID + + trueFlow := µflows.SequenceFlow{ + OriginID: splitID, + OriginConnectionIndex: AnchorRight, + DestinationConnectionIndex: AnchorLeft, + CaseValue: µflows.ExpressionCase{Expression: "true"}, + } + falseFlow := µflows.SequenceFlow{ + OriginID: splitID, + OriginConnectionIndex: AnchorBottom, + DestinationConnectionIndex: AnchorTop, + CaseValue: µflows.ExpressionCase{Expression: "false"}, + } + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + splitID: {trueFlow, falseFlow}, + } + + var lines []string + emitAnchorAnnotation(split, flowsByOrigin, nil, &lines, "") + + if len(lines) != 0 { + t.Fatalf("expected default branch anchor line to be omitted, got %v", lines) + } +} + func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) { // Mendix splits often use ExpressionCase (Expression == "true" / "false") // instead of EnumerationCase. The anchor emission must identify the @@ -105,16 +134,16 @@ func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) { trueFlow := µflows.SequenceFlow{ OriginID: splitID, - OriginConnectionIndex: AnchorRight, - DestinationConnectionIndex: AnchorLeft, + OriginConnectionIndex: AnchorTop, + DestinationConnectionIndex: AnchorTop, CaseValue: µflows.ExpressionCase{ Expression: "true", }, } falseFlow := µflows.SequenceFlow{ OriginID: splitID, - OriginConnectionIndex: AnchorBottom, - DestinationConnectionIndex: AnchorTop, + OriginConnectionIndex: AnchorLeft, + DestinationConnectionIndex: AnchorRight, CaseValue: µflows.ExpressionCase{ Expression: "false", }, @@ -131,8 +160,8 @@ func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) { } out := lines[0] for _, want := range []string{ - "true: (from: right, to: left)", - "false: (from: bottom, to: top)", + "true: (from: top, to: top)", + "false: (from: left, to: right)", } { if !strings.Contains(out, want) { t.Errorf("output missing %q\nfull: %s", want, out) @@ -155,8 +184,8 @@ func TestEmitSplitAnchor_SupportsBooleanCase(t *testing.T) { } falseFlow := µflows.SequenceFlow{ OriginID: splitID, - OriginConnectionIndex: AnchorBottom, - DestinationConnectionIndex: AnchorTop, + OriginConnectionIndex: AnchorLeft, + DestinationConnectionIndex: AnchorRight, CaseValue: µflows.BooleanCase{Value: false}, } flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ @@ -171,8 +200,8 @@ func TestEmitSplitAnchor_SupportsBooleanCase(t *testing.T) { } out := lines[0] for _, want := range []string{ - "true: (from: top, to: left)", - "false: (from: bottom, to: top)", + "true: (from: top)", + "false: (from: left, to: right)", } { if !strings.Contains(out, want) { t.Errorf("output missing %q\nfull: %s", want, out) From d6f947159311c2f41926e525b01ffa007a47d4d9 Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Mon, 27 Apr 2026 00:00:26 +0200 Subject: [PATCH 2/3] test: add bug-test reproducer for default anchor suppression in describe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an MDL script under mdl-examples/bug-tests/ verifying that linear flows produce zero @anchor lines (all defaults) while non-default sides like `to: top` survive a describe → exec → describe cycle. Co-Authored-By: Claude Opus 4.7 --- ...iber-suppress-default-anchor-fragments.mdl | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl diff --git a/mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl b/mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl new file mode 100644 index 00000000..6eb706b5 --- /dev/null +++ b/mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl @@ -0,0 +1,55 @@ +-- ============================================================================ +-- Bug #320: DESCRIBE emitted default anchor fragments redundantly +-- ============================================================================ +-- +-- Symptom (before fix): +-- `describe microflow` printed `@anchor` lines for every activity, even +-- when every from/to side matched the MDL default for that flow shape: +-- - regular sequence flows default to `from: right, to: left` +-- - true branches of IF default to `from: right, to: left` +-- - false branches of IF default to `from: bottom, to: top` +-- Output looked like +-- log info node 'X' 'a'; +-- @anchor(from: right, to: left) -- noise: this is the default +-- log info node 'X' 'b'; +-- producing verbose, non-author-friendly DESCRIBE output and noisy +-- `mxcli diff-local` runs after a roundtrip. +-- +-- After fix: +-- `emitAnchorAnnotation` and `emitSplitAnchorAnnotation` (in +-- cmd_microflows_show_helpers.go) suppress fragments that match the +-- default and skip the entire annotation when all sides are defaults. +-- Non-default sides (e.g. `to: top` on a return) are still emitted. +-- +-- Usage: +-- mxcli exec mdl-examples/bug-tests/320-describer-suppress-default-anchor-fragments.mdl -p app.mpr +-- +-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_LinearDefaults" +-- The output must contain NO `@anchor` line — every flow uses defaults. +-- +-- mxcli -p app.mpr -c "describe microflow BugTest320.MF_NonDefaultReturn" +-- The output must keep `@anchor(to: top)` on the return statement. +-- ============================================================================ + +create module BugTest320; + +-- Linear flow, all defaults — describe must emit zero @anchor lines. +create microflow BugTest320.MF_LinearDefaults () +begin + log info node 'BugTest320' 'a'; + log info node 'BugTest320' 'b'; + log info node 'BugTest320' 'c'; +end; +/ + +-- One non-default `to: top` on the return — describe must keep that fragment. +create microflow BugTest320.MF_NonDefaultReturn ( + $value: integer +) +returns boolean as $ok +begin + log info node 'BugTest320' 'check'; + @anchor(to: top) + return $value > 0; +end; +/ From bffbff41495ef0646d105ea6c5c76bc4c2aec66c Mon Sep 17 00:00:00 2001 From: Henrique Costa Date: Mon, 27 Apr 2026 19:47:56 +0200 Subject: [PATCH 3/3] fix: suppress builder-default split anchors Symptom: describe/exec/describe could add @anchor metadata to IF statements whose branch flows only used the layout that the microflow builder generated. Root cause: split anchor emission only treated one hard-coded true/false branch layout as default. IF statements without an ELSE use a different generated layout, with the true branch going down and the false branch continuing horizontally, so the describer exposed those generated anchors as authored metadata. Fix: suppress branch fragments when their from/to sides match either supported builder-default split layout, while still emitting non-default destination sides. Tests: added split-anchor regressions for the no-ELSE builder defaults and for a non-default destination anchor; ran make build, make lint-go, and make test. --- mdl/executor/cmd_microflows_show_helpers.go | 19 +++++-- ...d_microflows_split_incoming_anchor_test.go | 55 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index dfc4b765..6b6af284 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -182,10 +182,10 @@ func emitSplitAnchorAnnotation( if inTo != "" && inTo != "left" { parts = append(parts, "to: "+inTo) } - if p := branchAnchorFragmentWithDefaults("true", trueFrom, trueTo, "right", "left"); p != "" { + if p := branchAnchorFragmentWithDefaultSides("true", trueFrom, trueTo, []string{"right", "bottom"}, []string{"left"}); p != "" { parts = append(parts, p) } - if p := branchAnchorFragmentWithDefaults("false", falseFrom, falseTo, "bottom", "top"); p != "" { + if p := branchAnchorFragmentWithDefaultSides("false", falseFrom, falseTo, []string{"bottom", "right"}, []string{"top", "left"}); p != "" { parts = append(parts, p) } if len(parts) == 0 { @@ -210,16 +210,25 @@ func branchAnchorFragment(label, from, to string) string { return fmt.Sprintf("%s: (%s)", label, strings.Join(inner, ", ")) } -func branchAnchorFragmentWithDefaults(label, from, to, defaultFrom, defaultTo string) string { - if from == defaultFrom { +func branchAnchorFragmentWithDefaultSides(label, from, to string, defaultFroms, defaultTos []string) string { + if containsString(defaultFroms, from) { from = "" } - if to == defaultTo { + if containsString(defaultTos, to) { to = "" } return branchAnchorFragment(label, from, to) } +func containsString(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} + // emitLoopAnchorAnnotation emits the loop form of @anchor for a LoopedActivity. // A LoopedActivity has up to four flows worth describing: // - the incoming flow from the previous activity (normal `to:`) diff --git a/mdl/executor/cmd_microflows_split_incoming_anchor_test.go b/mdl/executor/cmd_microflows_split_incoming_anchor_test.go index a3ce841f..ac7c5df3 100644 --- a/mdl/executor/cmd_microflows_split_incoming_anchor_test.go +++ b/mdl/executor/cmd_microflows_split_incoming_anchor_test.go @@ -122,6 +122,61 @@ func TestEmitSplitAnchor_OmitsDefaultBranchAnchors(t *testing.T) { } } +func TestEmitSplitAnchor_OmitsBuilderNoElseBranchAnchors(t *testing.T) { + splitID := model.ID("split-builder-defaults") + split := µflows.ExclusiveSplit{} + split.ID = splitID + + trueFlow := µflows.SequenceFlow{ + OriginID: splitID, + OriginConnectionIndex: AnchorBottom, + DestinationConnectionIndex: AnchorLeft, + CaseValue: µflows.ExpressionCase{Expression: "true"}, + } + falseFlow := µflows.SequenceFlow{ + OriginID: splitID, + OriginConnectionIndex: AnchorRight, + DestinationConnectionIndex: AnchorLeft, + CaseValue: µflows.ExpressionCase{Expression: "false"}, + } + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + splitID: {trueFlow, falseFlow}, + } + + var lines []string + emitAnchorAnnotation(split, flowsByOrigin, nil, &lines, "") + + if len(lines) != 0 { + t.Fatalf("expected builder-generated branch anchors to be omitted, got %v", lines) + } +} + +func TestEmitSplitAnchor_EmitsNonDefaultDestinationAgainstBuilderDefaults(t *testing.T) { + splitID := model.ID("split-non-default-destination") + split := µflows.ExclusiveSplit{} + split.ID = splitID + + trueFlow := µflows.SequenceFlow{ + OriginID: splitID, + OriginConnectionIndex: AnchorBottom, + DestinationConnectionIndex: AnchorTop, + CaseValue: µflows.ExpressionCase{Expression: "true"}, + } + flowsByOrigin := map[model.ID][]*microflows.SequenceFlow{ + splitID: {trueFlow}, + } + + var lines []string + emitAnchorAnnotation(split, flowsByOrigin, nil, &lines, "") + + if len(lines) != 1 { + t.Fatalf("expected non-default destination anchor to be emitted, got %v", lines) + } + if !strings.Contains(lines[0], "true: (to: top)") { + t.Fatalf("expected true branch destination anchor, got %q", lines[0]) + } +} + func TestEmitSplitAnchor_SupportsExpressionCase(t *testing.T) { // Mendix splits often use ExpressionCase (Expression == "true" / "false") // instead of EnumerationCase. The anchor emission must identify the