From ec0c7ed4cf140f14b7453a0057cf7215b103d15d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 19:25:45 +0000 Subject: [PATCH 1/5] fix(pages): write Variable: null on an Expression-bound flow argument Studio Pro writes Expression, Parameter and Variable on every Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping and nulls the unused slot. An Expression-bound argument left the gen Variable part unset, so the encoder omitted the key, and opening the change in Studio Pro's Changes panel threw "do not have the same properties. baseNames = Expression, Parameter, Variable; newNames = Parameter, Expression". Register Variable as a null field on both types, which covers the action, nanoflow action and nanoflow data-source writers alike. Refs mendixlabs/mxcli#1317 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HamahFUo9MM2xuaNmm5mhr --- .../widget_flow_param_null_variable_test.go | 98 +++++++++++++++++++ mdl/backend/modelsdk/widget_write.go | 12 +++ 2 files changed, 110 insertions(+) create mode 100644 mdl/backend/modelsdk/widget_flow_param_null_variable_test.go diff --git a/mdl/backend/modelsdk/widget_flow_param_null_variable_test.go b/mdl/backend/modelsdk/widget_flow_param_null_variable_test.go new file mode 100644 index 000000000..d8d2883cc --- /dev/null +++ b/mdl/backend/modelsdk/widget_flow_param_null_variable_test.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 + +package modelsdkbackend + +import ( + "sort" + "strings" + "testing" + + bsonv1 "go.mongodb.org/mongo-driver/bson" + + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// mendixlabs/mxcli#1317 — a flow argument bound through Expression (a literal, +// $currentObject, a path, a primitive parameter) was written WITHOUT the +// Variable key. Studio Pro always writes all three keys and nulls the unused +// slot, so opening the change in the Changes panel crashed with +// +// Objects with ID … of type Forms$MicroflowParameterMapping do not have the +// same properties. baseNames = Expression, Parameter, Variable; +// newNames = Parameter, Expression +// +// mx check and mxbuild are both clean on it; only the Changes-panel diff sees +// the key set. Same class as #1180 (DomainModels$NoGeneralization). + +// mappingKeys returns the non-$ keys of a parameter mapping, sorted. +func mappingKeys(pm bsonv1.D) []string { + var keys []string + for _, e := range pm { + if !strings.HasPrefix(e.Key, "$") { + keys = append(keys, e.Key) + } + } + sort.Strings(keys) + return keys +} + +// assertStudioProMappingShape checks the key set Studio Pro writes for a +// Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping at 11.12.2 +// (the baseNames of the reported crash), with Variable null when unused. +func assertStudioProMappingShape(t *testing.T, pm bsonv1.D) { + t.Helper() + got := strings.Join(mappingKeys(pm), ", ") + if want := "Expression, Parameter, Variable"; got != want { + t.Fatalf("mapping keys = %s; Studio Pro writes %s — the Changes panel "+ + "throws \"do not have the same properties\" on the difference", got, want) + } + if v := docGet(pm, "Variable"); v != nil { + t.Errorf("Variable = %#v, want null for an Expression-bound argument", v) + } +} + +func TestMicroflowActionExpressionArgWritesNullVariable(t *testing.T) { + a := &pages.MicroflowClientAction{ + MicroflowName: "LearningJourney.ACT_ExcludeItemStep", + ParameterMappings: []*pages.MicroflowParameterMapping{ + {ParameterName: "LearningJourney", Expression: "$LearningJourney"}, + }, + } + // The reported path: a pluggable widget's named action slot, serialized + // through the child serializer the page mutator calls. + doc := codecChildSerializer{}.SerializeClientAction(a) + settings, ok := docGet(doc, "MicroflowSettings").(bsonv1.D) + if !ok { + t.Fatalf("MicroflowSettings missing: %#v", doc) + } + assertStudioProMappingShape(t, firstParamMapping(t, settings)) +} + +func TestNanoflowActionExpressionArgWritesNullVariable(t *testing.T) { + a := &pages.NanoflowClientAction{ + NanoflowName: "MyModule.ACT_Toggle_NF", + ParameterMappings: []*pages.NanoflowParameterMapping{ + {ParameterName: "Flag", Expression: "true"}, + }, + } + assertStudioProMappingShape(t, firstParamMapping(t, encodeAction(t, a))) +} + +// Control: the Variable-bound form already wrote all three keys (#1140) and +// must keep its PageVariable rather than be nulled by the default. +func TestMicroflowActionVariableArgKeepsPageVariable(t *testing.T) { + a := &pages.MicroflowClientAction{ + MicroflowName: "LearningJourney.ACT_ExcludeItemStep", + ParameterMappings: []*pages.MicroflowParameterMapping{ + {ParameterName: "LearningJourney", Variable: "$LearningJourney", VariableKind: "parameter"}, + }, + } + settings, _ := docGet(encodeAction(t, a), "MicroflowSettings").(bsonv1.D) + pm := firstParamMapping(t, settings) + if got := strings.Join(mappingKeys(pm), ", "); got != "Expression, Parameter, Variable" { + t.Fatalf("mapping keys = %s", got) + } + if paramVariable(pm) == nil { + t.Fatalf("Variable = %#v, want a Forms$PageVariable", docGet(pm, "Variable")) + } +} diff --git a/mdl/backend/modelsdk/widget_write.go b/mdl/backend/modelsdk/widget_write.go index 9b6ebdeb2..e6a03b2aa 100644 --- a/mdl/backend/modelsdk/widget_write.go +++ b/mdl/backend/modelsdk/widget_write.go @@ -297,6 +297,18 @@ func init() { codec.RegisterListMarker("Forms$MicroflowParameterMapping", 2) codec.RegisterListMarker("Forms$PageParameterMapping", 2) codec.RegisterListMarker("Forms$SnippetParameterMapping", 2) + // A flow argument fills one of two slots and Studio Pro writes both. The + // Variable side already writes Expression "" (bindParameterMappingValue); + // an Expression-bound argument left Variable unset, so the key was omitted + // and Studio Pro's Changes panel threw "do not have the same properties. + // baseNames = Expression, Parameter, Variable; newNames = Parameter, + // Expression" (mendixlabs/mxcli#1317, the #1180 class). + codec.RegisterTypeDefaults("Forms$MicroflowParameterMapping", codec.TypeDefaults{ + NullFields: []string{"Variable"}, + }) + codec.RegisterTypeDefaults("Forms$NanoflowParameterMapping", codec.TypeDefaults{ + NullFields: []string{"Variable"}, + }) codec.RegisterTypeDefaults("Forms$SnippetCall", codec.TypeDefaults{ MandatoryListMarkers: map[string]int32{"ParameterMappings": 2}, }) From 82e1ccf56d0184497f298722e71a3a8e960059d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 19:25:45 +0000 Subject: [PATCH 2/5] fix(alter page): seed parameter scope when SET builds an action convertASTAction built its pageBuilder with no parameter or variable scope, so `set (Action: call microflow M.F(P = $P))` and a pluggable widget's named action slot wrote a page parameter as the Expression "$P" rather than binding it through Variable -> Forms$PageVariable (the #1140 form, which Studio Pro does not resolve). Seed it from the mutator as INSERT/REPLACE already do, including isSnippet. Refs mendixlabs/mxcli#1317 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HamahFUo9MM2xuaNmm5mhr --- mdl/executor/cmd_alter_page.go | 32 ++++++--- mdl/executor/cmd_alter_page_action_test.go | 78 ++++++++++++++++++++++ 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index 90ddb441b..6b75e4022 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -271,7 +271,7 @@ func applySetPropertyMutator(ctx *ExecContext, mutator backend.PageMutator, op * } else if propName == "Action" { // Action is a polymorphic node, not a scalar — it goes through the // same builder CREATE PAGE uses rather than being written as a value. - action, err := convertASTAction(ctx, value, moduleName, moduleID) + action, err := convertASTAction(ctx, mutator, value, moduleName, moduleID) if err != nil { return err } @@ -284,7 +284,7 @@ func applySetPropertyMutator(ctx *ExecContext, mutator backend.PageMutator, op * // Same builder as `Action`; the mutator checks the key is an // action-typed property of the stored widget. Through // SetWidgetProperty it would be stringified into a PrimitiveValue. - action, err := convertASTAction(ctx, value, moduleName, moduleID) + action, err := convertASTAction(ctx, mutator, value, moduleName, moduleID) if err != nil { return err } @@ -363,21 +363,31 @@ func convertASTDataSource(value interface{}) (pages.DataSource, error) { // alternative is the failure mode #855 documents for DataSource, where SET // carried a narrower vocabulary than REPLACE and each missing case surfaced as // its own bug report. -func convertASTAction(ctx *ExecContext, value any, moduleName string, moduleID model.ID) (pages.ClientAction, error) { +// +// The builder is seeded with the stored document's parameters and variables, as +// buildWidgetsFromAST is. Without them a `$Param` argument cannot be told from an +// expression and is written as one, which Studio Pro does not bind +// (mendixlabs/mxcli#1317, the #1140 rule). +func convertASTAction(ctx *ExecContext, mutator backend.PageMutator, value any, moduleName string, moduleID model.ID) (pages.ClientAction, error) { action, ok := value.(*ast.ActionV3) if !ok { return nil, mdlerrors.NewValidation("Action value must be an action expression, " + "for example `set (Action: microflow Module.MF) on btnSave`") } + paramScope, paramEntityNames := mutator.ParamScope() pb := &pageBuilder{ - ctx: ctx, - backend: ctx.Backend, - moduleID: moduleID, - moduleName: moduleName, - execCache: ctx.Cache, - fragments: ctx.Fragments, - themeRegistry: ctx.GetThemeRegistry(), - widgetBackend: ctx.Backend, + ctx: ctx, + backend: ctx.Backend, + moduleID: moduleID, + moduleName: moduleName, + paramScope: paramScope, + paramEntityNames: paramEntityNames, + execCache: ctx.Cache, + fragments: ctx.Fragments, + themeRegistry: ctx.GetThemeRegistry(), + widgetBackend: ctx.Backend, + localVariables: storedPageVariables(mutator), + isSnippet: mutator.ContainerType() == backend.ContainerSnippet, } return pb.buildClientActionV3(action) } diff --git a/mdl/executor/cmd_alter_page_action_test.go b/mdl/executor/cmd_alter_page_action_test.go index 93b0a76ad..02193f956 100644 --- a/mdl/executor/cmd_alter_page_action_test.go +++ b/mdl/executor/cmd_alter_page_action_test.go @@ -171,3 +171,81 @@ func TestAlterPage_SetAction_RejectsNonAction(t *testing.T) { assertError(t, err) assertContainsStr(t, err.Error(), "must be an action expression") } + +// mendixlabs/mxcli#1317 — `alter page … set ('onClickAction': call microflow +// M.F(P = $P)) on w` built the action with an empty parameter scope, so a page +// parameter argument fell through classifyFlowArgValue and was written as the +// Expression "$P" — the form #1140 showed Studio Pro does not bind (CE1571 on +// opening the page). CREATE PAGE and ALTER PAGE INSERT/REPLACE already seed the +// scope from the stored page; SET must too. +func TestAlterPage_SetNamedAction_PageParamBindsThroughVariable(t *testing.T) { + for _, tc := range []struct { + name string + container backend.ContainerKind + wantKind string + }{ + {"page", backend.ContainerPage, "parameter"}, + {"snippet", backend.ContainerSnippet, "snippet"}, + } { + t.Run(tc.name, func(t *testing.T) { + mod := mkModule("LearningJourney") + pg := mkPage(mod.ID, "LearningJourney_Details") + mf := mkMicroflow(mod.ID, "ACT_ExcludeItemStep") + var got pages.ClientAction + + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListFoldersFunc: func() ([]*types.FolderInfo, error) { return nil, nil }, + ListPagesFunc: func() ([]*pages.Page, error) { return []*pages.Page{pg}, nil }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return []*microflows.Microflow{mf}, nil }, + OpenPageForMutationFunc: func(unitID model.ID) (backend.PageMutator, error) { + return &mock.MockPageMutator{ + ContainerTypeFunc: func() backend.ContainerKind { return tc.container }, + ParamScopeFunc: func() (map[string]model.ID, map[string]string) { + return map[string]model.ID{"LearningJourney": "p1"}, + map[string]string{"LearningJourney": "LearningJourney.LearningJourney"} + }, + SetWidgetNamedActionFunc: func(widgetRef, key string, action pages.ClientAction) error { + got = action + return nil + }, + SaveFunc: func() error { return nil }, + }, nil + }, + } + h := mkHierarchy(mod) + withContainer(h, pg.ContainerID, mod.ID) + withContainer(h, mf.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + + assertNoError(t, execAlterPage(ctx, &ast.AlterPageStmt{ + PageName: ast.QualifiedName{Module: "LearningJourney", Name: "LearningJourney_Details"}, + Operations: []ast.AlterPageOperation{ + &ast.SetPropertyOp{ + Target: ast.WidgetRef{Widget: "pDSLink_ButtonAndTracking8"}, + Properties: map[string]any{"onClickAction": &ast.ActionV3{ + Type: "microflow", + Target: "LearningJourney.ACT_ExcludeItemStep", + Args: []ast.FlowArgV3{{Name: "LearningJourney", Value: "$LearningJourney"}}, + }}, + }, + }, + })) + + mfa, ok := got.(*pages.MicroflowClientAction) + if !ok { + t.Fatalf("action = %T, want *pages.MicroflowClientAction", got) + } + if len(mfa.ParameterMappings) != 1 { + t.Fatalf("mappings = %d, want 1", len(mfa.ParameterMappings)) + } + pm := mfa.ParameterMappings[0] + if pm.VariableKind != tc.wantKind || pm.Variable != "$LearningJourney" { + t.Errorf("mapping = Variable %q kind %q, want $LearningJourney kind %q — "+ + "an unclassified $-argument is written as an Expression Studio Pro does not bind", + pm.Variable, pm.VariableKind, tc.wantKind) + } + }) + } +} From 4d30699ca964e0df53d0f1e847351c2578fda6db Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 19:25:45 +0000 Subject: [PATCH 3/5] test(pages): bug-test and finding for flow-argument mapping shape Regression MDL for mendixlabs/mxcli#1317 and the finding: `mx diff` reproduces Studio Pro Changes-panel crashes headless. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HamahFUo9MM2xuaNmm5mhr --- .../fix-issue/findings/mdl-backend.jsonl | 1 + .../1317-flow-arg-mapping-null-variable.mdl | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 mdl-examples/bug-tests/1317-flow-arg-mapping-null-variable.mdl diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl index 0734bee63..776ee1c5a 100644 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl @@ -170,3 +170,4 @@ {"area": "mdl/backend", "date": "2026-10-04", "symptom": "ako/mxcli#980: create or modify navigation on a native profile ignored its menu block, sync block, login/not-found page and on-sync-error while reporting 'updated'; describe printed a native nanoflow home as `home microflow` and bottom bar items without action or icon", "cause": "navPatchNativeProfile only patched home pages; nativeNavProfileFromGen read BottomBarItem.Page only; the grammar had no nanoflow home", "file": "`mdl/backend/modelsdk/navigation_write.go` navPatchNativeProfile, `navigation_read.go` nativeNavProfileFromGen, `mdl/executor/cmd_navigation.go` checkProfileClauses", "insight": "A profile-kind-specific writer must refuse every clause it does not apply (checked in the executor before anything is written, including profile creation), and describe must not print what exec would refuse — list it as comments. No native profile exists in any local fixture, so the reader is pinned with a hand-built gen document", "refs": ["#980"]} {"area": "mdl/backend", "date": "2026-10-05", "symptom": "On Mendix 11.15 `TestMappingFixtureRoundTrip` fails for the blank app's own FeedbackModule mappings: describe -> exec adds `\"MessageDefinition\": \"\"`; and a mapping `with message definition` built on 11.15 fails `mx check` with CE0270 \"No root element could be found in the schema\"", "cause": "Mendix 11.15 REMOVED the mapping's `MessageDefinition` key (message definitions became `MessageDefinitions$MessageDefinition2` documents; the source moved to `MessageDefinition2` as `Module.MessageName`, measured with `mx convert` 11.15.0). The writer added the key unconditionally and the executor put the source in it, which 11.15 no longer reads", "file": "`model.ImportMapping`/`ExportMapping` (`MessageDefinition *string`, `MessageDefinitionSource()`), `mdl/backend/modelsdk/mapping_read.go` (`optionalStringFromRaw`) + `mapping_write.go`, `mdl/executor/mapping_messagedefinition.go` (`mappingMessageDefinitionKeys`, `messageDefinitionsAreDocuments`), `sdk/versions/mendix-1{0,1}.yaml` (`integration.message_definition_collection` max 11.14.99, `message_definition_document` min 11.15.0)", "insight": "**A key can be REMOVED by a version, not only introduced** \u2014 the #279 carry rule applies in both directions, so `MessageDefinition` is now a pointer like `MessageDefinition2`. The source key follows the stored document's shape on an update (a pre-11.15 document transplanted into an 11.15 project keeps its source in `MessageDefinition`, which is what kept the fixture's AgentCore/Email_Connector mappings green) and the project version only on a create. Caught by the new 11.15 nightly job, not by any fixture: none of the pinned documents was 11.15-shaped until the blank 11.15 app's own FeedbackModule mappings appeared in the base project", "refs": ["ako/mxcli#987", "ako/mxcli#279"]} {"area": "mdl/backend", "date": "2026-10-06", "symptom": "`describe microflow` prints `-- Unsupported action: Microflows$SendEmailAction` for the Studio Pro 11.13+ Send Email activity, and MDL has no statement to create it", "cause": "No reader case, and no grammar/AST/builder/writer. The vendored gen type `genMf.SendEmailAction`/`EmailMessage` predates 11.12: it binds Subject/MessageBody* as expressions, but 11.13 deleted those and stores SubjectTemplate/MessageBodyPlainTextTemplate/MessageBodyHtmlTemplate (Microflows$StringTemplate) plus a CustomHeaders list the gen type has no field for", "file": "mdl/backend/modelsdk/microflow_send_email.go", "insight": "The gen type existing (the issue cited it as 'the storage side seems known') was a false lead: it is the pre-11.12 shape, so both directions are raw-keyed against a Studio Pro-saved document (ako/TestApp Email.EmailMF) and a key-for-key shape test (TestSendEmailActionToGen_MatchesStudioProShape) pins types and markers — it caught ConnectionTimeout being int64 and the empty CustomHeaders list needing marker 3. Version facts come from mendixmodelsdk 4.116 StructureVersionInfo (templates 11.12.0, plain Subject deleted 11.13.0), hence the 11.13.0 gate. Run negative controls through mxbuild before writing check rules: of the plausible rules, 'check server identity needs SSL' and the header-name rule are ACCEPTED by mxbuild 11.15.0-rc.4 (so warnings), while type faults are CE9528 for address/host/port/user (not the CE0117 exprcheck's E009 used to name — slots can now carry their own Mxbuild code). Attachment is a bare variable name (CE0109 on a wrong one). Shapes MDL cannot restate (auth document, pre-11.13 expression subject, non-SMTP) stay UnsupportedAction. Studio Pro's Test Email tab (TestEmailMessage) is written empty on rewrite, by design. An unedited describe->exec round trip is elided as Unchanged, so it cannot demonstrate loss; author-from-script is the meaningful control (old binary: `mismatched input 'email' expecting REST`).", "refs": ["mendixlabs/mxcli#1315"], "ce": ["CE9528", "CE0117", "CE0720", "CE0109"], "rules": ["MDL-EMAIL01", "MDL-EMAIL02", "MDL-EMAIL03", "E009"]} +{"area": "mdl/backend", "date": "2026-10-06", "symptom": "Opening a change in Studio Pro's Changes panel throws `System.InvalidOperationException: Objects with ID … of type Forms$MicroflowParameterMapping do not have the same properties. baseNames = Expression, Parameter, Variable; newNames = Parameter, Expression` after `alter page … set ('onClickAction': call microflow M.F(P = $P)) on w`. `mx check` is 0 errors; the page opens. Second, silent half: `$P` (a page parameter) is stored as the Expression \"$P\", which Studio Pro does not bind (CE1571, the #1140 form)", "cause": "(1) `bindParameterMappingValue` sets only Expression for an Expression-bound argument; the gen `Variable` Part stays unset and the encoder omits an unset Part on a new element, so the key vanished. Studio Pro writes all three keys, nulling the unused slot, and its merge library compares key sets per $ID — the canon $ID carry makes the new mapping the SAME object as the stored one, so the diff sees two shapes of one element. (2) `convertASTAction` (ALTER PAGE SET Action and named action slots) built its pageBuilder with no paramScope/localVariables, so `classifyFlowArgValue` could not recognise a page/snippet parameter", "file": "`mdl/backend/modelsdk/widget_write.go` (RegisterTypeDefaults NullFields Variable on Forms$MicroflowParameterMapping + Forms$NanoflowParameterMapping), `mdl/executor/cmd_alter_page.go` (convertASTAction seeds ParamScope, storedPageVariables, isSnippet from the mutator)", "insight": "**`mx diff base.mpr new.mpr out.mpr` reproduces Changes-panel crashes headless** — it runs the same MergeLib DifferenceComputer and prints the identical \"do not have the same properties\" line (exit 129). Base = a copy of the project before the write, made by the FIXED build so it has Studio Pro's key set. That turns a Windows-GUI-only symptom into a two-binary A/B on Linux. **The base must share the $ID** — the crash needs a paired object, which the identity carry supplies; a fresh element is only an add and never trips it, so a create-only repro passes on broken code. **Register the null slot, don't set it at the call site**: `Part.Set(nil)` still encodes as omitted, and there are three construction sites (action, nanoflow action, nanoflow data source) — NullFields covers all and any future one. **A builder constructed outside CREATE PAGE is the recurring hole for #1140-style classification**: INSERT/REPLACE already seeded the scope from the mutator, SET did not; grep for `&pageBuilder{` and check each seeds paramScope + localVariables + isSnippet. Measured on mxbuild 11.12.2: buggy mx diff exit 129 with the reported line, fixed exit 0; mx check 0 errors on both", "refs": ["mendixlabs/mxcli#1317", "mendixlabs/mxcli#1180", "mendixlabs/mxcli#1140"], "ce": ["CE1571"]} diff --git a/mdl-examples/bug-tests/1317-flow-arg-mapping-null-variable.mdl b/mdl-examples/bug-tests/1317-flow-arg-mapping-null-variable.mdl new file mode 100644 index 000000000..3c7cbaf3e --- /dev/null +++ b/mdl-examples/bug-tests/1317-flow-arg-mapping-null-variable.mdl @@ -0,0 +1,71 @@ +mdl 1; +-- mendixlabs/mxcli#1317 — an ALTER PAGE that set a call-microflow action crashed +-- Studio Pro's Changes panel when the change was opened: +-- +-- System.InvalidOperationException: Objects with ID [redacted] of type +-- Forms$MicroflowParameterMapping do not have the same properties. +-- baseNames = Expression, Parameter, Variable; newNames = Parameter, Expression +-- +-- Reported statement (Studio Pro 11.12.2, a pluggable widget's named slot): +-- alter page LearningJourney.LearningJourney_Details { +-- set ('onClickAction': call microflow LearningJourney.ACT_ExcludeItemStep( +-- LearningJourney = $LearningJourney)) on pDSLink_ButtonAndTracking8 }; +-- +-- TWO DEFECTS, both on this statement: +-- +-- 1. An argument bound through Expression (a literal, $currentObject, a path) +-- was written WITHOUT the Variable key. Studio Pro writes all three keys and +-- nulls the unused slot; the merge library compares key sets per $ID and +-- throws. Same class as #1180 (DomainModels$NoGeneralization). Every flow +-- action and nanoflow data source shares the writer, so this was never +-- specific to ALTER. +-- +-- 2. ALTER PAGE SET built the action with an EMPTY parameter scope, so +-- `$LearningJourney` — a page parameter — was not recognised and was written +-- as the Expression "$LearningJourney" instead of Variable -> PageVariable: +-- the #1140 binding, which Studio Pro does not resolve (CE1571 on opening). +-- CREATE PAGE and ALTER PAGE INSERT/REPLACE already seeded the scope. +-- +-- REPRODUCING. `mx diff` runs the same merge library as the Changes panel: +-- +-- mxcli exec 1317-flow-arg-mapping-null-variable.mdl -p app.mpr (up to ALTER) +-- copy app.mpr + mprcontents/ aside as the base, then run the ALTER +-- mx diff base/app.mpr app.mpr out.mpr +-- +-- Measured on mxbuild 11.12.2: before the fix `mx diff` exits 129 with +-- "Objects with ID … of type Forms$MicroflowParameterMapping do not have the same +-- properties. baseNames = Parameter, Expression, Variable; newNames = Parameter, +-- Expression"; after, exit 0. `mx check` is 0 errors on BOTH — the build is not +-- a safety net for either defect. + +create module LJ; + +create persistent entity LJ.LearningJourney ( Name: String(100) ); + +create or modify layout LJ.App_Default ( layouttype: 'Responsive' ) { + scrollcontainer layoutContainer { region center { placeholder Main } } +}; + +create or modify microflow LJ.ACT_ExcludeItemStep + ($LearningJourney: LJ.LearningJourney, $Flag: Boolean) +begin + log info 'exclude'; +end; + +create or modify page LJ.LearningJourney_Details ( + Title: 'Details', Layout: LJ.App_Default, + Params: ( $LearningJourney: LJ.LearningJourney ) +) { + placeholder Main { + dataview dv (DataSource: $LearningJourney) { + actionbutton btnGo (Caption: 'Go', + Action: call microflow LJ.ACT_ExcludeItemStep(LearningJourney = $currentObject, Flag = true)) + } + } +}; + +-- The ALTER. $LearningJourney must bind through Variable (defect 2); the literal +-- `false` stays an Expression and must carry `Variable: null` (defect 1). +alter page LJ.LearningJourney_Details { + set (Action: call microflow LJ.ACT_ExcludeItemStep(LearningJourney = $LearningJourney, Flag = false)) on btnGo +}; From 983e7444f7140300354f9e00e37dab49655fe5e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 05:10:30 +0000 Subject: [PATCH 4/5] fix(alter snippet): bind an inserted widget's flow argument as a snippet parameter buildWidgetsFromAST and buildColumnSpecsFromAST seeded the stored parameter scope but never isSnippet, so a button inserted into a snippet with `call microflow M.F(P = $P)` wrote Forms$PageVariable.PageParameter instead of SnippetParameter. mxbuild 11.12.2 reports CE0115 on that button only; CREATE SNIPPET and ALTER SNIPPET SET were already correct. Set isSnippet from the mutator's container type, as convertASTAction does. Refs mendixlabs/mxcli#1317 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HamahFUo9MM2xuaNmm5mhr --- .../fix-issue/findings/mdl-executor.jsonl | 1 + .../1317-alter-snippet-insert-flow-arg.mdl | 38 ++++++++++++ mdl/executor/cmd_alter_page.go | 2 + mdl/executor/cmd_alter_page_action_test.go | 58 +++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 78100e936..0fcfdc388 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -868,3 +868,4 @@ {"area": "mdl/executor", "date": "2026-10-05", "symptom": "On Mendix 11.15, `TestMxCheck_DoctypeScripts/40-message-definition-examples.mdl` fails with CE0270 \"No root element could be found in the schema\" at MsgTest.IMM_Order. On a converted or Studio Pro 11.15 project, `list message definition collections` finds nothing, and `describe import mapping` prints no source", "cause": "Mendix 11.15 replaced `MessageDefinitions$MessageDefinitionCollection` with one `MessageDefinitions$MessageDefinition2` document per definition. `mx convert` turns the collection into a `Projects$Folder` with the same unit ID and name, and the ExposedEntity tree is byte-identical apart from $IDs. mxcli read and wrote only collections, so on 11.15 there was nothing to resolve a mapping against", "file": "`mdl/backend/modelsdk/messagedefinition_document.go` (read/write, reusing the collection's exposedNodeFromGen/messageNodeToGen), `mdl/executor/cmd_messagedefinition_documents.go` (create/describe/drop/list + version refusals), `mdl/executor/mapping_messagedefinition.go` (`findMessageDefinition(ctx, ref)` returns the stored reference), `cmd_messagedefinitions.go` (alter target resolution, association-drop guard), grammar `createMessageDefinitionStatement` / `DROP|DESCRIBE MESSAGE DEFINITION` / `LIST MESSAGE DEFINITIONS`", "insight": "**`mx convert` is the oracle for a storage change.** Converting a known 11.14 project gives a Studio Pro-authored 11.15 reference for the new shape and for how references move, so no guessing is needed. **Under the revert, 11.15 fails earlier than CE0270.** A two-part name written into the old `MessageDefinition` key makes mxbuild refuse to load the project (`StorageLoadException: ... is not a valid OldMessageDefinitionIdentifier`). The old key is typed as a three-part identifier even though 11.15 no longer reads it. The doctype script uses `-- @version: ..11.14` / `11.15+` sections, and the 11.15 mapping needs a different name from the older section's, because a plain `check` sees both sections and reports MDL-DUPDEF", "refs": ["ako/mxcli#987"]} {"area": "mdl/executor", "date": "2026-10-06", "symptom": "v0.25.0: `describe microflow` 4-8x slower than v0.24.0 on a flow arranged by hand in Studio Pro, and the cost grows faster than the flow (60 if/else blocks: 4.7 s -> 35.6 s); flows laid out by mxcli barely change", "cause": "The canonical describe's layout derivation (#748) re-renders the description every round, and a hand-laid flow takes gatedLayoutRounds+2 = 8 rounds. Each render re-ran the stored flow's split/merge analysis (findSplitMergePoints, labelCrossedMerges) and the body warnings (postDominators via microflowgraph.Analyze) - superlinear, and independent of which annotations the round keeps", "file": "`mdl/executor/cmd_microflows_derived_layout.go` (flowAnalysisMemo, installed by useDerivedFlowLayout, rebuildOnly view for the rounds), `mdl/executor/cmd_microflows_show.go` (findSplitMergePoints / labels / microflowBodyWarnings go through it)", "insight": "**Profile before believing the issue's theory.** The report blamed the round count, but rounds were already capped at 8; the rebuild itself was cheap. The CPU profile showed the cost was the *render* inside each round - the stored flow's graph analysis, redone per round. Memoising it per describe (keyed on the stored collection pointer) and leaving the `-- WARNING` comment lines out of rounds (the rebuild never reads them) gave 14.2 s -> 1.7 s at 60 blocks, with the printed description byte-identical to the unfixed build. A wall-clock assertion would be flaky; the test counts split/merge analyses per describe (18 over 8 rounds before, 2 after), with the round count as its control", "refs": ["mendixlabs/mxcli#1301"]} {"area":"mdl-executor","date":"2026-10-05","symptom":"A view entity selecting a **non-localized** DateTime column (Studio Pro's \"Localize\" unticked — the normal choice for a calendar date) passes `mxcli check --references` and exec, then `mx check` fails CE6770 \"View Entity is out of sync with the OQL Query.\" The view attribute was always written LocalizeDate = true, and MDL has no spelling for the flag, so describe -> exec re-broke it on every run","cause":"execCreateViewEntity built every view attribute with convertDataType, whose DateTime is Mendix's default LocalizeDate = true; nothing looked at the attribute the column reads","file":"`mdl/executor/oql_view_localize_date.go` (viewDateTimeLocalize), `mdl/executor/cmd_entities.go` (execCreateViewEntity); test `mdl/executor/view_entity_localize_date_test.go`; bug-test `mdl-examples/bug-tests/1297-view-entity-non-localized-datetime.mdl`","insight":"**Derive, don't add syntax**: on a view entity the column's localization is a property of the query, so the fix reads it from the source attribute and describe -> exec round-trips (`Unchanged`) with nothing new to spell — same reasoning as the view association (the column is the declaration). **Measure the shapes before choosing the scope**: one project, one view per shape, mxbuild 11.12.5 — pass-through, `s/Attr`, MIN, MAX and CASE over a non-localized source are ALL CE6770 when written localized and 0 errors when patched to false, so a pass-through-only rule (the obvious one, mirroring the string-length rule) would have fixed the report and left MAX(date) broken. Sources that disagree (coalesce of a localized and a non-localized column) are unmeasured and keep the default. **Repro needs a Studio Pro-authored flag**: mxcli writes every persistent DateTime localized, so the bug is invisible from MDL alone — the reporter's pymongo patch flipping Sale.SaleDate is the cheapest stand-in. Control: stubbing the assignment fails 5 of 6 cases with `LocalizeDate = true`; pre-fix binary on the same project gives 5 × CE6770, fixed gives 0","refs":["mendixlabs/mxcli#1297"]} +{"area": "mdl/executor", "date": "2026-10-07", "symptom": "A button added to a snippet with `alter snippet … insert` (or replace) calling `call microflow M.F(P = $P)` with a snippet parameter fails mxbuild with CE0115 \"The arguments that are passed to microflow 'M.F' do not match the expected parameters and need to be refreshed\" on that button only; the same button in CREATE SNIPPET or via ALTER SNIPPET SET is clean", "cause": "`buildWidgetsFromAST` and `buildColumnSpecsFromAST` (mdl/executor/cmd_alter_page.go) seeded paramScope and localVariables from the mutator but not `isSnippet`, so `classifyFlowArgValue` returned kind \"parameter\" and the Forms$PageVariable named PageParameter instead of SnippetParameter", "file": "`mdl/executor/cmd_alter_page.go` (isSnippet from mutator.ContainerType() in both builders)", "insight": "**A pageBuilder built outside CREATE needs THREE scope fields, not two** — paramScope, localVariables and isSnippet; #1317 found the SET builder missing all three and this one missing the last. When adding a builder, copy the seeding from convertASTAction rather than from memory. **Unlike the Expression-form defect (#1140/#1317), mxbuild DOES catch the wrong slot** — CE0115 rather than CE1571 — so one script exercising CREATE, INSERT and SET on the same snippet with `mx check` is a complete A/B: only the broken path's button is named. Verified on 11.12.2: 1 error before, 0 after", "refs": ["mendixlabs/mxcli#1317", "mendixlabs/mxcli#1140"], "ce": ["CE0115"]} diff --git a/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl b/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl new file mode 100644 index 000000000..8cebda9c1 --- /dev/null +++ b/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl @@ -0,0 +1,38 @@ +mdl 1; +-- Follow-up to mendixlabs/mxcli#1317 — ALTER SNIPPET INSERT/REPLACE bound a +-- snippet-parameter flow argument as a PAGE parameter. +-- +-- buildWidgetsFromAST / buildColumnSpecsFromAST seeded the stored parameter scope +-- but never set isSnippet, so classifyFlowArgValue returned kind "parameter" and +-- the mapping's Forms$PageVariable carried PageParameter: "Order" inside a +-- snippet. CREATE SNIPPET and ALTER SNIPPET SET (fixed in #1317) bind the same +-- argument as SnippetParameter: "Order". +-- +-- Measured on mxbuild 11.12.2 with this script on a fresh project: +-- before: [CE0115] "The arguments that are passed to microflow 'SN.ACT_Use' do +-- not match the expected parameters and need to be refreshed." at +-- Action button 'btnInsert' -> 1 error, on the INSERTED button only +-- after: 0 errors; all three buttons bind SnippetParameter: "Order" +-- +-- btnCreate (CREATE path) and btnSet (SET path) are the controls. + +create module SN; +create persistent entity SN.Order ( Code: String(20) ); +create or modify microflow SN.ACT_Use ($Order: SN.Order) +begin + log info 'use'; +end; +create snippet SN.SNIPPET_Order ( params: ( $Order: SN.Order ) ) { + container ctn { + actionbutton btnCreate (Caption: 'Create', Action: call microflow SN.ACT_Use(Order = $Order)) + actionbutton btnSet (Caption: 'Set', Action: call microflow SN.ACT_Use(Order = empty)) + } +}; +alter snippet SN.SNIPPET_Order { + insert after btnCreate { + actionbutton btnInsert (Caption: 'Insert', Action: call microflow SN.ACT_Use(Order = $Order)) + } +}; +alter snippet SN.SNIPPET_Order { + set (Action: call microflow SN.ACT_Use(Order = $Order)) on btnSet +}; diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index 6b75e4022..4498bacc7 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -798,6 +798,7 @@ func buildColumnSpecsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleNa themeRegistry: ctx.GetThemeRegistry(), widgetBackend: ctx.Backend, localVariables: storedPageVariables(mutator), + isSnippet: mutator.ContainerType() == backend.ContainerSnippet, } var result []*backend.DataGridColumnSpec @@ -892,6 +893,7 @@ func buildWidgetsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName s themeRegistry: ctx.GetThemeRegistry(), widgetBackend: ctx.Backend, localVariables: storedPageVariables(mutator), + isSnippet: mutator.ContainerType() == backend.ContainerSnippet, } var result []pages.Widget diff --git a/mdl/executor/cmd_alter_page_action_test.go b/mdl/executor/cmd_alter_page_action_test.go index 02193f956..52f30cae5 100644 --- a/mdl/executor/cmd_alter_page_action_test.go +++ b/mdl/executor/cmd_alter_page_action_test.go @@ -249,3 +249,61 @@ func TestAlterPage_SetNamedAction_PageParamBindsThroughVariable(t *testing.T) { }) } } + +// The INSERT/REPLACE half of #1317's scope fix. buildWidgetsFromAST and +// buildColumnSpecsFromAST seeded the parameter scope but never isSnippet, so a +// button inserted into a snippet bound its `$Order` argument as a PageParameter. +// mxbuild 11.12.2 reports it as CE0115 ("arguments … do not match the expected +// parameters") on the inserted button only; CREATE SNIPPET and ALTER SET bind +// the same argument as SnippetParameter. +func TestAlterSnippet_InsertedFlowArgBindsSnippetParameter(t *testing.T) { + mod := mkModule("SN") + mf := mkMicroflow(mod.ID, "ACT_Use") + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListMicroflowsFunc: func() ([]*microflows.Microflow, error) { return []*microflows.Microflow{mf}, nil }, + } + h := mkHierarchy(mod) + withContainer(h, mf.ContainerID, mod.ID) + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + + for _, tc := range []struct { + container backend.ContainerKind + wantKind string + }{ + {backend.ContainerSnippet, "snippet"}, + {backend.ContainerPage, "parameter"}, // control: a page keeps PageParameter + } { + t.Run(string(tc.container), func(t *testing.T) { + mut := &mock.MockPageMutator{ + ContainerTypeFunc: func() backend.ContainerKind { return tc.container }, + ParamScopeFunc: func() (map[string]model.ID, map[string]string) { + return map[string]model.ID{"Order": "p1"}, map[string]string{"Order": "SN.Order"} + }, + WidgetScopeFunc: func() map[string]model.ID { return map[string]model.ID{} }, + } + btn := &ast.WidgetV3{Type: "actionbutton", Name: "btnInsert", Properties: map[string]any{ + "Caption": "Insert", + "Action": &ast.ActionV3{Type: "microflow", Target: "SN.ACT_Use", + Args: []ast.FlowArgV3{{Name: "Order", Value: "$Order"}}}, + }} + ws, err := buildWidgetsFromAST(ctx, []*ast.WidgetV3{btn}, "SN", mod.ID, "", mut) + assertNoError(t, err) + if len(ws) != 1 { + t.Fatalf("widgets = %d, want 1", len(ws)) + } + b, ok := ws[0].(*pages.ActionButton) + if !ok { + t.Fatalf("widget = %T, want *pages.ActionButton", ws[0]) + } + mfa, ok := b.Action.(*pages.MicroflowClientAction) + if !ok || len(mfa.ParameterMappings) != 1 { + t.Fatalf("action = %#v, want one microflow mapping", b.Action) + } + if got := mfa.ParameterMappings[0].VariableKind; got != tc.wantKind { + t.Errorf("VariableKind = %q, want %q — mxbuild reports CE0115 on the button", got, tc.wantKind) + } + }) + } +} From c01aad8a3c593c3468dc6b0e42f07615e29af31e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 07:11:32 +0000 Subject: [PATCH 5/5] test(alter snippet): cover the data grid column builder in the #1317 snippet case The isSnippet fix also reaches buildColumnSpecsFromAST. Add a column with a flow button inserted into a grid inside a snippet. Measured on mxbuild 11.12.2: before, PageParameter and CE0115 at the button; after, SnippetParameter and 0 errors. Refs mendixlabs/mxcli#1317 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HamahFUo9MM2xuaNmm5mhr --- .../1317-alter-snippet-insert-flow-arg.mdl | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl b/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl index 8cebda9c1..eeb9162b6 100644 --- a/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl +++ b/mdl-examples/bug-tests/1317-alter-snippet-insert-flow-arg.mdl @@ -36,3 +36,31 @@ alter snippet SN.SNIPPET_Order { alter snippet SN.SNIPPET_Order { set (Action: call microflow SN.ACT_Use(Order = $Order)) on btnSet }; + +-- The same defect through the DATA GRID COLUMN builder (buildColumnSpecsFromAST): +-- a flow button in a column inserted into a grid inside a snippet. Measured on +-- mxbuild 11.12.2: before, PageParameter: "Order" and CE0115 at btnCol; after, +-- SnippetParameter: "Order" and 0 errors. $currentObject stays an Expression. + +create module SC; +create persistent entity SC.Order ( Code: String(20) ); +create persistent entity SC.Line ( Qty: Integer ); +create or modify microflow SC.ACT_Use ($Order: SC.Order, $Line: SC.Line) +begin + log info 'use'; +end; +create snippet SC.SNIPPET_Lines ( params: ( $Order: SC.Order ) ) { + container ctn { + datagrid dgLines ( datasource: database from SC.Line ) { + column (attribute: Qty, caption: 'Qty') + } + } +}; +alter snippet SC.SNIPPET_Lines { + insert after dgLines.Qty { + column Act ( attribute: Qty, caption: 'Act', ShowContentAs: customContent ) { + actionbutton btnCol (Caption: 'Use', + Action: call microflow SC.ACT_Use(Order = $Order, Line = $currentObject)) + } + } +};